dotnet / dotnet/runtime

Regex source generator produces CS1027 for conditional modifiers and generic headers

Open
#133,454 2 comments 0 reactions 0 assignees View on GitHub
area-System.Text.RegularExpressions
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

### Description

The Regex source generator can copy an opening `#if` from a valid user declaration into generated source without copying its matching `#endif`. The resulting generated C# fails to compile with CS1027.

Confirmed scenarios are conditional boundaries inside a method's modifier list, inside a property's modifier list, and inside a containing type's generic parameter list. Each example below has balanced directives in the original source and reproduces with `TRIVIA_BRANCH` both defined and undefined.

### Reproduction Steps

With the Regex source generator enabled, compile each snippet separately. Compile once with `TRIVIA_BRANCH` defined as a compilation symbol and once with it undefined. The property example requires a language version supporting partial properties; the validation used C# preview with .NET 11.

**1. Conditional boundary inside method modifiers**

```csharp
using System.Text.RegularExpressions;

namespace TriviaValidation;

public partial class Test
{
[GeneratedRegex("a")]
public
#if TRIVIA_BRANCH
static partial Regex Method();
#else
static partial Regex Method2();
#endif
}
```

**2. Conditional boundary inside property modifiers**

```csharp
using System.Text.RegularExpressions;

namespace TriviaValidation;

public partial class Test
{
[GeneratedRegex("a")]
public
#if TRIVIA_BRANCH
static partial Regex Property { get; }
#else
static partial Regex Property2 { get; }
#endif
}
```

**3. Conditional boundary crossing the end of a containing type's generic parameter list**

```csharp
using System.Text.RegularExpressions;

namespace TriviaValidation;

public partial class Test
#else
V>
#endif
{
[GeneratedRegex("a")]
public static partial Regex Method();
}
```

### Expected behavior

All snippets compile in either symbol configuration. The generator implements the active partial member within the correct containing type without introducing unmatched preprocessor directives.

### Actual behavior

All three scenarios fail in both symbol configurations: six failing builds, each reporting this error in the emitted `RegexGenerator.g.cs`:

```text
error CS1027: #endif directive expected
```

The generator does run and emit source. In the first-branch modifier case, the generated declaration retains `public`, `#if TRIVIA_BRANCH`, and `static partial`, but not the later `#else` or `#endif`. With the second branch active, the captured text also retains the disabled first branch and `#else`, but still omits the final `#endif`.

The generic-header case likewise retains the opening directive within `<...>` while omitting the closing directive following `>`.

### Regression?

Unknown. Earlier versions were not tested or bisected.

### Known Workarounds

Keep conditional boundaries outside the entire modifier list or generic parameter list. For example, place `public` inside each branch rather than before `#if`, or place the complete generic type header inside each branch.

Controls with the entire method declaration conditional compiled successfully. Cases with both opening and closing directives wholly inside the copied modifier/type-parameter span also compiled successfully when the compilation symbol was shared with generated source.

### Configuration

- .NET SDK `11.0.100-rc.1.26420.103`; consumer target framework `net11.0`; C# preview.
- Windows x64.
- `System.Text.RegularExpressions.Generator` built from runtime commit `53b238dbc5ff78e72c3f15d4a7b5328bbb013aa3`, targeting `netstandard2.0`, Debug configuration.
- The checkout-built analyzer was explicitly supplied to the compiler, excluding implicit SDK analyzers. These are not claims about a separately tested released NuGet/SDK generator version.
- Fresh compiler processes were used. Other SDK versions and operating systems were not tested.

### Other information

The relevant captures are in [RegexGenerator.Parser.cs](https://github.com/dotnet/runtime/blob/53b238dbc5ff78e72c3f15d4a7b5328bbb013aa3/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Parser.cs#L202-L243): `memberSyntax.Modifiers.ToString()` and the interpolation of `typeDec.TypeParameterList` / `parent.TypeParameterList` into the type name. [EmitRegexPartialMethod](https://github.com/dotnet/runtime/blob/53b238dbc5ff78e72c3f15d4a7b5328bbb013aa3/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Emitter.cs#L60-L114) writes these strings back into the output.

Stringifying a token list or syntax node preserves internal trivia while dropping trivia outside the selected span. Here, the selected span contains the opening directive but ends before its matching closing directive.

Containing-type modifier splits did not reproduce this failure, because those modifier lists are not copied into the generated containing declarations.

> [!NOTE]
> This report and its reproductions were prepared with GitHub Copilot.

Contributor guide

Open the contributing guide

Research direction

Reproduce the six compilation failures with RegexGenerator.Parser.cs and RegexGenerator.Emitter.cs in the reported checkout, comparing the modifier and type-parameter captures with the emitted source. Trace the spans around memberSyntax.Modifiers and the containing TypeParameterList, then verify that all three conditional-boundary scenarios compile with the symbol both defined and undefined without an unmatched directive.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
compilers
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.