Fix Roslyn step-definition discovery to honor Cucumber Expression alternation and optional text - #486
Merged
clrudolphi merged 3 commits intoAug 26, 2026
Conversation
…ernation and optional text (#476) BuildRegex previously escaped '/' and '()' as literal text, so a Roslyn-discovered binding using alternative text (cat/dog) or optional text (eat(s)) never matched what the connector's runtime-computed regex matched, causing spurious undefined-step diagnostics that reasserted on every .cs edit. BuildRegex now parses the expression for whitespace-scoped alternation and nested optional groups (with backslash escaping), mirroring real Cucumber Expression semantics. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…parser (#476) Replaces the hand-rolled alternation/optional-text parser added for #476 with the real Cucumber.CucumberExpressions library (Cucumber.CucumberExpressions v17.1.0, the exact version Reqnroll 3.2.0 itself depends on) via its public CucumberExpression class -- the same one Reqnroll's runtime uses for this exact purpose -- so LSP.Core's regex generation is guaranteed to agree with the connector-discovered path by construction rather than by a second, independently maintained implementation staying in sync (option 2 from the original issue). Two small pieces the package doesn't provide were ported (not re-derived) from Reqnroll's own runtime, verified against a local Reqnroll source checkout: - CucumberExpressionDetector: the plain-regex vs. Cucumber-Expression heuristic, which lives in the Reqnroll runtime assembly itself (not the CucumberExpressions package) -- LSP.Core deliberately doesn't reference that assembly so discovery can run pre-build. - LspCucumberExpressionParameterTypeRegistry: a minimal IParameterTypeRegistry using the exact same regex fragments as Reqnroll's own registry (ParameterTypeConstants), since the real registry builds its parameter-type set by reflecting over already-compiled binding methods -- information that doesn't exist yet during syntax-only discovery. Unknown names (custom [StepArgumentTransformation] types, enums) fall back to the permissive `.*` match instead of throwing, preserving the existing fallback behavior. Also hardens BuildRegex against the real grammar's stricter validation (e.g. "an alternative may not be empty") throwing for malformed input the old hand-rolled version silently tolerated: caught and surfaced as an invalid (null-regex) binding rather than aborting discovery of the whole file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…parameter type
- BuildRegex caught bare Exception, which would silently turn a genuine bug (e.g. an NRE in
LspCucumberExpressionParameterTypeRegistry) into "just an invalid binding" with no diagnostic
trail. Narrowed to CucumberExpressionException (the grammar's own validation) and
ArgumentException (covers RegexParseException from the plain-regex branch) so anything
unexpected propagates instead of being swallowed.
- LspCucumberExpressionParameterTypeRegistry registered "short" as a known Cucumber parameter
type, but verified against a local Reqnroll checkout that Reqnroll's real registry only
aliases int/float/double/byte/long/decimal to their C# keywords -- {short} resolves solely via
its CLR type name Int16 (RuntimeBindingType.Name => Type.Name), so it's genuinely undefined in
a real Reqnroll project. Recognizing it here would have falsely validated {short} via Roslyn
discovery while the connector-discovered path leaves it undefined -- the exact class of
Roslyn-vs-connector divergence #476 exists to fix, just for a different name. Removed; it now
falls back to the wildcard match like any other unrecognized type name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
clrudolphi
deleted the
fix/476-cucumber-expression-alternation-optional
branch
August 26, 2026 14:28
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
StepDefinitionFileParser.BuildRegextreated/(alternative text) and()(optional text) as literal characters, so a Roslyn-discovered binding's regex diverged from the connector-discovered regex (computed by Reqnroll's real runtime) for any Cucumber Expression using those constructs.Cucumber.CucumberExpressionslibrary (v17.1.0 — the exact version Reqnroll 3.2.0 itself depends on) via its publicCucumberExpressionclass, the same one Reqnroll's runtime uses for this. This is option 2 from the original issue: LSP.Core's regex generation is guaranteed to agree with the connector-discovered path by construction, rather than by two independently-maintained implementations staying in sync. (An earlier revision of this PR hand-rolled the alternation/optional grammar instead — replaced once it became clear taking the dependency was straightforward.)CucumberExpressionDetector— the plain-regex-vs-Cucumber-Expression heuristic. This lives in theReqnrollruntime assembly itself, not inCucumber.CucumberExpressions, and LSP.Core deliberately doesn't reference that assembly so discovery can run pre-build.LspCucumberExpressionParameterTypeRegistry— a minimalIParameterTypeRegistryusing the exact same regex fragments as Reqnroll's own registry (ParameterTypeConstants). Reqnroll's real registry builds its parameter-type set by reflecting over already-compiled binding methods, which doesn't exist yet during syntax-only discovery; unknown names (custom[StepArgumentTransformation]types, enums) fall back to a permissive.*match instead of throwing, preserving today's behavior.BuildRegexagainst the real grammar's stricter validation (e.g. "an alternative may not be empty") throwing for malformed input the old hand-rolled version silently tolerated — caught and surfaced as an invalid (null-regex) binding rather than aborting discovery of the whole file.{param}→ treat as plain regex" contract is preserved viaCucumberExpressionDetector, so hand-written regex bindings using bare(...)capturing groups keep working exactly as before.Fixes #476
Test plan
StepDefinitionFileParserTestscovering:/alternation,()optional text, alternation+optional combined, backslash-escaped syntax, standard parameter types (now asserted by match behavior rather than exact regex text, since the pattern text is the library's own),{word}not matching across a space, a custom/unknown parameter type falling back to wildcard, and a malformed expression degrading to an invalid binding instead of throwing.dotnet test tests/LSP/Reqnroll.IdeSupport.LSP.Core.Tests— 673 passed, 0 failed, 1 skipped (pre-existing skip).dotnet test— all green.🤖 Generated with Claude Code