Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e95c4ad
Fix source generator diagnostics to support #pragma warning disable
eiriktsarpalis Feb 27, 2026
6506135
Add tests verifying diagnostic LocationKind.SourceFile for all genera…
eiriktsarpalis Feb 28, 2026
4c7bb58
Add incremental generation tests for RegexGenerator
eiriktsarpalis Feb 28, 2026
7a2b000
Refactor diagnostic emission lambdas into named EmitDiagnostics methods
eiriktsarpalis Mar 1, 2026
a4e500e
Extract source model projections into explicit IVP bindings
eiriktsarpalis Mar 2, 2026
091625e
Extract diagnostic pipelines into explicit IVP projections
eiriktsarpalis Mar 2, 2026
bdfaf17
Restructure Regex generator to use deeply equatable result record
eiriktsarpalis Mar 3, 2026
8e7da78
Restructure Regex generator to use deeply equatable result record
eiriktsarpalis Mar 3, 2026
8547065
Simplify Regex pipeline with mutable accumulator pattern
eiriktsarpalis Mar 4, 2026
b34eef0
Eliminate per-item diagnostic arrays in Regex generator pipeline
eiriktsarpalis Mar 10, 2026
96b5918
Use nullable ImmutableArray.Builder for diagnostics in ParseAndGenera…
eiriktsarpalis Mar 10, 2026
ad8f67d
Restore accidentally removed runtextpos assignment in FindFirstChar e…
eiriktsarpalis Mar 10, 2026
d1f6520
Fix logger generator incrementality with ImmutableEquatableArray
eiriktsarpalis Mar 10, 2026
440461f
Standardize on ImmutableArray.Builder over List<> for building Immuta…
eiriktsarpalis Mar 10, 2026
b1a6b90
Move diagnostic deduplication into collect phase of logger pipeline
eiriktsarpalis Mar 10, 2026
758d2df
Improve pragma suppression tests and remove unused shared DiagnosticInfo
eiriktsarpalis Mar 10, 2026
1db4a0c
Test pragma suppression with dual-location diagnostics
eiriktsarpalis Mar 10, 2026
8f9bbcd
Minimize Regex generator diff: segregate diagnostics from source output
eiriktsarpalis Mar 10, 2026
850a8ea
Refine Regex generator Results to ImmutableEquatableArray without dia…
eiriktsarpalis Mar 10, 2026
cb51e3b
Revert "Refine Regex generator Results to ImmutableEquatableArray wit…
eiriktsarpalis Mar 10, 2026
e0708f2
Remove redundant line.
eiriktsarpalis Mar 10, 2026
0acbfb2
Refine Regex generator Results to filtered ImmutableArray without dia…
eiriktsarpalis Mar 10, 2026
8f2157f
Lift diagnostics IVP projection into named variable binding
eiriktsarpalis Mar 10, 2026
baf2d63
Replace var with explicit type for collected pipeline variable
eiriktsarpalis Mar 10, 2026
1d6b1bc
Remove Location from RegexPatternAndSyntax and RegexMethod records
eiriktsarpalis Mar 10, 2026
1708293
Remove unnecessary (object) casts from second Select lambda
eiriktsarpalis Mar 10, 2026
466a960
Lift SupportsCodeGeneration check into first Select, eliminating (Reg…
eiriktsarpalis Mar 10, 2026
3637180
Restore Location in RegexPatternAndSyntax record
eiriktsarpalis Mar 10, 2026
0bd7f87
Merge branch 'main' into fix/sourcegen-suppressions
eiriktsarpalis Mar 10, 2026
5962188
Address ericstj feedback: remove DiagnosticInfo.cs, fix dedup, add tests
eiriktsarpalis Mar 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 0 additions & 60 deletions src/libraries/Common/src/SourceGenerators/DiagnosticInfo.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ internal sealed partial class Parser(CompilationData compilationData)
private bool _emitEnumParseMethod;
private bool _emitGenericParseEnum;

public List<DiagnosticInfo>? Diagnostics { get; private set; }
public List<Diagnostic>? Diagnostics { get; private set; }

public SourceGenerationSpec? GetSourceGenerationSpec(ImmutableArray<BinderInvocation?> invocations, CancellationToken cancellationToken)
{
if (!_langVersionIsSupported)
{
RecordDiagnostic(DiagnosticDescriptors.LanguageVersionNotSupported, trimmedLocation: Location.None);
RecordDiagnostic(DiagnosticDescriptors.LanguageVersionNotSupported, location: Location.None);
return null;
}

Expand Down Expand Up @@ -979,10 +979,10 @@ private void ReportContainingTypeDiagnosticIfRequired(TypeParseInfo typeParseInf
}
}

private void RecordDiagnostic(DiagnosticDescriptor descriptor, Location trimmedLocation, params object?[]? messageArgs)
private void RecordDiagnostic(DiagnosticDescriptor descriptor, Location location, params object?[]? messageArgs)
{
Diagnostics ??= new List<DiagnosticInfo>();
Diagnostics.Add(DiagnosticInfo.Create(descriptor, trimmedLocation, messageArgs));
Diagnostics ??= new List<Diagnostic>();
Diagnostics.Add(Diagnostic.Create(descriptor, location, messageArgs));
}

private void CheckIfToEmitParseEnumMethod()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
using System.Diagnostics;
using System.Reflection;
using System.Threading;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using SourceGenerators;

namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
{
Expand Down Expand Up @@ -37,7 +37,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
? new CompilationData((CSharpCompilation)compilation)
: null);

IncrementalValueProvider<(SourceGenerationSpec?, ImmutableEquatableArray<DiagnosticInfo>?)> genSpec = context.SyntaxProvider
IncrementalValueProvider<(SourceGenerationSpec?, ImmutableArray<Diagnostic>)> genSpec = context.SyntaxProvider
.CreateSyntaxProvider(
(node, _) => BinderInvocation.IsCandidateSyntaxNode(node),
BinderInvocation.Create)
Expand All @@ -48,14 +48,16 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
{
if (tuple.Right is not CompilationData compilationData)
{
return (null, null);
return (null, ImmutableArray<Diagnostic>.Empty);
}

try
{
Parser parser = new(compilationData);
SourceGenerationSpec? spec = parser.GetSourceGenerationSpec(tuple.Left, cancellationToken);
ImmutableEquatableArray<DiagnosticInfo>? diagnostics = parser.Diagnostics?.ToImmutableEquatableArray();
ImmutableArray<Diagnostic> diagnostics = parser.Diagnostics is { } diags
? diags.ToImmutableArray()
: ImmutableArray<Diagnostic>.Empty;
return (spec, diagnostics);
}
catch (Exception ex)
Expand All @@ -65,7 +67,26 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
})
.WithTrackingName(GenSpecTrackingName);

