Logging source generator produces CS1027 for conditional modifiers and generic headers
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Description
The `LoggerMessage` source generator can copy an opening `#if` from valid user syntax without its matching `#endif`, producing generated C# that fails with CS1027.
Confirmed scenarios are conditional boundaries inside logging-method modifiers, the logging type's generic parameter list, and an enclosing 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
Reference `Microsoft.Extensions.Logging.Abstractions` with its source generator enabled. Compile each snippet separately, once with `TRIVIA_BRANCH` defined as a compilation symbol and once with it undefined.
**1. Conditional boundary inside logging-method modifiers**
```csharp
using Microsoft.Extensions.Logging;
namespace TriviaValidation;
public static partial class LoggingMethodModifiers
{
[LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Value {value}")]
public
#if TRIVIA_BRANCH
static partial void Log(ILogger logger, int value);
#else
static partial void Log(ILogger logger, int value);
#endif
}
```
**2. Conditional boundary crossing the end of the logging type's generic parameter list**
```csharp
using Microsoft.Extensions.Logging;
namespace TriviaValidation;
public static partial class LoggingTargetTypeParameters<
#if TRIVIA_BRANCH
T>
#else
T>
#endif
{
[LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Value {value}")]
public static partial void Log(ILogger logger, int value);
}
```
**3. Conditional boundary crossing the end of an enclosing type's generic parameter list**
```csharp
using Microsoft.Extensions.Logging;
namespace TriviaValidation;
public partial class LoggingOuterTypeParameters<
#if TRIVIA_BRANCH
T>
#else
T>
#endif
{
public static partial class Logger
{
[LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Value {value}")]
public static partial void Log(ILogger logger, int value);
}
}
```
The intentionally identical branches in these minimal examples isolate the effect of directive placement rather than changing the logging signature.
### Expected behavior
All snippets compile in either symbol configuration. The generator should implement the active logging method without introducing unmatched preprocessor directives into its method or containing declarations.
### Actual behavior
All three scenarios fail in both symbol configurations: six failing builds, each reporting this error in the emitted `LoggerMessage.g.cs`:
```text
error CS1027: #endif directive expected
```
Source generation does run. The generated method/type header retains the opening `#if` inside the copied span, while the matching `#endif` after the original signature or `>` is omitted. With the second branch active, disabled first-branch text and `#else` can also be copied, still without the closing `#endif`.
### Regression?
Unknown. Earlier versions were not tested or bisected.
### Known Workarounds
Keep conditional boundaries outside the complete modifier list and generic parameter list. For the method case, move `public` inside each conditional branch. For the type cases, avoid splitting the generic header across directive boundaries, for example by making the complete header conditional instead.
### Configuration
- .NET SDK `11.0.100-rc.1.26420.103`; consumer target framework `net11.0`; C# preview.
- Windows x64.
- `Microsoft.Extensions.Logging.Generators.Roslyn4.4.csproj` built from runtime commit `53b238dbc5ff78e72c3f15d4a7b5328bbb013aa3`, targeting `netstandard2.0`, Debug configuration.
- The resulting `Microsoft.Extensions.Logging.Generators.dll` 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
[LoggerMessageGenerator.Parser.cs](https://github.com/dotnet/runtime/blob/53b238dbc5ff78e72c3f15d4a7b5328bbb013aa3/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs#L242-L254) captures method modifiers with `method.Modifiers.ToString()`. [GenerateClassName](https://github.com/dotnet/runtime/blob/53b238dbc5ff78e72c3f15d4a7b5328bbb013aa3/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs#L650-L675) combines the identifier with the type-parameter syntax for both the target and enclosing types. These captures preserve internal trivia, including directives, but their spans need not include the corresponding closing directive.
The controls passed. Splitting target/enclosing type modifiers did not reproduce the failure, because those modifier lists are not copied. A conditional boundary in a logging method's generic parameter list also passed: that list is reconstructed from type-parameter symbols rather than copied as raw syntax.
> [!NOTE]
> This report and its reproductions were prepared with GitHub Copilot.
Contributor guide
Assessment
This issue has not been assessed yet.