Skip to content

Commit c860af4

Browse files
committed
Only provide fallback options for host analyzers
1 parent dda119a commit c860af4

File tree

39 files changed

+797
-281
lines changed

39 files changed

+797
-281
lines changed

src/EditorFeatures/CSharpTest/Diagnostics/DiagnosticAnalyzerDriver/DiagnosticAnalyzerDriverTests.cs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -656,21 +656,28 @@ await TestNuGetAndVsixAnalyzerCoreAsync(
656656
// 1) No duplicate diagnostics
657657
// 2) Both NuGet and Vsix analyzers execute
658658
// 3) Appropriate diagnostic filtering is done - Nuget suppressor suppresses VSIX analyzer.
659+
//
660+
// 🐛 After splitting fallback options into separate CompilationWithAnalyzers for project and host analyzers,
661+
// NuGet-installed suppressors no longer act on VSIX-installed analyzer diagnostics. Fixing this requires us to
662+
// add NuGet-installed analyzer references to the host CompilationWithAnalyzers, with an additional flag
663+
// indicating that only suppressors should run for these references.
664+
// https://github.com/dotnet/roslyn/issues/75399
665+
const bool FalseButShouldBeTrue = false;
659666
await TestNuGetAndVsixAnalyzerCoreAsync(
660667
nugetAnalyzers: ImmutableArray.Create(firstNugetAnalyzer),
661668
expectedNugetAnalyzersExecuted: true,
662669
vsixAnalyzers: ImmutableArray.Create(vsixAnalyzer),
663670
expectedVsixAnalyzersExecuted: true,
664671
nugetSuppressors: ImmutableArray.Create(nugetSuppressor),
665-
expectedNugetSuppressorsExecuted: true,
672+
expectedNugetSuppressorsExecuted: FalseButShouldBeTrue,
666673
vsixSuppressors: ImmutableArray<VsixSuppressor>.Empty,
667674
expectedVsixSuppressorsExecuted: false,
668675
new[]
669676
{
670677
(Diagnostic("A", "Class").WithLocation(1, 7), nameof(NuGetAnalyzer)),
671-
(Diagnostic("X", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer)),
672-
(Diagnostic("Y", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer)),
673-
(Diagnostic("Z", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer))
678+
(Diagnostic("X", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer)),
679+
(Diagnostic("Y", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer)),
680+
(Diagnostic("Z", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer))
674681
});
675682

676683
// Suppressors with duplicate support for VsixAnalyzer, but not 100% overlap. Verify the following:
@@ -684,15 +691,15 @@ await TestNuGetAndVsixAnalyzerCoreAsync(
684691
vsixAnalyzers: ImmutableArray.Create(vsixAnalyzer),
685692
expectedVsixAnalyzersExecuted: true,
686693
nugetSuppressors: ImmutableArray.Create(partialNugetSuppressor),
687-
expectedNugetSuppressorsExecuted: true,
694+
expectedNugetSuppressorsExecuted: FalseButShouldBeTrue,
688695
vsixSuppressors: ImmutableArray.Create(vsixSuppressor),
689696
expectedVsixSuppressorsExecuted: false,
690697
new[]
691698
{
692699
(Diagnostic("A", "Class").WithLocation(1, 7), nameof(NuGetAnalyzer)),
693700
(Diagnostic("X", "Class", isSuppressed: false).WithLocation(1, 7), nameof(VsixAnalyzer)),
694-
(Diagnostic("Y", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer)),
695-
(Diagnostic("Z", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer))
701+
(Diagnostic("Y", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer)),
702+
(Diagnostic("Z", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer))
696703
});
697704

698705
// Suppressors with duplicate support for VsixAnalyzer, with 100% overlap. Verify the following:
@@ -706,15 +713,15 @@ await TestNuGetAndVsixAnalyzerCoreAsync(
706713
vsixAnalyzers: ImmutableArray.Create(vsixAnalyzer),
707714
expectedVsixAnalyzersExecuted: true,
708715
nugetSuppressors: ImmutableArray.Create(nugetSuppressor),
709-
expectedNugetSuppressorsExecuted: true,
716+
expectedNugetSuppressorsExecuted: FalseButShouldBeTrue,
710717
vsixSuppressors: ImmutableArray.Create(vsixSuppressor),
711718
expectedVsixSuppressorsExecuted: false,
712719
new[]
713720
{
714721
(Diagnostic("A", "Class").WithLocation(1, 7), nameof(NuGetAnalyzer)),
715-
(Diagnostic("X", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer)),
716-
(Diagnostic("Y", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer)),
717-
(Diagnostic("Z", "Class", isSuppressed: true).WithLocation(1, 7), nameof(VsixAnalyzer))
722+
(Diagnostic("X", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer)),
723+
(Diagnostic("Y", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer)),
724+
(Diagnostic("Z", "Class", isSuppressed: FalseButShouldBeTrue).WithLocation(1, 7), nameof(VsixAnalyzer))
718725
});
719726
}
720727

