dotnet / dotnet/runtime

Options validation generator produces CS1027 for conditional enclosing generic headers

Open
#133,455 3 comments 0 reactions 1 assignee Claimed by @rosebyte View on GitHub
area-Extensions-Options
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

### Description

The Options validation source generator can copy an opening `#if` from an enclosing type's generic parameter list without the matching `#endif`. A valid nested `[OptionsValidator]` declaration then produces generated C# that fails with CS1027.

The confirmed scenario is a conditional block that begins inside the enclosing type's `<...>` list and ends after its closing `>`. The original user source has balanced directives and represents a valid declaration in either symbol configuration.

### Reproduction Steps

Reference `Microsoft.Extensions.Options` with the Options validation source generator enabled. Compile the following snippet once with `TRIVIA_BRANCH` defined as a compilation symbol and once with it undefined:

```csharp
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;

namespace TriviaValidation;

public sealed class OptionsOuterTypeParametersModel
{
[Range(0, 10)]
public int Value { get; set; } = 1;
}

public partial class OptionsOuterTypeParameters<
#if TRIVIA_BRANCH
T>
#else
T>
#endif
{
[OptionsValidator]
public sealed partial class Validator : IValidateOptions
{
}
}
```

The intentionally identical branches isolate the effect of directive placement rather than changing the validator's type parameters.

### Expected behavior

The snippet compiles in either symbol configuration. The generator emits the validator inside a correctly reconstructed enclosing generic type, without unmatched preprocessor directives.

### Actual behavior

Both symbol configurations fail with this error in the emitted `Validators.g.cs`:

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

Source generation does run. The generated enclosing declaration preserves the opening `#if` within the copied generic parameter list, but omits the matching `#endif` that follows `>` in the original file. With the second branch active, the copied span also includes disabled first-branch text and `#else`, still without the closing `#endif`.

### Regression?

Unknown. Earlier versions were not tested or bisected.

### Known Workarounds

Avoid splitting the enclosing type's generic parameter list across a conditional boundary. Use an unconditional generic header, or put the complete enclosing type header inside each conditional branch rather than placing `#if` inside `<...>` and `#endif` after it.

### Configuration

- .NET SDK `11.0.100-rc.1.26420.103`; consumer target framework `net11.0`; C# preview.
- Windows x64.
- `Microsoft.Extensions.Options.SourceGeneration` built from runtime commit `53b238dbc5ff78e72c3f15d4a7b5328bbb013aa3`, targeting `netstandard2.0`, Debug configuration.
- The checkout-built analyzer was explicitly supplied to the compiler, excluding implicit SDK analyzers. Consumer references came from the .NET/ASP.NET Core 11 reference packs.
- Fresh compiler processes were used. Released package versions, other SDK versions, and other operating systems were not tested.

### Other information

The enclosing declaration is constructed in [Parser.cs](https://github.com/dotnet/runtime/blob/53b238dbc5ff78e72c3f15d4a7b5328bbb013aa3/src/libraries/Microsoft.Extensions.Options/gen/Parser.cs#L163-L181) using the parent identifier, `parent.TypeParameterList`, and `parent.ConstraintClauses`. [Emitter.cs](https://github.com/dotnet/runtime/blob/53b238dbc5ff78e72c3f15d4a7b5328bbb013aa3/src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs#L110-L123) writes the captured enclosing declarations.

Stringifying `parent.TypeParameterList` retains its internal directive trivia, but the span ends at `>` and therefore excludes a later matching `#endif`.

The ordinary validator control passed. Target/enclosing modifier-list splits and a generic-parameter split on the validator type itself also passed. The target validator name is obtained from symbols, whereas the enclosing type's generic list is copied from syntax. Enclosing constraint-clause boundaries were not separately tested; the repro here is specifically for the enclosing generic parameter list.

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

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.