context.RegisterSourceOutput(genSpec, ReportDiagnosticsAndEmitSource);
// Project the combined pipeline result to just the equatable model, discarding diagnostics.
// SourceGenerationSpec implements value equality, so Roslyn's Select operator will compare
// successive model snapshots and only propagate changes downstream when the model structurally
// differs. This ensures source generation is fully incremental: re-emitting code only when
// the binding spec actually changes, not on every keystroke or positional shift.
IncrementalValueProvider<SourceGenerationSpec?> sourceGenerationSpec =
genSpec.Select(static (t, _) => t.Item1);

context.RegisterSourceOutput(sourceGenerationSpec, EmitSource);

// Project to just the diagnostics, discarding the model. ImmutableArray<Diagnostic> does not
// implement value equality, so Roslyn's incremental pipeline uses reference equality for these
// values — the callback fires on every compilation change. This is by design: diagnostic
// emission is cheap, and we need fresh SourceLocation instances that are pragma-suppressible
// (cf. https://github.com/dotnet/runtime/issues/92509).
// No source code is generated from this pipeline — it exists solely to report diagnostics.
IncrementalValueProvider<ImmutableArray<Diagnostic>> diagnostics =
genSpec.Select(static (t, _) => t.Item2);

context.RegisterSourceOutput(diagnostics, EmitDiagnostics);

