Skip to content

Commit 2a2f7bb

Browse files
Additional cleanup of the DiagnosticAnalyzerServier (#80005)
2 parents 9f8e62d + d3b74d7 commit 2a2f7bb

File tree

31 files changed

+2248
-2381
lines changed

31 files changed

+2248
-2381
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ public async Task TestDiagnosticSpan()
338338
var compilerEngineCompilation = (CSharpCompilation)(await compilerEngineWorkspace.CurrentSolution.Projects.Single().GetRequiredCompilationAsync(CancellationToken.None));
339339

340340
var diagnostics = compilerEngineCompilation.GetAnalyzerDiagnostics([analyzer]);
341-
AssertEx.Any(diagnostics, d => d.Id == DocumentAnalysisExecutor.AnalyzerExceptionDiagnosticId);
341+
AssertEx.Any(diagnostics, d => d.Id == DiagnosticAnalyzerService.AnalyzerExceptionDiagnosticId);
342342
}
343343

344344
private sealed class InvalidSpanAnalyzer : DiagnosticAnalyzer

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.Threading;
1212
using System.Threading.Tasks;
1313
using Microsoft.CodeAnalysis.Diagnostics.Telemetry;
14+
using Roslyn.Utilities;
1415

1516
namespace Microsoft.CodeAnalysis.Diagnostics;
1617

@@ -41,8 +42,8 @@ public static void AppendAnalyzerMap(this Dictionary<string, DiagnosticAnalyzer>
4142
}
4243
}
4344

44-
public static IEnumerable<AnalyzerPerformanceInfo> ToAnalyzerPerformanceInfo(this IDictionary<DiagnosticAnalyzer, AnalyzerTelemetryInfo> analysisResult, DiagnosticAnalyzerInfoCache analyzerInfo)
45-
=> analysisResult.Select(kv => new AnalyzerPerformanceInfo(kv.Key.GetAnalyzerId(), analyzerInfo.IsTelemetryCollectionAllowed(kv.Key), kv.Value.ExecutionTime));
45+
public static ImmutableArray<AnalyzerPerformanceInfo> ToAnalyzerPerformanceInfo(this IDictionary<DiagnosticAnalyzer, AnalyzerTelemetryInfo> analysisResult, DiagnosticAnalyzerInfoCache analyzerInfo)
46+
=> analysisResult.SelectAsArray(kv => new AnalyzerPerformanceInfo(kv.Key.GetAnalyzerId(), analyzerInfo.IsTelemetryCollectionAllowed(kv.Key), kv.Value.ExecutionTime));
4647

