Options validation generator produces CS1027 for conditional enclosing generic headers
- 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
Assessment
This issue has not been assessed yet.