if (!s_hasInitializedInterceptorVersion)
{
Expand Down Expand Up @@ -136,17 +157,17 @@ internal static int DetermineInterceptableVersion()
/// </summary>
public Action<SourceGenerationSpec>? OnSourceEmitting { get; init; }

private void ReportDiagnosticsAndEmitSource(SourceProductionContext sourceProductionContext, (SourceGenerationSpec? SourceGenerationSpec, ImmutableEquatableArray<DiagnosticInfo>? Diagnostics) input)
private static void EmitDiagnostics(SourceProductionContext context, ImmutableArray<Diagnostic> diagnostics)
{
if (input.Diagnostics is ImmutableEquatableArray<DiagnosticInfo> diagnostics)
foreach (Diagnostic diagnostic in diagnostics)
{
foreach (DiagnosticInfo diagnostic in diagnostics)
{
sourceProductionContext.ReportDiagnostic(diagnostic.CreateDiagnostic());
}
context.ReportDiagnostic(diagnostic);
}
}

if (input.SourceGenerationSpec is SourceGenerationSpec spec)
private void EmitSource(SourceProductionContext sourceProductionContext, SourceGenerationSpec? spec)
{
if (spec is not null)
{
OnSourceEmitting?.Invoke(spec);
Emitter emitter = new(spec);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
<Compile Include="$(CommonPath)\Roslyn\DiagnosticDescriptorHelper.cs" Link="Common\Roslyn\DiagnosticDescriptorHelper.cs" />
<Compile Include="$(CommonPath)\Roslyn\GetBestTypeByMetadataName.cs" Link="Common\Roslyn\GetBestTypeByMetadataName.cs" />
<Compile Include="$(CommonPath)\SourceGenerators\CSharpSyntaxUtilities.cs" Link="Common\SourceGenerators\CSharpSyntaxUtilities.cs" />
<Compile Include="$(CommonPath)\SourceGenerators\DiagnosticInfo.cs" Link="Common\SourceGenerators\DiagnosticInfo.cs" />
<Compile Include="$(CommonPath)\SourceGenerators\ImmutableEquatableArray.cs" Link="Common\SourceGenerators\ImmutableEquatableArray.cs" />
<Compile Include="$(CommonPath)\SourceGenerators\SourceWriter.cs" Link="Common\SourceGenerators\SourceWriter.cs" />
<Compile Include="$(CommonPath)\SourceGenerators\TypeModelHelper.cs" Link="Common\SourceGenerators\TypeModelHelper.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -414,5 +415,129 @@ public class AnotherGraphWithUnsupportedMembers
Assert.True(result.Diagnostics.Any(diag => diag.Id == Diagnostics.TypeNotSupported.Id));
Assert.True(result.Diagnostics.Any(diag => diag.Id == Diagnostics.PropertyNotSupported.Id));
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNetCore))]
public async Task Diagnostic_HasPragmaSuppressibleLocation()
{
// SYSLIB1103: ValueTypesInvalidForBind (Warning, configurable).
string source = """
#pragma warning disable SYSLIB1103
using System;
using Microsoft.Extensions.Configuration;

public class Program
{
public static void Main()
{
ConfigurationBuilder configurationBuilder = new();
IConfigurationRoot config = configurationBuilder.Build();

int myInt = 1;
config.Bind(myInt);
}
}
""";

ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source);
var effective = CompilationWithAnalyzers.GetEffectiveDiagnostics(result.Diagnostics, result.OutputCompilation);
Diagnostic diagnostic = Assert.Single(effective, d => d.Id == "SYSLIB1103");
Assert.True(diagnostic.IsSuppressed);
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNetCore))]
public async Task Diagnostic_NoPragma_IsNotSuppressed()
{
string source = """
using System;
using Microsoft.Extensions.Configuration;

public class Program
{
public static void Main()
{
ConfigurationBuilder configurationBuilder = new();
IConfigurationRoot config = configurationBuilder.Build();

int myInt = 1;
config.Bind(myInt);
}
}
""";

ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source);
var effective = CompilationWithAnalyzers.GetEffectiveDiagnostics(result.Diagnostics, result.OutputCompilation);
Diagnostic diagnostic = Assert.Single(effective, d => d.Id == "SYSLIB1103");
Assert.False(diagnostic.IsSuppressed);
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNetCore))]
public async Task Diagnostic_MultipleDiagnostics_OnlySomeSuppressed()
{
string source = """
using System;
using System.Collections.Immutable;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Configuration;

public class Program
{
public static void Main()
{
ConfigurationBuilder configurationBuilder = new();
IConfigurationRoot config = configurationBuilder.Build();

// SYSLIB1103 suppressed for this call only.
#pragma warning disable SYSLIB1103
int myInt = 1;
config.Bind(myInt);
#pragma warning restore SYSLIB1103

// SYSLIB1103 NOT suppressed for this call.
long myLong = 1;
config.Bind(myLong);
}
}
""";

ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source);
var effective = CompilationWithAnalyzers.GetEffectiveDiagnostics(result.Diagnostics, result.OutputCompilation)
.Where(d => d.Id == "SYSLIB1103")
.ToList();

