Don't attach code-fix locations from outside the analyzed compilation - #131828
Don't attach code-fix locations from outside the analyzed compilation#131828sbomer wants to merge 3 commits into
Conversation
The DynamicallyAccessedMembers analyzer crashes with AD0001 in Visual
Studio when a diagnostic is reported against a symbol that lives in a
referenced project:
System.ArgumentException: Reported diagnostic 'IL2075' has a source
location in file '...\Attributes.cs', which is not part of the
compilation being analyzed.
The analyzer attaches an extra location pointing at the declaration of
the symbol that should be annotated, so that the code fixer knows where
to insert the attribute. Roslyn validates that every location on a
reported diagnostic (including AdditionalLocations) belongs to the
compilation being analyzed, so attaching a declaration from another
compilation throws.
Two guards were used to decide whether that extra location was safe to
attach, and both are wrong:
* DiagnosticContext.CreateDiagnostic checked
symbol.DeclaringSyntaxReferences.Length == 0
* DynamicallyAccessedMembersAnalyzer.VerifyDamOnMethodsMatch checked
Location.IsInSource
Both are really asking "is this symbol part of the compilation I am
analyzing?", but neither answers that question. Under a
CompilationReference - which is how Visual Studio models a
project-to-project reference - a symbol from another project is still a
source symbol with real declaring syntax references, so both guards
happily let the foreign location through.
This is why the crash only reproduced in Visual Studio, and only with
"run analysis on unopened files" enabled. Command-line builds reference
other projects through their emitted assemblies, so cross-project
symbols are metadata symbols with no declaring syntax references and the
old guard tripped correctly.
Replace both guards with Compilation.ContainsSyntaxTree, which asks the
question directly. This requires threading the Compilation into
DiagnosticContext; it is a required constructor parameter rather than an
optional one so that the compiler points at every construction site.
The second guard affects IL2092/IL2093/IL2094. GetTargetAndRequirements
picks the base method as the attribute target when an override is
annotated and the base is not, so an override in the current project
whose base lives in a referenced project hit the same crash.
Both cases are covered by new regression tests, which use
TestState.AdditionalProjects to get a real CompilationReference. Note
that ReferenceCompatibilityTestUtils cannot reproduce this, as it emits
to a stream and creates a MetadataReference.
Suppressing the extra location only means the code fix is not offered
for declarations the fixer could not have edited anyway; the underlying
warning is still reported. The code fix provider already returns early
when there are no additional locations.
Fixes dotnet#109352
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 47e32591-30fa-4a23-b1c1-48d277b1a3ed
Assisted-by: GitHub Copilot CLI:claude-opus-5
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @agocke, @dotnet/illink |
There was a problem hiding this comment.
Pull request overview
This PR fixes a Roslyn analyzer crash caused by reporting diagnostics whose AdditionalLocations point to source files outside the analyzed Compilation (e.g., when the symbol being annotated comes from a referenced project via CompilationReference). The change makes code-fix locations conditional on Compilation.ContainsSyntaxTree(...) and adds regression coverage for both affected diagnostic sites.
Changes:
- Thread
CompilationintoTrimAnalysis.DiagnosticContextand gate code-fix location attachment onCompilation.ContainsSyntaxTree(...). - Update all
DiagnosticContextconstruction sites in trim analysis / requires analyzers to pass the active compilation. - Add regression tests using
AdditionalProjects/AdditionalProjectReferencesto simulate project-to-project references.
Show a summary per file
| File | Description |
|---|---|
| src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/DynamicallyAccessedMembersAnalyzerTests.cs | Adds regression tests ensuring no foreign AdditionalLocations are attached when the target symbol lives in a referenced project. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/TypeNameResolver.cs | Exposes the current compilation internally so downstream trim-analysis helpers can build safe DiagnosticContext instances. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimAnalysisReflectionAccessPattern.cs | Passes context.Compilation into DiagnosticContext so code-fix location gating can be compilation-aware. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimAnalysisMethodCallPattern.cs | Same as above for method-call pattern diagnostics. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimAnalysisFieldAccessPattern.cs | Same as above for field access diagnostics. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimAnalysisBackingFieldAccessPattern.cs | Same as above for backing-field access diagnostics. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/RequireDynamicallyAccessedMembersAction.cs | Ensures type-name resolution diagnostics use a compilation-aware DiagnosticContext. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/ReflectionAccessAnalyzer.cs | Updates diagnostic emission paths to construct DiagnosticContext with the correct compilation. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/HandleCallAction.cs | Updates stored/constructed diagnostic contexts to include compilation. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/FeatureCheckReturnValuePattern.cs | Updates diagnostic context creation to include compilation. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/DiagnosticContext.cs | Implements the new compilation-aware guard (ContainsSyntaxTree) before attaching code-fix locations. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/RequiresAnalyzerBase.cs | Updates diagnostic context creation in implicit base-ctor analysis to pass context.Compilation. |
| src/tools/illink/src/ILLink.RoslynAnalyzer/DynamicallyAccessedMembersAnalyzer.cs | Replaces Location.IsInSource with a compilation-aware guard for code-fix locations in override/virtual mismatch diagnostics. |
Copilot's findings
- Files reviewed: 13/13 changed files
- Comments generated: 1
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47e32591-30fa-4a23-b1c1-48d277b1a3ed Assisted-by: GitHub Copilot CLI:claude-opus-5
The tests only need the referenced symbols to come from a CompilationReference, which Compilation.ToMetadataReference() produces directly. Building one and passing it through the existing analyzer verification path avoids the AdditionalProjects plumbing, and with it the EmptyCodeFixProvider alias that only existed because the code fix test infrastructure requires a code fix provider type argument. This also lets the tests reuse TestCaseUtils.UseMSBuildProperties like the rest of the file instead of hand-rolling an .editorconfig. Verified that the tests still fail without the fix, with the same ArgumentException as before. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47e32591-30fa-4a23-b1c1-48d277b1a3ed Assisted-by: GitHub Copilot CLI:claude-opus-5
| private static bool CanOfferCodeFixAt(Location location, Compilation compilation) | ||
| => location.SourceTree is { } sourceTree && compilation.ContainsSyntaxTree(sourceTree); | ||
|
|
||
| private static (IMethodSymbol Method, DynamicallyAccessedMemberTypes Requirements) GetTargetAndRequirements(IMethodSymbol method, IMethodSymbol overriddenMethod, DynamicallyAccessedMemberTypes methodAnnotation, DynamicallyAccessedMemberTypes overriddenMethodAnnotation) { |
There was a problem hiding this comment.
| private static (IMethodSymbol Method, DynamicallyAccessedMemberTypes Requirements) GetTargetAndRequirements(IMethodSymbol method, IMethodSymbol overriddenMethod, DynamicallyAccessedMemberTypes methodAnnotation, DynamicallyAccessedMemberTypes overriddenMethodAnnotation) { | |
| private static (IMethodSymbol Method, DynamicallyAccessedMemberTypes Requirements) GetTargetAndRequirements(IMethodSymbol method, IMethodSymbol overriddenMethod, DynamicallyAccessedMemberTypes methodAnnotation, DynamicallyAccessedMemberTypes overriddenMethodAnnotation) | |
| { |
| private static bool CanOfferCodeFixAt(Location location, Compilation compilation) | ||
| => location.SourceTree is { } sourceTree && compilation.ContainsSyntaxTree(sourceTree); |
There was a problem hiding this comment.
I'm slightly concerned that this and CanOfferCodeFixOn are separate and could diverge. Could we make this one internal/public and call this from CanOfferCodeFixOn?
jtschuster
left a comment
There was a problem hiding this comment.
LGTM aside from a couple nits.
|
Heh, I was looking at this as well. Good news -- at first glance I think we have roughly the same implementation. |
| /// references: the IDE models project-to-project references as compilation references, which expose source | ||
| /// symbols whose syntax trees belong to a different compilation. | ||
| /// </summary> | ||
| private bool CanOfferCodeFixOn(ISymbol symbol) |
There was a problem hiding this comment.
Actually, I was taking a different approach. This is approaching the problem from trying to fix things in many different scenarios and then catching mistakes.
I think a more robust solution would be to use the inherent structure of the program to decide when a code fix should be offered.
So, an example: in a base/override scenario, we can always offer the code fixer on the override, and just ignore fixes that on the base entirely. We actually don't have to know about the compilation at all to know that the override always belongs to the current compilation.
| /// </summary> | ||
| private bool CanOfferCodeFixOn(ISymbol symbol) | ||
| { | ||
| if (symbol.DeclaringSyntaxReferences.Length == 0) |
There was a problem hiding this comment.
If we set things up right I think this should actually be impossible to hit.
| @@ -223,7 +223,7 @@ private static void VerifyDamOnMethodsMatch(SymbolAnalysisContext context, IMeth | |||
| Location attributableSymbolLocation = GetPrimaryLocation(attributableMethod.Locations); | |||
|
|
|||
| // code fix does not support merging multiple attributes. If an attribute is present or the method is not in source, do not provide args for code fix. | |||
| (Location[]? sourceLocation, Dictionary<string, string?>? DAMArgs) = (!attributableSymbolLocation.IsInSource | |||
There was a problem hiding this comment.
I think we can stop attaching additional source locations here entirely. Symbols don't need locations because the code fixer can completely hydrate all necessary information without looking at locations at all.
The only place we might need auxiliary information is in data flow.
Same is true for DAM annotations -- on symbols this info is trivial to reconstruct in the fixer. It's only helpful to attach that info for flow analysis, where the fixer would have to re-do all of flow analysis to find the right data.
Fixes #109352
The crash
The
DynamicallyAccessedMembersanalyzer crashes with AD0001 in Visual Studio when it reports a diagnostic against a symbol that lives in a referenced project:Root cause
When the analyzer reports a data flow warning, it also attaches an extra location pointing at the declaration of the symbol that should be annotated, so the code fixer knows where to insert the attribute. Roslyn validates that every location on a reported diagnostic — including
AdditionalLocations— belongs to the compilation being analyzed, so attaching a declaration from another compilation throws.Two guards were used to decide whether that extra location was safe to attach, and both are wrong:
DiagnosticContext.CreateDiagnosticsymbol.DeclaringSyntaxReferences.Length == 0DynamicallyAccessedMembersAnalyzer.VerifyDamOnMethodsMatchLocation.IsInSourceBoth are really asking "is this symbol part of the compilation I am analyzing?", but neither answers that question. Under a
CompilationReference— which is how Visual Studio models a project-to-project reference — a symbol from another project is still a source symbol with real declaring syntax references, so both guards happily let the foreign location through.This is why the crash only reproduced in Visual Studio, and only with "run analysis on unopened files" enabled — which matches the reproduction history on the issue. Command-line builds reference other projects through their emitted assemblies, so cross-project symbols are metadata symbols with no declaring syntax references, and the old guard tripped correctly.
The fix
Replace both guards with
Compilation.ContainsSyntaxTree, which asks the question directly.This requires threading the
CompilationintoDiagnosticContext. I made it a required constructor parameter rather than an optional one so the compiler points at every construction site — that immediately surfaced three target-typednew(...)sites that a grep fornew DiagnosticContext(had missed.The second guard affects IL2092/IL2093/IL2094.
GetTargetAndRequirementspicks the base method as the attribute target when an override is annotated and the base is not, so an override in the current project whose base lives in a referenced project hit the same crash. I found this while auditing for other instances of the bug class, and it reproduces independently.Suppressing the extra location only means the code fix is not offered for declarations the fixer could not have edited anyway. The underlying warning is still reported, and
DynamicallyAccessedMembersCodeFixProvideralready returns early when there are no additional locations.Tests
Two regression tests, one per bug site (IL2075 and IL2092). Both were confirmed to fail before the fix with the exact
ArgumentExceptionfrom the issue, and to pass after.They use
TestState.AdditionalProjects/AdditionalProjectReferences, which the Roslyn testing SDK models as a realCompilationReference. Worth noting for anyone testing in this area: the existingReferenceCompatibilityTestUtilshelper cannot reproduce this, because it emits to a stream and creates aMetadataReference.Full
ILLink.RoslynAnalyzer.Testssuite: 1204 passed, 0 failed, 9 skipped.Other sites audited
I checked the remaining diagnostic sites for the same bug class and believe they are safe:
RequiresAnalyzerBase— safe by construction; when an interface implementation is inherited from a base class, it already redirectsoriginto the current type rather than reporting on the base member.DynamicallyAccessedMembersTypeHierarchyandReflectionAccessAnalyzer— only use locations from symbols already established to be within the type being analyzed.Note
This pull request was created with assistance from GitHub Copilot.