4748
public static Task<ImmutableArray<DiagnosticDescriptor>> GetDiagnosticDescriptorsAsync(
4849
this Project project,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.Collections.Immutable;
7+
using Roslyn.Utilities;
8+
9+
namespace Microsoft.CodeAnalysis.Diagnostics;
10+
11+
internal sealed partial class DiagnosticAnalyzerService
12+
{
13+
private sealed class ChecksumAndAnalyzersEqualityComparer
14+
: IEqualityComparer<(Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers)>
15+
{
16+
public static readonly ChecksumAndAnalyzersEqualityComparer Instance = new();
17+
18+
public bool Equals((Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers) x, (Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers) y)
19+
{
20+
if (x.checksum != y.checksum)
21+
return false;
22+
23+
// Fast path for when the analyzers are the same reference.
24+
return x.analyzers == y.analyzers || x.analyzers.SetEquals(y.analyzers);
25+
}
26+
27+
public int GetHashCode((Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers) obj)
28+
{
29+
var hashCode = obj.checksum.GetHashCode();
30+
31+
// Use addition so that we're resilient to any order for the analyzers.
32+
foreach (var analyzer in obj.analyzers)
33+
hashCode += analyzer.GetHashCode();
34+
35+
return hashCode;
36+
}
37+
}
38+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.IO;
8+
using System.Reflection;
9+
10+
namespace Microsoft.CodeAnalysis.Diagnostics;
11+
12+
internal sealed partial class DiagnosticAnalyzerService
13+
{
14+
private sealed class DiagnosticAnalyzerComparer : IEqualityComparer<DiagnosticAnalyzer>
15+
{
16+
public static readonly DiagnosticAnalyzerComparer Instance = new();
17+
18+
public bool Equals(DiagnosticAnalyzer? x, DiagnosticAnalyzer? y)
19+
=> (x, y) switch
20+
{
21+
(null, null) => true,
22+
(null, _) => false,
23+
(_, null) => false,
24+
_ => GetAnalyzerIdAndLastWriteTime(x) == GetAnalyzerIdAndLastWriteTime(y)
25+
};
26+
27+
public int GetHashCode(DiagnosticAnalyzer obj) => GetAnalyzerIdAndLastWriteTime(obj).GetHashCode();
28+
29+
private static (string analyzerId, DateTime lastWriteTime) GetAnalyzerIdAndLastWriteTime(DiagnosticAnalyzer analyzer)
30+
{
31+
// Get the unique ID for given diagnostic analyzer.
32+
// note that we also put version stamp so that we can detect changed analyzer.
33+
var typeInfo = analyzer.GetType().GetTypeInfo();
34+
return (analyzer.GetAnalyzerId(), GetAnalyzerLastWriteTime(typeInfo.Assembly.Location));
35+
}
36+
37+
private static DateTime GetAnalyzerLastWriteTime(string path)
38+
{
39+
if (path == null || !File.Exists(path))
40+
return default;
41+
42+
return File.GetLastWriteTimeUtc(path);
43+
}
44+
}
45+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.Collections.Immutable;
7+
using System.Diagnostics;
8+
using System.Linq;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Microsoft.CodeAnalysis.PooledObjects;
12+
13+
namespace Microsoft.CodeAnalysis.Diagnostics;
14+
15+
internal sealed partial class DiagnosticAnalyzerService
16+
{
17+
private sealed class HostAnalyzerInfo
18+
{
19+
private const int BuiltInCompilerPriority = -2;
20+
private const int RegularDiagnosticAnalyzerPriority = -1;
21+
22+
private readonly ImmutableHashSet<DiagnosticAnalyzer> _hostAnalyzers;
23+
private readonly ImmutableHashSet<DiagnosticAnalyzer> _allAnalyzers;
24+
public readonly ImmutableArray<DiagnosticAnalyzer> OrderedAllAnalyzers;
25+
26+
public HostAnalyzerInfo(
27+
ImmutableHashSet<DiagnosticAnalyzer> hostAnalyzers,
28+
ImmutableHashSet<DiagnosticAnalyzer> allAnalyzers)
29+
{
30+
_hostAnalyzers = hostAnalyzers;
31+
_allAnalyzers = allAnalyzers;
32+
33+
// order analyzers.
34+
// order will be in this order
35+
// BuiltIn Compiler Analyzer (C#/VB) < Regular DiagnosticAnalyzers < Document/ProjectDiagnosticAnalyzers
36+
OrderedAllAnalyzers = [.. _allAnalyzers.OrderBy(PriorityComparison)];
37+
}
38+
39+
public bool IsHostAnalyzer(DiagnosticAnalyzer analyzer)
40+
=> _hostAnalyzers.Contains(analyzer);
41+
42+
public HostAnalyzerInfo WithExcludedAnalyzers(ImmutableHashSet<DiagnosticAnalyzer> excludedAnalyzers)
43+
{
44+
if (excludedAnalyzers.IsEmpty)
45+
{
46+
return this;
47+
}
48+
49+
return new(_hostAnalyzers, _allAnalyzers.Except(excludedAnalyzers));
50+
}
51+
52+
private int PriorityComparison(DiagnosticAnalyzer state1, DiagnosticAnalyzer state2)
53+
=> GetPriority(state1) - GetPriority(state2);
54+
55+
private static int GetPriority(DiagnosticAnalyzer state)
56+
{
57+
// compiler gets highest priority
58+
if (state.IsCompilerAnalyzer())
59+
{
60+
return BuiltInCompilerPriority;
61+
}
62+
63+
return state switch
64+
{
65+
DocumentDiagnosticAnalyzer analyzer => analyzer.Priority,
66+
_ => RegularDiagnosticAnalyzerPriority,
67+
};
68+
}
69+
}
70+
71+
/// <summary>
72+
/// Return <see cref="DiagnosticAnalyzer"/>s for the given <see cref="Project"/>.
73+
/// </summary>
74+
internal ImmutableArray<DiagnosticAnalyzer> GetProjectAnalyzers(Project project)
75+
{
76+
var hostAnalyzerInfo = GetOrCreateHostAnalyzerInfo(project);
77+
var projectAnalyzerInfo = GetOrCreateProjectAnalyzerInfo(project);
78+
return hostAnalyzerInfo.OrderedAllAnalyzers.AddRange(projectAnalyzerInfo.Analyzers);
79+
}
80+
81+
private HostAnalyzerInfo GetOrCreateHostAnalyzerInfo(Project project)
82+
{
83+
var projectAnalyzerInfo = GetOrCreateProjectAnalyzerInfo(project);
84+
85+
var solution = project.Solution;
86+
var key = new HostAnalyzerInfoKey(project.Language, project.State.HasSdkCodeStyleAnalyzers, solution.SolutionState.Analyzers.HostAnalyzerReferences);
87+
// Some Host Analyzers may need to be treated as Project Analyzers so that they do not have access to the
88+
// Host fallback options. These ids will be used when building up the Host and Project analyzer collections.
89+
var referenceIdsToRedirect = GetReferenceIdsToRedirectAsProjectAnalyzers(project);
90+
var hostAnalyzerInfo = ImmutableInterlocked.GetOrAdd(ref _hostAnalyzerStateMap, key, CreateLanguageSpecificAnalyzerMap, (solution.SolutionState.Analyzers, referenceIdsToRedirect));
91+
return hostAnalyzerInfo.WithExcludedAnalyzers(projectAnalyzerInfo.SkippedAnalyzersInfo.SkippedAnalyzers);
92+
93+
static HostAnalyzerInfo CreateLanguageSpecificAnalyzerMap(HostAnalyzerInfoKey arg, (HostDiagnosticAnalyzers HostAnalyzers, ImmutableHashSet<object> ReferenceIdsToRedirect) state)
94+
{
95+
var language = arg.Language;
96+
var analyzersPerReference = state.HostAnalyzers.GetOrCreateHostDiagnosticAnalyzersPerReference(language);
97+
98+
var (hostAnalyzerCollection, projectAnalyzerCollection) = GetAnalyzerCollections(analyzersPerReference, state.ReferenceIdsToRedirect);
99+
var (hostAnalyzers, allAnalyzers) = PartitionAnalyzers(projectAnalyzerCollection, hostAnalyzerCollection, includeWorkspacePlaceholderAnalyzers: true);
100+
101+
return new HostAnalyzerInfo(hostAnalyzers, allAnalyzers);
102+
}
103+
104+
static (ImmutableArray<ImmutableArray<DiagnosticAnalyzer>> HostAnalyzerCollection, ImmutableArray<ImmutableArray<DiagnosticAnalyzer>> ProjectAnalyzerCollection) GetAnalyzerCollections(
105+
ImmutableDictionary<object, ImmutableArray<DiagnosticAnalyzer>> analyzersPerReference,
106+
ImmutableHashSet<object> referenceIdsToRedirectAsProjectAnalyzers)
107+
{
108+
if (referenceIdsToRedirectAsProjectAnalyzers.IsEmpty)
109+
{
110+
return ([.. analyzersPerReference.Values], []);
111+
}
112+
113+
using var _1 = ArrayBuilder<ImmutableArray<DiagnosticAnalyzer>>.GetInstance(out var hostAnalyzerCollection);
114+
using var _2 = ArrayBuilder<ImmutableArray<DiagnosticAnalyzer>>.GetInstance(out var projectAnalyzerCollection);
115+
116+
foreach (var (referenceId, analyzers) in analyzersPerReference)
117+
{
118+
if (referenceIdsToRedirectAsProjectAnalyzers.Contains(referenceId))
119+
{
120+
projectAnalyzerCollection.Add(analyzers);
121+
}
122+
else
123+
{
124+
hostAnalyzerCollection.Add(analyzers);
125+
}
126+
}
127+
128+
return (hostAnalyzerCollection.ToImmutableAndClear(), projectAnalyzerCollection.ToImmutableAndClear());
129+
}
130+
}
131+
132+
private static (ImmutableHashSet<DiagnosticAnalyzer> hostAnalyzers, ImmutableHashSet<DiagnosticAnalyzer> allAnalyzers) PartitionAnalyzers(
133+
ImmutableArray<ImmutableArray<DiagnosticAnalyzer>> projectAnalyzerCollection,
134+
ImmutableArray<ImmutableArray<DiagnosticAnalyzer>> hostAnalyzerCollection,
135+
bool includeWorkspacePlaceholderAnalyzers)
136+
{
137+
using var _1 = PooledHashSet<DiagnosticAnalyzer>.GetInstance(out var hostAnalyzers);
138+
using var _2 = PooledHashSet<DiagnosticAnalyzer>.GetInstance(out var allAnalyzers);
139+
140+
if (includeWorkspacePlaceholderAnalyzers)
141+
{
142+
hostAnalyzers.Add(FileContentLoadAnalyzer.Instance);
143+
hostAnalyzers.Add(GeneratorDiagnosticsPlaceholderAnalyzer.Instance);
144+
allAnalyzers.Add(FileContentLoadAnalyzer.Instance);
145+
allAnalyzers.Add(GeneratorDiagnosticsPlaceholderAnalyzer.Instance);
146+
}
147+
148+
foreach (var analyzers in projectAnalyzerCollection)
149+
{
150+
foreach (var analyzer in analyzers)
151+
{
152+
Debug.Assert(analyzer != FileContentLoadAnalyzer.Instance && analyzer != GeneratorDiagnosticsPlaceholderAnalyzer.Instance);
153+
allAnalyzers.Add(analyzer);
154+
}
155+
}
156+
157+
foreach (var analyzers in hostAnalyzerCollection)
158+
{
159+
foreach (var analyzer in analyzers)
160+
{
161+
Debug.Assert(analyzer != FileContentLoadAnalyzer.Instance && analyzer != GeneratorDiagnosticsPlaceholderAnalyzer.Instance);
162+
allAnalyzers.Add(analyzer);
163+
hostAnalyzers.Add(analyzer);
164+
}
165+
}
166+
167+
return (hostAnalyzers.ToImmutableHashSet(), allAnalyzers.ToImmutableHashSet());
168+
}
169+
170+
private static ImmutableHashSet<object> GetReferenceIdsToRedirectAsProjectAnalyzers(Project project)
171+
{
172+
if (project.State.HasSdkCodeStyleAnalyzers)
173+
{
174+
// When a project uses CodeStyle analyzers added by the SDK, we remove them in favor of the
175+
// Features analyzers. We need to then treat the Features analyzers as Project analyzers so
176+
// they do not get access to the Host fallback options.
177+
return GetFeaturesAnalyzerReferenceIds(project.Solution.SolutionState.Analyzers);
178+
}
179+
180+
return [];
181+
182+
static ImmutableHashSet<object> GetFeaturesAnalyzerReferenceIds(HostDiagnosticAnalyzers hostAnalyzers)
183+
{
184+
var builder = ImmutableHashSet.CreateBuilder<object>();
185+
186+
foreach (var analyzerReference in hostAnalyzers.HostAnalyzerReferences)
187+
{
188+
if (analyzerReference.IsFeaturesAnalyzer())
189+
builder.Add(analyzerReference.Id);
190+
}
191+
192+
return builder.ToImmutable();
193+
}
194+
}
195+
196+
private readonly record struct HostAnalyzerInfoKey(
197+
string Language, bool HasSdkCodeStyleAnalyzers, IReadOnlyList<AnalyzerReference> AnalyzerReferences);
198+
}

0 commit comments

Comments
 (0)