Assert.Equal(2, effective.Count);
Assert.Single(effective, d => d.IsSuppressed);
Assert.Single(effective, d => !d.IsSuppressed);
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNetCore))]
public async Task Diagnostic_PragmaRestoreOutsideSpan_IsNotSuppressed()
{
string source = """
using System;
using Microsoft.Extensions.Configuration;

public class Program
{
public static void Main()
{
ConfigurationBuilder configurationBuilder = new();
IConfigurationRoot config = configurationBuilder.Build();

// Suppress and restore BEFORE the diagnostic site.
#pragma warning disable SYSLIB1103
#pragma warning restore SYSLIB1103

int myInt = 1;
config.Bind(myInt);
}
}
""";

ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source);
var effective = CompilationWithAnalyzers.GetEffectiveDiagnostics(result.Diagnostics, result.OutputCompilation);
Diagnostic diagnostic = Assert.Single(effective, d => d.Id == "SYSLIB1103");
Assert.False(diagnostic.IsSuppressed);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ internal sealed class Parser
private readonly INamedTypeSymbol _stringSymbol;
private readonly Action<Diagnostic>? _reportDiagnostic;

public List<DiagnosticInfo> Diagnostics { get; } = new();
public List<Diagnostic> Diagnostics { get; } = new();

public Parser(
INamedTypeSymbol loggerMessageAttribute,
Expand Down Expand Up @@ -811,12 +811,14 @@ private static string GenerateClassName(TypeDeclarationSyntax typeDeclaration)

private void Diag(DiagnosticDescriptor desc, Location? location, params object?[]? messageArgs)
{
Diagnostic diagnostic = Diagnostic.Create(desc, location, messageArgs);

// Report immediately if callback is provided (preserves pragma suppression with original locations)
_reportDiagnostic?.Invoke(Diagnostic.Create(desc, location, messageArgs));
_reportDiagnostic?.Invoke(diagnostic);

// Also collect for scenarios that need the diagnostics list; in Roslyn 4.0+ incremental generators,
// this list is exposed via parser.Diagnostics (as ImmutableEquatableArray<DiagnosticInfo>) and reported in Execute.
Diagnostics.Add(DiagnosticInfo.Create(desc, location, messageArgs));
// this list is exposed via parser.Diagnostics and reported in the diagnostic pipeline.
Diagnostics.Add(diagnostic);
}

private static bool IsBaseOrIdentity(ITypeSymbol source, ITypeSymbol dest, Compilation compilation)
Expand Down
Loading