src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,18 +112,18 @@ private sealed class CombinedAnalyzerConfigOptions(AnalyzerConfigData fileDirect
112112

113113
public override NamingStylePreferences GetNamingStylePreferences()
114114
{
115-
var preferences = _fileDirectoryConfigData.ConfigOptions.GetNamingStylePreferences();
115+
var preferences = _fileDirectoryConfigData.ConfigOptionsWithoutFallback.GetNamingStylePreferences();
116116
if (preferences.IsEmpty && _projectDirectoryConfigData.HasValue)
117117
{
118-
preferences = _projectDirectoryConfigData.Value.ConfigOptions.GetNamingStylePreferences();
118+
preferences = _projectDirectoryConfigData.Value.ConfigOptionsWithoutFallback.GetNamingStylePreferences();
119119
}
120120

121121
return preferences;
122122
}
123123

124124
public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
125125
{
126-
if (_fileDirectoryConfigData.ConfigOptions.TryGetValue(key, out value))
126+
if (_fileDirectoryConfigData.ConfigOptionsWithoutFallback.TryGetValue(key, out value))
127127
{
128128
return true;
129129
}
@@ -134,7 +134,7 @@ public override bool TryGetValue(string key, [NotNullWhen(true)] out string? val
134134
return false;
135135
}
136136

137-
if (_projectDirectoryConfigData.Value.ConfigOptions.TryGetValue(key, out value))
137+
if (_projectDirectoryConfigData.Value.ConfigOptionsWithoutFallback.TryGetValue(key, out value))
138138
{
139139
return true;
140140
}
@@ -156,23 +156,23 @@ public override IEnumerable<string> Keys
156156
{
157157
get
158158
{
159-
foreach (var key in _fileDirectoryConfigData.ConfigOptions.Keys)
159+
foreach (var key in _fileDirectoryConfigData.ConfigOptionsWithoutFallback.Keys)
160160
yield return key;
161161

162162
if (!_projectDirectoryConfigData.HasValue)
163163
yield break;
164164

165-
foreach (var key in _projectDirectoryConfigData.Value.ConfigOptions.Keys)
165+
foreach (var key in _projectDirectoryConfigData.Value.ConfigOptionsWithoutFallback.Keys)
166166
{
167-
if (!_fileDirectoryConfigData.ConfigOptions.TryGetValue(key, out _))
167+
if (!_fileDirectoryConfigData.ConfigOptionsWithoutFallback.TryGetValue(key, out _))
168168
yield return key;
169169
}
170170

171171
foreach (var (key, severity) in _projectDirectoryConfigData.Value.TreeOptions)
172172
{
173173
var diagnosticKey = "dotnet_diagnostic." + key + ".severity";
174-
if (!_fileDirectoryConfigData.ConfigOptions.TryGetValue(diagnosticKey, out _) &&
175-
!_projectDirectoryConfigData.Value.ConfigOptions.TryGetValue(diagnosticKey, out _))
174+
if (!_fileDirectoryConfigData.ConfigOptionsWithoutFallback.TryGetValue(diagnosticKey, out _) &&
175+
!_projectDirectoryConfigData.Value.ConfigOptionsWithoutFallback.TryGetValue(diagnosticKey, out _))
176176
{
177177
yield return diagnosticKey;
178178
}

src/EditorFeatures/Test/Diagnostics/DiagnosticAnalyzerServiceTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -697,13 +697,13 @@ internal async Task TestOnlyRequiredAnalyzerExecutedDuringDiagnosticComputation(
697697
var analyzer1 = new NamedTypeAnalyzerWithConfigurableEnabledByDefault(isEnabledByDefault: true, DiagnosticSeverity.Warning, throwOnAllNamedTypes: false);
698698
var analyzer1Id = analyzer1.GetAnalyzerId();
699699
var analyzer2 = new NamedTypeAnalyzer();
700-
var analyzerIdsToRequestDiagnostics = new[] { analyzer1Id };
700+
var analyzerIdsToRequestDiagnostics = ImmutableArray.Create(analyzer1Id);
701701
var analyzerReference = new AnalyzerImageReference(ImmutableArray.Create<DiagnosticAnalyzer>(analyzer1, analyzer2));
702702
workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences([analyzerReference]));
703703
var project = workspace.CurrentSolution.Projects.Single();
704704
var document = documentAnalysis ? project.Documents.Single() : null;
705705
var diagnosticsMapResults = await DiagnosticComputer.GetDiagnosticsAsync(
706-
document, project, Checksum.Null, span: null, analyzerIdsToRequestDiagnostics,
706+
document, project, Checksum.Null, span: null, projectAnalyzerIds: [], analyzerIdsToRequestDiagnostics,
707707
AnalysisKind.Semantic, new DiagnosticAnalyzerInfoCache(), workspace.Services,
708708
isExplicit: false, reportSuppressedDiagnostics: false, logPerformanceInfo: false, getTelemetryInfo: false,
709709
cancellationToken: CancellationToken.None);
@@ -742,7 +742,7 @@ void M()
742742

743743
var analyzer = new FilterSpanTestAnalyzer(kind);
744744
var analyzerId = analyzer.GetAnalyzerId();
745-
var analyzerIdsToRequestDiagnostics = new[] { analyzerId };
745+
var analyzerIdsToRequestDiagnostics = ImmutableArray.Create(analyzerId);
746746
var analyzerReference = new AnalyzerImageReference(ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));
747747
project = project.AddAnalyzerReference(analyzerReference);
748748

@@ -771,7 +771,7 @@ async Task VerifyCallbackSpanAsync(TextSpan? filterSpan)
771771
: AnalysisKind.Semantic;
772772
var documentToAnalyze = kind == FilterSpanTestAnalyzer.AnalysisKind.AdditionalFile ? additionalDocument : document;
773773
_ = await DiagnosticComputer.GetDiagnosticsAsync(
774-
documentToAnalyze, project, Checksum.Null, filterSpan, analyzerIdsToRequestDiagnostics,
774+
documentToAnalyze, project, Checksum.Null, filterSpan, analyzerIdsToRequestDiagnostics, hostAnalyzerIds: [],
775775
analysisKind, new DiagnosticAnalyzerInfoCache(), workspace.Services,
776776
isExplicit: false, reportSuppressedDiagnostics: false, logPerformanceInfo: false, getTelemetryInfo: false,
777777
CancellationToken.None);
@@ -820,14 +820,14 @@ void M()
820820
var diagnosticAnalyzerInfoCache = new DiagnosticAnalyzerInfoCache();
821821

822822
var kind = actionKind == AnalyzerRegisterActionKind.SyntaxTree ? AnalysisKind.Syntax : AnalysisKind.Semantic;
823-
var analyzerIds = new[] { analyzer.GetAnalyzerId() };
823+
var analyzerIds = ImmutableArray.Create(analyzer.GetAnalyzerId());
824824

825825
// First invoke analysis with cancellation token, and verify canceled compilation and no reported diagnostics.
826826
Assert.Empty(analyzer.CanceledCompilations);
827827
try
828828
{
829829
_ = await DiagnosticComputer.GetDiagnosticsAsync(document, project, Checksum.Null, span: null,
830-
analyzerIds, kind, diagnosticAnalyzerInfoCache, workspace.Services, isExplicit: false, reportSuppressedDiagnostics: false,
830+
projectAnalyzerIds: [], analyzerIds, kind, diagnosticAnalyzerInfoCache, workspace.Services, isExplicit: false, reportSuppressedDiagnostics: false,
831831
logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: analyzer.CancellationToken);
832832

833833
throw ExceptionUtilities.Unreachable();
@@ -840,7 +840,7 @@ void M()
840840

841841
// Then invoke analysis without cancellation token, and verify non-cancelled diagnostic.
842842
var diagnosticsMap = await DiagnosticComputer.GetDiagnosticsAsync(document, project, Checksum.Null, span: null,
843-
analyzerIds, kind, diagnosticAnalyzerInfoCache, workspace.Services, isExplicit: false, reportSuppressedDiagnostics: false,
843+
projectAnalyzerIds: [], analyzerIds, kind, diagnosticAnalyzerInfoCache, workspace.Services, isExplicit: false, reportSuppressedDiagnostics: false,
844844
logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None);
845845
var builder = diagnosticsMap.Diagnostics.Single().diagnosticMap;
846846
var diagnostic = kind == AnalysisKind.Syntax ? builder.Syntax.Single().Item2.Single() : builder.Semantic.Single().Item2.Single();

src/Features/CSharp/Portable/SyncNamespaces/CSharpSyncNamespacesService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ internal sealed class CSharpSyncNamespacesService(
2323
{
2424
public override AbstractMatchFolderAndNamespaceDiagnosticAnalyzer<SyntaxKind, BaseNamespaceDeclarationSyntax> DiagnosticAnalyzer { get; } = diagnosticAnalyzer;
2525

26+
public override bool IsHostAnalyzer => false;
27+
2628
public override AbstractChangeNamespaceToMatchFolderCodeFixProvider CodeFixProvider { get; } = codeFixProvider;
2729
}

src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System.Collections.Generic;
6+
using System.Collections.Immutable;
67
using System.IO;
78
using System.Linq;
89
using System.Reflection;
@@ -51,7 +52,7 @@ private static VersionStamp GetAnalyzerVersion(string path)
5152
public static string GetAnalyzerAssemblyName(this DiagnosticAnalyzer analyzer)
5253
=> analyzer.GetType().Assembly.GetName().Name ?? throw ExceptionUtilities.Unreachable();
5354

54-
public static void AppendAnalyzerMap(this Dictionary<string, DiagnosticAnalyzer> analyzerMap, IEnumerable<DiagnosticAnalyzer> analyzers)
55+
public static void AppendAnalyzerMap(this Dictionary<string, DiagnosticAnalyzer> analyzerMap, ImmutableArray<DiagnosticAnalyzer> analyzers)
5556
{
5657
foreach (var analyzer in analyzers)
5758
{

src/Features/Core/Portable/Diagnostics/DiagnosticArguments.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Collections.Immutable;
56
using System.Diagnostics;
67
using System.Runtime.Serialization;
78
using Microsoft.CodeAnalysis.Text;
@@ -66,7 +67,13 @@ internal class DiagnosticArguments
6667
/// Array of analyzer IDs for analyzers that need to be executed for computing diagnostics.
6768
/// </summary>
6869
[DataMember(Order = 7)]
69-
public string[] AnalyzerIds;
70+
public ImmutableArray<string> ProjectAnalyzerIds;
71+
72+
/// <summary>
73+
/// Array of analyzer IDs for analyzers that need to be executed for computing diagnostics.
74+
/// </summary>
75+
[DataMember(Order = 8)]
76+
public ImmutableArray<string> HostAnalyzerIds;
7077

7178
/// <summary>
7279
/// Indicates diagnostic computation for an explicit user-invoked request,
@@ -83,14 +90,15 @@ public DiagnosticArguments(
8390
TextSpan? documentSpan,
8491
AnalysisKind? documentAnalysisKind,
8592
ProjectId projectId,
86-
string[] analyzerIds,
93+
ImmutableArray<string> projectAnalyzerIds,
94+
ImmutableArray<string> hostAnalyzerIds,
8795
bool isExplicit)
8896
{
8997
Debug.Assert(documentId != null || documentSpan == null);
9098
Debug.Assert(documentId != null || documentAnalysisKind == null);
9199
Debug.Assert(documentAnalysisKind is null or
92100
(AnalysisKind?)AnalysisKind.Syntax or (AnalysisKind?)AnalysisKind.Semantic);
93-
Debug.Assert(analyzerIds.Length > 0);
101+
Debug.Assert(projectAnalyzerIds.Length > 0 || hostAnalyzerIds.Length > 0);
94102

95103
ReportSuppressedDiagnostics = reportSuppressedDiagnostics;
96104
LogPerformanceInfo = logPerformanceInfo;
@@ -99,7 +107,8 @@ public DiagnosticArguments(
99107
DocumentSpan = documentSpan;
100108
DocumentAnalysisKind = documentAnalysisKind;
101109
ProjectId = projectId;
102-
AnalyzerIds = analyzerIds;
110+
ProjectAnalyzerIds = projectAnalyzerIds;
111+
HostAnalyzerIds = hostAnalyzerIds;
103112
IsExplicit = isExplicit;
104113
}
105114
}

src/Features/Core/Portable/ExternalAccess/UnitTesting/SolutionCrawler/UnitTestingWorkCoordinator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ private async Task EnqueueProjectConfigurationChangeWorkItemAsync(ProjectChanges
439439
!object.Equals(oldProject.AssemblyName, newProject.AssemblyName) ||
440440
!object.Equals(oldProject.Name, newProject.Name) ||
441441
!object.Equals(oldProject.AnalyzerOptions, newProject.AnalyzerOptions) ||
442+
!object.Equals(oldProject.HostAnalyzerOptions, newProject.HostAnalyzerOptions) ||
442443
!object.Equals(oldProject.DefaultNamespace, newProject.DefaultNamespace) ||
443444
!object.Equals(oldProject.OutputFilePath, newProject.OutputFilePath) ||
444445
!object.Equals(oldProject.OutputRefFilePath, newProject.OutputRefFilePath) ||

src/Features/Core/Portable/SyncNamespaces/AbstractSyncNamespacesService.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ internal abstract class AbstractSyncNamespacesService<TSyntaxKind, TNamespaceSyn
2525
where TNamespaceSyntax : SyntaxNode
2626
{
2727
public abstract AbstractMatchFolderAndNamespaceDiagnosticAnalyzer<TSyntaxKind, TNamespaceSyntax> DiagnosticAnalyzer { get; }
28+
public abstract bool IsHostAnalyzer { get; }
2829
public abstract AbstractChangeNamespaceToMatchFolderCodeFixProvider CodeFixProvider { get; }
2930

3031
/// <inheritdoc/>
@@ -38,7 +39,7 @@ public async Task<Solution> SyncNamespacesAsync(
3839

3940
var solution = projects[0].Solution;
4041
var diagnosticAnalyzers = ImmutableArray.Create<DiagnosticAnalyzer>(DiagnosticAnalyzer);
41-
var diagnosticsByProject = await GetDiagnosticsByProjectAsync(projects, diagnosticAnalyzers, cancellationToken).ConfigureAwait(false);
42+
var diagnosticsByProject = await GetDiagnosticsByProjectAsync(projects, diagnosticAnalyzers, IsHostAnalyzer, cancellationToken).ConfigureAwait(false);
4243

4344
// If no diagnostics are reported, then there is nothing to fix.
4445
if (diagnosticsByProject.Values.All(diagnostics => diagnostics.IsEmpty))
@@ -57,13 +58,14 @@ public async Task<Solution> SyncNamespacesAsync(
5758
private static async Task<ImmutableDictionary<Project, ImmutableArray<Diagnostic>>> GetDiagnosticsByProjectAsync(
5859
ImmutableArray<Project> projects,
5960
ImmutableArray<DiagnosticAnalyzer> diagnosticAnalyzers,
61+
bool isHostAnalyzer,
6062
CancellationToken cancellationToken)
6163
{
6264
var builder = ImmutableDictionary.CreateBuilder<Project, ImmutableArray<Diagnostic>>();
6365

6466
foreach (var project in projects)
6567
{
66-
var diagnostics = await GetDiagnosticsAsync(project, diagnosticAnalyzers, cancellationToken).ConfigureAwait(false);
68+
var diagnostics = await GetDiagnosticsAsync(project, diagnosticAnalyzers, isHostAnalyzer, cancellationToken).ConfigureAwait(false);
6769
builder.Add(project, diagnostics);
6870
}
6971

@@ -73,13 +75,14 @@ private static async Task<ImmutableDictionary<Project, ImmutableArray<Diagnostic
7375
private static async Task<ImmutableArray<Diagnostic>> GetDiagnosticsAsync(
7476
Project project,
7577
ImmutableArray<DiagnosticAnalyzer> diagnosticAnalyzers,
78+
bool isHostAnalyzer,
7679
CancellationToken cancellationToken)
7780
{
7881
var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
7982
RoslynDebug.AssertNotNull(compilation);
8083

8184
var analyzerOptions = new CompilationWithAnalyzersOptions(
82-
project.AnalyzerOptions,
85+
isHostAnalyzer ? project.HostAnalyzerOptions : project.AnalyzerOptions,
8386
onAnalyzerException: null,
8487
concurrentAnalysis: true,
8588
logAnalyzerExecutionTime: false,

0 commit comments

Comments
 (0)