Fix source generator diagnostics to support #pragma warning disable#124994
Fix source generator diagnostics to support #pragma warning disable#124994eiriktsarpalis wants to merge 9 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Enables Roslyn pragma-based suppression (#pragma warning disable) for diagnostics emitted by several incremental source generators by separating source emission from diagnostic emission and recreating diagnostics with SourceFile locations recovered from the current Compilation.
Changes:
- Split generator pipelines into (1) fully-incremental source generation and (2) diagnostics combined with
CompilationProviderfor pragma-suppressible locations. - Add
DiagnosticInfo.CreateDiagnostic(Compilation)(and Regex-specific equivalent) to rebuildLocationasLocationKind.SourceFile. - Update Json source generator incremental test commentary to reflect the new diagnostics pipeline behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.cs | Adds a diagnostics-only pipeline and recreates diagnostics with SourceFile locations. |
| src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorIncrementalTests.cs | Adjusts incremental-model encapsulation test commentary given diagnostics now reference SyntaxTree. |
| src/libraries/System.Text.Json/gen/JsonSourceGenerator.Roslyn4.0.cs | Splits Json SG into separate source + diagnostics pipelines; diagnostics now created with compilation context. |
| src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Roslyn4.0.cs | Splits LoggerMessage SG pipelines and preserves diagnostic deduping while making locations pragma-suppressible. |
| src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.cs | Splits binder SG pipelines and reports diagnostics using compilation-backed SourceFile locations. |
| src/libraries/Common/src/SourceGenerators/DiagnosticInfo.cs | Adds CreateDiagnostic(Compilation) to recreate pragma-suppressible SourceFile locations from trimmed locations. |
| string filePath = location.GetLineSpan().Path; | ||
| TextSpan textSpan = location.SourceSpan; | ||
| foreach (SyntaxTree tree in compilation.SyntaxTrees) | ||
| { | ||
| if (tree.FilePath == filePath) | ||
| { | ||
| location = Location.Create(tree, textSpan); | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
CreateDiagnostic(Compilation) does a full scan of compilation.SyntaxTrees for every diagnostic instance. Since this is invoked in loops (and some generators can produce many diagnostics), consider building a filePath→SyntaxTree lookup once per source-output callback and reusing it when creating diagnostics.
src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.cs
Outdated
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Roslyn4.0.cs
Outdated
Show resolved
Hide resolved
...on/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorIncrementalTests.cs
Show resolved
Hide resolved
effaba3 to
50000b3
Compare
Split each affected generator's RegisterSourceOutput pipeline into two separate pipelines: 1. Source generation pipeline - fully incremental, uses Select to extract just the equatable model. Only re-fires on structural changes. 2. Diagnostic pipeline - combines with CompilationProvider to recover the SyntaxTree from the Compilation at emission time. Uses Location.Create(SyntaxTree, TextSpan) to produce SourceLocation instances that support pragma suppression checks. Affected generators: - System.Text.Json (JsonSourceGenerator) - Microsoft.Extensions.Logging (LoggerMessageGenerator) - Microsoft.Extensions.Configuration.Binder (ConfigurationBindingGenerator) - System.Text.RegularExpressions (RegexGenerator) The shared DiagnosticInfo type gains a CreateDiagnostic(Compilation) overload that recovers the SyntaxTree from the trimmed ExternalFileLocation's file path, converting it back to a SourceLocation. The RegexGenerator's private DiagnosticData type gets an analogous ToDiagnostic(Compilation) overload. Fixes #92509 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
50000b3 to
e95c4ad
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.cs:66
- The catch block rethrows with
throw ex;, which resets the exception stack trace. Usethrow;(or remove the catch entirely) to preserve the original call stack for diagnosing generator failures.
catch (Exception ex)
{
throw ex;
}
...raries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs
Show resolved
Hide resolved
…tors Verifies that diagnostics from all 4 affected source generators (Regex, JSON, Logger, ConfigBinder) have LocationKind.SourceFile, which is the prerequisite for #pragma warning disable to work. Before this fix, diagnostics had LocationKind.ExternalFile which bypasses Roslyn's pragma suppression checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds RegexGeneratorIncrementalTests with 4 tests following the patterns established by JsonSourceGeneratorIncrementalTests and ConfigBinder's GeneratorTests.Incremental.cs: - SameInput_DoesNotRegenerate: verifies caching on identical compilations - EquivalentSources_Regenerates: documents that semantically equivalent sources trigger regeneration (pre-existing limitation due to Dictionary in model lacking value equality) - DifferentSources_Regenerates: verifies model changes trigger output - SourceGenModelDoesNotEncapsulateSymbolsOrCompilationData: walks the object graph to ensure no Compilation/ISymbol references leak Also adds SourceGenerationTrackingName constant and WithTrackingName() to the source pipeline to enable step tracking. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.cs:66
- The
catch (Exception ex) { throw ex; }pattern resets the original stack trace. If the intent is just to propagate, usethrow;(or remove the try/catch entirely) so failures in the generator preserve useful call stacks.
catch (Exception ex)
{
throw ex;
}
src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.cs
Outdated
Show resolved
Hide resolved
Replace inline lambda callbacks in the diagnostic pipelines with named EmitDiagnostics static methods, complementing the existing EmitSource methods in all 4 generators (JSON, Logger, ConfigBinder, Regex). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| compilation = compilation.ReplaceSyntaxTree( | ||
| compilation.SyntaxTrees.First(), | ||
| CSharpSyntaxTree.ParseText(SourceText.From(source2, Encoding.UTF8), s_parseOptions)); | ||
| driver = driver.RunGenerators(compilation); |
There was a problem hiding this comment.
Encoding.UTF8 is used here but the file doesn’t import System.Text (and there doesn’t appear to be a global using for it in this test project). This will fail to compile unless you add using System.Text; or fully-qualify System.Text.Encoding.UTF8 at the call sites.
| // 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.Create(desc, location, messageArgs)); |
There was a problem hiding this comment.
Diag currently creates two separate Diagnostic instances with identical content (one for _reportDiagnostic and one to store in Diagnostics). Consider creating the Diagnostic once and reusing it to avoid duplicate allocations and ensure both paths observe the exact same instance.
Create named IncrementalValueProvider variables for the equatable model projections in all 4 generators, with detailed comments explaining how Roslyn's Select operator uses model equality to guard source production. For the Regex generator, also extract the source emission lambda into a named EmitSource method, complementing EmitDiagnostics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
3323beb to
a4e500e
Compare
Create named IncrementalValueProvider variables for the diagnostic projections in all 4 generators, with comments explaining that ImmutableArray<Diagnostic> uses reference equality in the incremental pipeline — the callback fires on every compilation change by design. This also simplifies the EmitDiagnostics signatures to accept just the projected diagnostics rather than the full model+diagnostics tuple. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.cs:66
- Avoid
throw ex;here; it resets the original stack trace and makes failures harder to diagnose. Usethrow;to preserve the stack trace, or remove the try/catch entirely if it's only rethrowing.
catch (Exception ex)
{
throw ex;
}
| private void Diag(DiagnosticDescriptor desc, Location? location, params object?[]? messageArgs) | ||
| { | ||
| // Report immediately if callback is provided (preserves pragma suppression with original locations) | ||
| _reportDiagnostic?.Invoke(Diagnostic.Create(desc, location, messageArgs)); | ||
|
|
||
| // 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.Create(desc, location, messageArgs)); | ||
| } |
There was a problem hiding this comment.
Diagnostic.Create(...) is called twice here (once for immediate reporting and once for storing), which does redundant work and may lead to subtle inconsistencies if creation behavior ever changes. Create the Diagnostic once, then both invoke the callback and add the same instance to Diagnostics.
Replace the untyped object?-based pipeline model with a deeply equatable RegexSourceGenerationResult record containing ImmutableEquatableArray fields. This makes the Regex generator fully incremental — equivalent sources now produce Cached outputs instead of re-emitting code on every compilation. Key changes: - Add ImmutableEquatableArray.cs and HashHelpers.cs to the Regex gen csproj - Define RegexMethodEntry, HelperMethod, and RegexSourceGenerationResult records with deep value equality via ImmutableEquatableArray fields - Pre-compute XML expression description and capture metadata during pipeline so the equatable model contains no RegexTree/AnalysisResults references - Collect + aggregate step deduplicates helpers across all regex methods - Remove ObjectImmutableArraySequenceEqualityComparer (no longer needed) - Remove mutable IsDuplicate/GeneratedName from RegexMethod (computed at emit time) - Update EmitRegexPartialMethod/EmitRegexLimitedBoilerplate/EmitRegexDerivedImplementation to accept typed RegexMethodEntry + generatedName instead of RegexMethod - Update incremental tests: EquivalentSources now expects Cached Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| using System.Collections; | ||
| using System.Collections.Generic; | ||
| using System.Collections.Immutable; | ||
| using System.Linq; | ||
| using System.Reflection; | ||
| using System.Text.RegularExpressions.Generator; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.CSharp; | ||
| using Microsoft.CodeAnalysis.Text; | ||
| using Xunit; |
There was a problem hiding this comment.
Encoding.UTF8 is used in this file (e.g., when parsing/replacing syntax trees), but System.Text isn’t imported, so this won’t compile. Add using System.Text; or fully-qualify Encoding usages.
| ? cnsm.Cast<Collections.DictionaryEntry>().Select(de => (Key: (int)de.Key, Value: (int)de.Value!)).OrderBy(p => p.Key).ToImmutableEquatableArray() | ||
| : null; | ||
| ImmutableEquatableArray<(string Key, int Value)>? captureNameToNumberMapping = regexMethod.Tree.CaptureNameToNumberMapping is { } cntnm | ||
| ? cntnm.Cast<Collections.DictionaryEntry>().Select(de => (Key: (string)de.Key, Value: (int)de.Value!)).OrderBy(p => p.Key, StringComparer.Ordinal).ToImmutableEquatableArray() |
There was a problem hiding this comment.
Collections.DictionaryEntry is referenced here, but there’s no using Collections = System.Collections; alias (and Collections is not otherwise defined), so this will not compile. Use System.Collections.DictionaryEntry (or add the alias) for these casts.
| ? cnsm.Cast<Collections.DictionaryEntry>().Select(de => (Key: (int)de.Key, Value: (int)de.Value!)).OrderBy(p => p.Key).ToImmutableEquatableArray() | |
| : null; | |
| ImmutableEquatableArray<(string Key, int Value)>? captureNameToNumberMapping = regexMethod.Tree.CaptureNameToNumberMapping is { } cntnm | |
| ? cntnm.Cast<Collections.DictionaryEntry>().Select(de => (Key: (string)de.Key, Value: (int)de.Value!)).OrderBy(p => p.Key, StringComparer.Ordinal).ToImmutableEquatableArray() | |
| ? cnsm.Cast<System.Collections.DictionaryEntry>().Select(de => (Key: (int)de.Key, Value: (int)de.Value!)).OrderBy(p => p.Key).ToImmutableEquatableArray() | |
| : null; | |
| ImmutableEquatableArray<(string Key, int Value)>? captureNameToNumberMapping = regexMethod.Tree.CaptureNameToNumberMapping is { } cntnm | |
| ? cntnm.Cast<System.Collections.DictionaryEntry>().Select(de => (Key: (string)de.Key, Value: (int)de.Value!)).OrderBy(p => p.Key, StringComparer.Ordinal).ToImmutableEquatableArray() |
fff8b03 to
584568b
Compare
Move all regex parsing, code generation, and data extraction into the ForAttributeWithMetadataName transform. The pipeline returns the final typed (RegexMethodEntry?, ImmutableEquatableArray<HelperMethod>, ImmutableArray<Diagnostic>) tuple directly — no intermediate object? boxing and no DiagnosticLocation sidecar. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
584568b to
8e7da78
Compare
| ? cnsm.Cast<Collections.DictionaryEntry>().Select(de => (Key: (int)de.Key, Value: (int)de.Value!)).OrderBy(p => p.Key).ToImmutableEquatableArray() | ||
| : null; | ||
| ImmutableEquatableArray<(string Key, int Value)>? captureNameToNumberMapping = regexTree.CaptureNameToNumberMapping is { } cntnm | ||
| ? cntnm.Cast<Collections.DictionaryEntry>().Select(de => (Key: (string)de.Key, Value: (int)de.Value!)).OrderBy(p => p.Key, StringComparer.Ordinal).ToImmutableEquatableArray() |
There was a problem hiding this comment.
Collections.DictionaryEntry is referenced here, but this file doesn't define a Collections alias (e.g., using Collections = System.Collections;). As-is, this won't compile. Use System.Collections.DictionaryEntry (or add the missing alias / using System.Collections; and use DictionaryEntry) for these Cast<...>() calls.
| ? cnsm.Cast<Collections.DictionaryEntry>().Select(de => (Key: (int)de.Key, Value: (int)de.Value!)).OrderBy(p => p.Key).ToImmutableEquatableArray() | |
| : null; | |
| ImmutableEquatableArray<(string Key, int Value)>? captureNameToNumberMapping = regexTree.CaptureNameToNumberMapping is { } cntnm | |
| ? cntnm.Cast<Collections.DictionaryEntry>().Select(de => (Key: (string)de.Key, Value: (int)de.Value!)).OrderBy(p => p.Key, StringComparer.Ordinal).ToImmutableEquatableArray() | |
| ? cnsm.Cast<System.Collections.DictionaryEntry>().Select(de => (Key: (int)de.Key, Value: (int)de.Value!)).OrderBy(p => p.Key).ToImmutableEquatableArray() | |
| : null; | |
| ImmutableEquatableArray<(string Key, int Value)>? captureNameToNumberMapping = regexTree.CaptureNameToNumberMapping is { } cntnm | |
| ? cntnm.Cast<System.Collections.DictionaryEntry>().Select(de => (Key: (string)de.Key, Value: (int)de.Value!)).OrderBy(p => p.Key, StringComparer.Ordinal).ToImmutableEquatableArray() |
| using System.Diagnostics.CodeAnalysis; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using System.Threading; |
There was a problem hiding this comment.
This file adds using System.Threading; but doesn't appear to use anything from that namespace. If warnings are treated as errors, this will break the build with CS8019. Please remove the unused using (or use the imported type if intended).
| using System.Threading; |
| var reportedDiagnostics = new HashSet<(string Id, TextSpan? Span, string? FilePath)>(); | ||
| foreach (ImmutableArray<Diagnostic> diagnosticBatch in items) | ||
| { | ||
| foreach (Diagnostic diagnostic in diagnosticBatch) | ||
| { | ||
| if (reportedDiagnostics.Add((diagnostic.Id, diagnostic.Location?.SourceSpan, diagnostic.Location?.SourceTree?.FilePath))) |
There was a problem hiding this comment.
The diagnostic deduplication key only considers (Id, SourceSpan, FilePath). The parser can legitimately emit multiple diagnostics with the same Id at the same location but different message args (e.g., one per missing template name), and this HashSet will incorrectly drop all but the first. Include something that distinguishes message/args (e.g., diagnostic.GetMessage() or diagnostic.ToString() / Descriptor + message) in the dedup key, or use a more faithful comparer.
| var reportedDiagnostics = new HashSet<(string Id, TextSpan? Span, string? FilePath)>(); | |
| foreach (ImmutableArray<Diagnostic> diagnosticBatch in items) | |
| { | |
| foreach (Diagnostic diagnostic in diagnosticBatch) | |
| { | |
| if (reportedDiagnostics.Add((diagnostic.Id, diagnostic.Location?.SourceSpan, diagnostic.Location?.SourceTree?.FilePath))) | |
| var reportedDiagnostics = new HashSet<(string Id, TextSpan? Span, string? FilePath, string Message)>(); | |
| foreach (ImmutableArray<Diagnostic> diagnosticBatch in items) | |
| { | |
| foreach (Diagnostic diagnostic in diagnosticBatch) | |
| { | |
| string message = diagnostic.GetMessage(); | |
| if (reportedDiagnostics.Add((diagnostic.Id, diagnostic.Location?.SourceSpan, diagnostic.Location?.SourceTree?.FilePath, message))) |
3d84367 to
ec6fd58
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.
You can also share your feedback on Copilot code review. Take the survey.
| using System.Collections; | ||
| using System.Collections.Generic; | ||
| using System.Collections.Immutable; | ||
| using System.Linq; |
There was a problem hiding this comment.
using System.Collections.Immutable; is not referenced in this test file. Consider removing the unused using to avoid IDE0005 / style warnings during build.
| using System.Linq; |
Refactor GetRegexMethodDataOrFailureDiagnostic to return RegexPatternAndSyntax? and accept an ImmutableArray<Diagnostic>.Builder accumulator. Refactor ParseAndGenerateRegex to return RegexMethodEntry? and accept diagnostic and helper accumulators. This eliminates the verbose 3-element tuple returns throughout both methods. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ec6fd58 to
8547065
Compare
Summary
Source generator diagnostics emitted by incremental generators in dotnet/runtime can now be suppressed using
#pragma warning disableinline directives.Fixes #92509
Problem
Incremental source generators that store diagnostic locations in equatable intermediate representations "trim" the
Locationto avoid holding references to theCompilationobject. The standard workaround —Location.Create(filePath, textSpan, linePositionSpan)— creates anExternalFileLocation(LocationKind.ExternalFile) which bypasses Roslyn's pragma suppression checks. OnlySourceLocation(LocationKind.SourceFile) instances, created viaLocation.Create(SyntaxTree, TextSpan), are checked against theSyntaxTree's pragma directive table.This is the issue described in dotnet/roslyn#68291.
Solution
Following the technique first applied in eiriktsarpalis/PolyType#401, split each affected generator's
RegisterSourceOutputpipeline into two separate pipelines:Selectto extract just the equatable model (which contains noLocation/SyntaxTreereferences), deduplicates by model equality, only re-fires on structural changes.Diagnosticobjects that preserve the originalSourceLocation(LocationKind.SourceFile) from the syntax tree. This enables Roslyn to check theSyntaxTree's pragma directive table for#pragma warning disabledirectives.Parsers now create
Diagnostic.Create(descriptor, location, ...)directly instead of wrapping inDiagnosticInfo.Create(...)which trimmed the location. SinceImmutableArray<Diagnostic>does not have value equality, the diagnostic pipeline fires more frequently but the work is trivially cheap (just iterating and reporting).RegexGenerator specifics
The RegexGenerator's pipeline uses a heterogeneous
ImmutableArray<object>and mixes diagnostic data with source generation data. To fully segregate diagnostics from the incremental model:DiagnosticDataandEquatableLocationtypes have been removed.DiagnosticLocationhas been removed fromRegexPatternAndSyntaxandRegexMethodrecords — the model is now completely free ofLocation/SyntaxTreereferences.(object? Model, Location? DiagnosticLocation, ImmutableArray<Diagnostic> Diagnostics)where:Modelhas value equality and drives incremental caching for source generation.DiagnosticLocationcarries the rawSourceLocationfor creating diagnostics in later pipeline steps (regex parse failures, limited support detection).Diagnosticsaccumulates all diagnostics across pipeline steps.ModelviaSelect, collects, and appliesObjectImmutableArraySequenceEqualityComparer.Diagnostics.Affected Generators
DiagnosticInfo.GetTrimmedLocation()List<Diagnostic>; split pipelineDiagnosticInfo.GetTrimmedLocation()List<Diagnostic>; split pipeline with dedupDiagnosticInfo.GetTrimmedLocation()List<Diagnostic>; split pipelineGetComparableLocation()Not Affected
Location(no trimming), already support pragma suppression.SourceProductionContext.ReportDiagnosticwith Compilation access.Changes
JsonSourceGenerator.Parser.csList<DiagnosticInfo>→List<Diagnostic>JsonSourceGenerator.Roslyn4.0.csJsonSourceGenerator.Roslyn3.11.csDiagnosticdirectlyLoggerMessageGenerator.Parser.csList<DiagnosticInfo>→List<Diagnostic>LoggerMessageGenerator.Roslyn4.0.csConfigurationBindingGenerator.Parser.csList<DiagnosticInfo>→List<Diagnostic>ConfigurationBindingGenerator.csRegexGenerator.Parser.csGetComparableLocation; removedDiagnosticLocationfrom records; returns(Model, Location, Diagnostics)tupleRegexGenerator.csDiagnosticData/EquatableLocation; pipeline fully segregates diagnostics from incremental modelJsonSourceGeneratorIncrementalTests.csSyntaxTree)