|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Diagnostics.Telemetry;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Telemetry;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Workspaces.Diagnostics;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
using static Microsoft.VisualStudio.Threading.ThreadingTools;
namespace Microsoft.CodeAnalysis.Remote.Diagnostics;
internal class DiagnosticComputer
{
/// <summary>
/// Cache of <see cref="CompilationWithAnalyzers"/> and a map from analyzer IDs to <see cref="DiagnosticAnalyzer"/>s
/// for all analyzers for the last project to be analyzed.
/// The <see cref="CompilationWithAnalyzers"/> instance is shared between all the following document analyses modes for the project:
/// 1. Span-based analysis for active document (lightbulb)
/// 2. Background analysis for active and open documents.
///
/// NOTE: We do not re-use this cache for project analysis as it leads to significant memory increase in the OOP process.
/// Additionally, we only store the cache entry for the last project to be analyzed instead of maintaining a CWT keyed off
/// each project in the solution, as the CWT does not seem to drop entries until ForceGC happens, leading to significant memory
/// pressure when there are large number of open documents across different projects to be analyzed by background analysis.
/// </summary>
private static CompilationWithAnalyzersCacheEntry? s_compilationWithAnalyzersCache = null;
/// <summary>
/// Set of high priority diagnostic computation tasks which are currently executing.
/// Any new high priority diagnostic request is added to this set before the core diagnostics
/// compute call is performed, and removed from this list after the computation finishes.
/// Any new normal priority diagnostic request first waits for all the high priority tasks in this set
/// to complete, and moves ahead only after this list becomes empty.
/// </summary>
/// <remarks>
/// Read/write access to this field is guarded by <see cref="s_gate"/>.
/// </remarks>
private static ImmutableHashSet<Task> s_highPriorityComputeTasks = [];
/// <summary>
/// Set of cancellation token sources for normal priority diagnostic computation tasks which are currently executing.
/// For any new normal priority diagnostic request, a new cancellation token source is created and added to this set
/// before the core diagnostics compute call is performed, and removed from this set after the computation finishes.
/// Any new high priority diagnostic request first fires cancellation on all the cancellation token sources in this set
/// to avoid resource contention between normal and high priority requests.
/// Canceled normal priority diagnostic requests are re-attempted from scratch after all the high priority requests complete.
/// </summary>
/// <remarks>
/// Read/write access to this field is guarded by <see cref="s_gate"/>.
/// </remarks>
private static ImmutableHashSet<CancellationTokenSource> s_normalPriorityCancellationTokenSources = [];
/// <summary>
/// Static gate controlling access to following static fields:
/// - <see cref="s_compilationWithAnalyzersCache"/>
/// - <see cref="s_highPriorityComputeTasks"/>
/// - <see cref="s_normalPriorityCancellationTokenSources"/>
/// </summary>
private static readonly object s_gate = new();
/// <summary>
/// Solution checksum for the diagnostic request.
/// We use this checksum and the <see cref="ProjectId"/> of the diagnostic request as the key
/// to the <see cref="s_compilationWithAnalyzersCache"/>.
/// </summary>
private readonly Checksum _solutionChecksum;
private readonly TextDocument? _document;
private readonly Project _project;
private readonly TextSpan? _span;
private readonly AnalysisKind? _analysisKind;
private readonly IPerformanceTrackerService? _performanceTracker;
private readonly DiagnosticAnalyzerInfoCache _analyzerInfoCache;
private readonly HostWorkspaceServices _hostWorkspaceServices;
private DiagnosticComputer(
TextDocument? document,
Project project,
Checksum solutionChecksum,
TextSpan? span,
AnalysisKind? analysisKind,
DiagnosticAnalyzerInfoCache analyzerInfoCache,
HostWorkspaceServices hostWorkspaceServices)
{
_document = document;
_project = project;
_solutionChecksum = solutionChecksum;
_span = span;
_analysisKind = analysisKind;
_analyzerInfoCache = analyzerInfoCache;
_hostWorkspaceServices = hostWorkspaceServices;
_performanceTracker = project.Solution.Services.GetService<IPerformanceTrackerService>();
}
public static Task<SerializableDiagnosticAnalysisResults> GetDiagnosticsAsync(
TextDocument? document,
Project project,
Checksum solutionChecksum,
TextSpan? span,
ImmutableArray<string> projectAnalyzerIds,
ImmutableArray<string> hostAnalyzerIds,
AnalysisKind? analysisKind,
DiagnosticAnalyzerInfoCache analyzerInfoCache,
HostWorkspaceServices hostWorkspaceServices,
bool isExplicit,
bool reportSuppressedDiagnostics,
bool logPerformanceInfo,
bool getTelemetryInfo,
CancellationToken cancellationToken)
{
// PERF: Due to the concept of InFlight solution snapshots in OOP process, we might have been
// handed a Project instance that does not match the Project instance corresponding to our
// cached CompilationWithAnalyzers instance, while the underlying Solution checksum matches
// for our cached entry and the incoming request.
// We detect this case upfront here and re-use the cached CompilationWithAnalyzers and Project
// instance for diagnostic computation, thus improving the performance of analyzer execution.
// This is an important performance optimization for lightbulb diagnostic computation.
// See https://github.com/dotnet/roslyn/issues/66968 for details.
lock (s_gate)
{
if (s_compilationWithAnalyzersCache?.SolutionChecksum == solutionChecksum &&
s_compilationWithAnalyzersCache.Project.Id == project.Id &&
s_compilationWithAnalyzersCache.Project != project)
{
project = s_compilationWithAnalyzersCache.Project;
if (document != null)
document = project.GetTextDocument(document.Id);
}
}
// We execute explicit, user-invoked diagnostics requests with higher priority compared to implicit requests
// from clients such as editor diagnostic tagger to show squiggles, background analysis to populate the error list, etc.
var diagnosticsComputer = new DiagnosticComputer(document, project, solutionChecksum, span, analysisKind, analyzerInfoCache, hostWorkspaceServices);
return isExplicit
? diagnosticsComputer.GetHighPriorityDiagnosticsAsync(projectAnalyzerIds, hostAnalyzerIds, reportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo, cancellationToken)
: diagnosticsComputer.GetNormalPriorityDiagnosticsAsync(projectAnalyzerIds, hostAnalyzerIds, reportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo, cancellationToken);
}
private async Task<SerializableDiagnosticAnalysisResults> GetHighPriorityDiagnosticsAsync(
ImmutableArray<string> projectAnalyzerIds,
ImmutableArray<string> hostAnalyzerIds,
bool reportSuppressedDiagnostics,
bool logPerformanceInfo,
bool getTelemetryInfo,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// Step 1:
// - Create the core 'computeTask' for computing diagnostics.
var computeTask = GetDiagnosticsAsync(projectAnalyzerIds, hostAnalyzerIds, reportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo, cancellationToken);
// Step 2:
// - Add this computeTask to the set of currently executing high priority tasks.
// This set of high priority tasks is used in 'GetNormalPriorityDiagnosticsAsync'
// method to ensure that any new or cancelled normal priority task waits for all
// the executing high priority tasks before starting its execution.
// - Note that it is critical to do this step prior to Step 3 below to ensure that
// any canceled normal priority tasks in Step 3 do not resume execution prior to
// completion of this high priority computeTask.
lock (s_gate)
{
Debug.Assert(!s_highPriorityComputeTasks.Contains(computeTask));
s_highPriorityComputeTasks = s_highPriorityComputeTasks.Add(computeTask);
}
try
{
// Step 3:
// - Force cancellation of all the executing normal priority tasks
// to minimize resource and CPU contention between normal priority tasks
// and the high priority computeTask in Step 4 below.
CancelNormalPriorityTasks();
// Step 4:
// - Execute the core 'computeTask' for diagnostic computation.
return await computeTask.ConfigureAwait(false);
}
finally
{
// Step 5:
// - Remove the 'computeTask' from the set of current executing high priority tasks.
lock (s_gate)
{
Debug.Assert(s_highPriorityComputeTasks.Contains(computeTask));
s_highPriorityComputeTasks = s_highPriorityComputeTasks.Remove(computeTask);
}
}
static void CancelNormalPriorityTasks()
{
ImmutableHashSet<CancellationTokenSource> cancellationTokenSources;
lock (s_gate)
{
cancellationTokenSources = s_normalPriorityCancellationTokenSources;
}
foreach (var cancellationTokenSource in cancellationTokenSources)
{
try
{
cancellationTokenSource.Cancel();
}
catch (ObjectDisposedException)
{
// CancellationTokenSource might get disposed if the normal priority
// task completes while we were executing this foreach loop.
// Gracefully handle this case and ignore this exception.
}
}
}
}
private async Task<SerializableDiagnosticAnalysisResults> GetNormalPriorityDiagnosticsAsync(
ImmutableArray<string> projectAnalyzerIds,
ImmutableArray<string> hostAnalyzerIds,
bool reportSuppressedDiagnostics,
bool logPerformanceInfo,
bool getTelemetryInfo,
CancellationToken cancellationToken)
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
// Step 1:
// - Normal priority task must wait for all the executing high priority tasks to complete
// before beginning execution.
await WaitForHighPriorityTasksAsync(cancellationToken).ConfigureAwait(false);
// Step 2:
// - Create a custom 'cancellationTokenSource' associated with the current normal priority
// request and add it to the tracked set of normal priority cancellation token sources.
// This token source allows normal priority computeTasks to be cancelled when
// a subsequent high priority diagnostic request is received.
using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
lock (s_gate)
{
s_normalPriorityCancellationTokenSources = s_normalPriorityCancellationTokenSources.Add(cancellationTokenSource);
}
try
{
// Step 3:
// - Execute the core compute task for diagnostic computation.
return await GetDiagnosticsAsync(projectAnalyzerIds, hostAnalyzerIds, reportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo,
cancellationTokenSource.Token).ConfigureAwait(false);
}
catch (OperationCanceledException ex) when (ex.CancellationToken == cancellationTokenSource.Token)
{
// Step 4:
// - Attempt to re-execute this cancelled normal priority task by running the loop again.
continue;
}
finally
{
// Step 5:
// - Remove the 'cancellationTokenSource' for completed or cancelled task.
// For the case where the computeTask was cancelled, we will create a new
// 'cancellationTokenSource' for the retry.
lock (s_gate)
{
Debug.Assert(s_normalPriorityCancellationTokenSources.Contains(cancellationTokenSource));
s_normalPriorityCancellationTokenSources = s_normalPriorityCancellationTokenSources.Remove(cancellationTokenSource);
}
}
}
static async Task WaitForHighPriorityTasksAsync(CancellationToken cancellationToken)
{
// We loop continuously until we have an empty high priority task queue.
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
ImmutableHashSet<Task> highPriorityTasksToAwait;
lock (s_gate)
{
highPriorityTasksToAwait = s_highPriorityComputeTasks;
}
if (highPriorityTasksToAwait.IsEmpty)
{
return;
}
// Wait for all the high priority tasks, ignoring all exceptions from it. Loop directly to avoid
// expensive allocations in Task.WhenAll.
foreach (var task in highPriorityTasksToAwait)
{
cancellationToken.ThrowIfCancellationRequested();
if (task.IsCompleted)
{
// Make sure to yield so continuations of 'task' can make progress.
await TaskScheduler.Default.SwitchTo(alwaysYield: true);
}
else
{
await task.WithCancellation(cancellationToken).NoThrowAwaitable(false);
}
}
}
}
}
private async Task<SerializableDiagnosticAnalysisResults> GetDiagnosticsAsync(
ImmutableArray<string> projectAnalyzerIds,
ImmutableArray<string> hostAnalyzerIds,
bool reportSuppressedDiagnostics,
bool logPerformanceInfo,
bool getTelemetryInfo,
CancellationToken cancellationToken)
{
var (compilationWithAnalyzers, analyzerToIdMap) = await GetOrCreateCompilationWithAnalyzersAsync(cancellationToken).ConfigureAwait(false);
if (compilationWithAnalyzers == null)
{
return SerializableDiagnosticAnalysisResults.Empty;
}
var (projectAnalyzers, hostAnalyzers) = GetAnalyzers(analyzerToIdMap, projectAnalyzerIds, hostAnalyzerIds);
if (projectAnalyzers.IsEmpty && hostAnalyzers.IsEmpty)
{
return SerializableDiagnosticAnalysisResults.Empty;
}
if (_document == null)
{
if (projectAnalyzers.Length < compilationWithAnalyzers.ProjectAnalyzers.Length)
{
Contract.ThrowIfFalse(projectAnalyzers.Length > 0 || compilationWithAnalyzers.HostCompilationWithAnalyzers is not null);
// PERF: Generate a new CompilationWithAnalyzers with trimmed analyzers for non-document analysis case.
compilationWithAnalyzers = new CompilationWithAnalyzersPair(
projectAnalyzers.Any() ? compilationWithAnalyzers.ProjectCompilation!.WithAnalyzers(projectAnalyzers, compilationWithAnalyzers.ProjectCompilationWithAnalyzers!.AnalysisOptions) : null,
compilationWithAnalyzers.HostCompilationWithAnalyzers);
}
if (hostAnalyzers.Length < compilationWithAnalyzers.HostAnalyzers.Length)
{
Contract.ThrowIfFalse(hostAnalyzers.Length > 0 || compilationWithAnalyzers.ProjectCompilationWithAnalyzers is not null);
// PERF: Generate a new CompilationWithAnalyzers with trimmed analyzers for non-document analysis case.
compilationWithAnalyzers = new CompilationWithAnalyzersPair(
compilationWithAnalyzers.ProjectCompilationWithAnalyzers,
hostAnalyzers.Any() ? compilationWithAnalyzers.HostCompilation!.WithAnalyzers(hostAnalyzers, compilationWithAnalyzers.HostCompilationWithAnalyzers!.AnalysisOptions) : null);
}
}
var skippedAnalyzersInfo = _project.GetSkippedAnalyzersInfo(_analyzerInfoCache);
return await AnalyzeAsync(compilationWithAnalyzers, analyzerToIdMap, projectAnalyzers, hostAnalyzers, skippedAnalyzersInfo,
reportSuppressedDiagnostics, logPerformanceInfo, getTelemetryInfo, cancellationToken).ConfigureAwait(false);
}
private async Task<SerializableDiagnosticAnalysisResults> AnalyzeAsync(
CompilationWithAnalyzersPair compilationWithAnalyzers,
BidirectionalMap<string, DiagnosticAnalyzer> analyzerToIdMap,
ImmutableArray<DiagnosticAnalyzer> projectAnalyzers,
ImmutableArray<DiagnosticAnalyzer> hostAnalyzers,
SkippedHostAnalyzersInfo skippedAnalyzersInfo,
bool reportSuppressedDiagnostics,
bool logPerformanceInfo,
bool getTelemetryInfo,
CancellationToken cancellationToken)
{
var documentAnalysisScope = _document != null
? new DocumentAnalysisScope(_document, _span, projectAnalyzers, hostAnalyzers, _analysisKind!.Value)
: null;
var (analysisResult, additionalPragmaSuppressionDiagnostics) = await compilationWithAnalyzers.GetAnalysisResultAsync(
documentAnalysisScope, _project, _analyzerInfoCache, cancellationToken).ConfigureAwait(false);
if (logPerformanceInfo && _performanceTracker != null)
{
// Only log telemetry snapshot is we have an active telemetry session,
// i.e. user has not opted out of reporting telemetry.
var telemetryService = _hostWorkspaceServices.GetRequiredService<IWorkspaceTelemetryService>();
if (telemetryService.HasActiveSession)
{
// +1 to include project itself
var unitCount = 1;
if (documentAnalysisScope == null)
unitCount += _project.DocumentIds.Count;
var performanceInfo = analysisResult?.MergedAnalyzerTelemetryInfo.ToAnalyzerPerformanceInfo(_analyzerInfoCache) ?? [];
_performanceTracker.AddSnapshot(performanceInfo, unitCount, forSpanAnalysis: _span.HasValue);
}
}
var builderMap = ImmutableDictionary<DiagnosticAnalyzer, DiagnosticAnalysisResultBuilder>.Empty;
if (analysisResult is not null)
{
builderMap = builderMap.AddRange(await analysisResult.ToResultBuilderMapAsync(
additionalPragmaSuppressionDiagnostics, documentAnalysisScope,
_project, VersionStamp.Default,
projectAnalyzers, hostAnalyzers, skippedAnalyzersInfo, reportSuppressedDiagnostics, cancellationToken).ConfigureAwait(false));
}
var telemetry = getTelemetryInfo
? GetTelemetryInfo(analysisResult, projectAnalyzers, hostAnalyzers, analyzerToIdMap)
: [];
return new SerializableDiagnosticAnalysisResults(Dehydrate(builderMap, analyzerToIdMap), telemetry);
}
private static ImmutableArray<(string analyzerId, SerializableDiagnosticMap diagnosticMap)> Dehydrate(
ImmutableDictionary<DiagnosticAnalyzer, DiagnosticAnalysisResultBuilder> builderMap,
BidirectionalMap<string, DiagnosticAnalyzer> analyzerToIdMap)
{
var diagnostics = new FixedSizeArrayBuilder<(string analyzerId, SerializableDiagnosticMap diagnosticMap)>(builderMap.Count);
foreach (var (analyzer, analyzerResults) in builderMap)
{
var analyzerId = GetAnalyzerId(analyzerToIdMap, analyzer);
diagnostics.Add((analyzerId,
new SerializableDiagnosticMap(
analyzerResults.SyntaxLocals.SelectAsArray(entry => (entry.Key, entry.Value)),
analyzerResults.SemanticLocals.SelectAsArray(entry => (entry.Key, entry.Value)),
analyzerResults.NonLocals.SelectAsArray(entry => (entry.Key, entry.Value)),
analyzerResults.Others)));
}
return diagnostics.MoveToImmutable();
}
private static ImmutableArray<(string analyzerId, AnalyzerTelemetryInfo)> GetTelemetryInfo(
AnalysisResultPair? analysisResult,
ImmutableArray<DiagnosticAnalyzer> projectAnalyzers,
ImmutableArray<DiagnosticAnalyzer> hostAnalyzers,
BidirectionalMap<string, DiagnosticAnalyzer> analyzerToIdMap)
{
Func<DiagnosticAnalyzer, bool> shouldInclude;
if (projectAnalyzers.Length < (analysisResult?.ProjectAnalysisResult?.AnalyzerTelemetryInfo.Count ?? 0)
|| hostAnalyzers.Length < (analysisResult?.HostAnalysisResult?.AnalyzerTelemetryInfo.Count ?? 0))
{
// Filter the telemetry info to the executed analyzers.
using var _1 = PooledHashSet<DiagnosticAnalyzer>.GetInstance(out var analyzersSet);
analyzersSet.AddRange(projectAnalyzers);
analyzersSet.AddRange(hostAnalyzers);
shouldInclude = analyzer => analyzersSet.Contains(analyzer);
}
else
{
shouldInclude = _ => true;
}
using var _2 = ArrayBuilder<(string analyzerId, AnalyzerTelemetryInfo)>.GetInstance(out var telemetryBuilder);
if (analysisResult is not null)
{
foreach (var (analyzer, analyzerTelemetry) in analysisResult.MergedAnalyzerTelemetryInfo)
{
if (shouldInclude(analyzer))
{
var analyzerId = GetAnalyzerId(analyzerToIdMap, analyzer);
telemetryBuilder.Add((analyzerId, analyzerTelemetry));
}
}
}
return telemetryBuilder.ToImmutableAndClear();
}
private static string GetAnalyzerId(BidirectionalMap<string, DiagnosticAnalyzer> analyzerMap, DiagnosticAnalyzer analyzer)
{
var analyzerId = analyzerMap.GetKeyOrDefault(analyzer);
Contract.ThrowIfNull(analyzerId);
return analyzerId;
}
private static (ImmutableArray<DiagnosticAnalyzer> projectAnalyzers, ImmutableArray<DiagnosticAnalyzer> hostAnalyzers) GetAnalyzers(BidirectionalMap<string, DiagnosticAnalyzer> analyzerMap, ImmutableArray<string> projectAnalyzerIds, ImmutableArray<string> hostAnalyzerIds)
{
// TODO: this probably need to be cached as well in analyzer service?
var projectBuilder = ImmutableArray.CreateBuilder<DiagnosticAnalyzer>();
var hostBuilder = ImmutableArray.CreateBuilder<DiagnosticAnalyzer>();
foreach (var analyzerId in projectAnalyzerIds)
{
if (analyzerMap.TryGetValue(analyzerId, out var analyzer))
{
projectBuilder.Add(analyzer);
}
}
foreach (var analyzerId in hostAnalyzerIds)
{
if (analyzerMap.TryGetValue(analyzerId, out var analyzer))
{
hostBuilder.Add(analyzer);
}
}
var projectAnalyzers = projectBuilder.ToImmutableAndClear();
if (hostAnalyzerIds.Any())
{
// If any host analyzers are active, make sure to also include any project diagnostic suppressors
hostBuilder.AddRange(projectAnalyzers.WhereAsArray(static a => a is DiagnosticSuppressor));
}
return (projectAnalyzers, hostBuilder.ToImmutableAndClear());
}
private async Task<(CompilationWithAnalyzersPair? compilationWithAnalyzers, BidirectionalMap<string, DiagnosticAnalyzer> analyzerToIdMap)> GetOrCreateCompilationWithAnalyzersAsync(CancellationToken cancellationToken)
{
var cacheEntry = await GetOrCreateCacheEntryAsync().ConfigureAwait(false);
return (cacheEntry.CompilationWithAnalyzers, cacheEntry.AnalyzerToIdMap);
async Task<CompilationWithAnalyzersCacheEntry> GetOrCreateCacheEntryAsync()
{
if (_document == null)
{
// Only use cache for document analysis.
return await CreateCompilationWithAnalyzersCacheEntryAsync(cancellationToken).ConfigureAwait(false);
}
lock (s_gate)
{
if (s_compilationWithAnalyzersCache?.SolutionChecksum == _solutionChecksum &&
s_compilationWithAnalyzersCache.Project == _project)
{
return s_compilationWithAnalyzersCache;
}
}
var entry = await CreateCompilationWithAnalyzersCacheEntryAsync(cancellationToken).ConfigureAwait(false);
lock (s_gate)
{
s_compilationWithAnalyzersCache = entry;
}
return entry;
}
}
private async Task<CompilationWithAnalyzersCacheEntry> CreateCompilationWithAnalyzersCacheEntryAsync(CancellationToken cancellationToken)
{
// We could consider creating a service so that we don't do this repeatedly if this shows up as perf cost
using var pooledObject = SharedPools.Default<HashSet<object>>().GetPooledObject();
using var pooledMap = SharedPools.Default<Dictionary<string, DiagnosticAnalyzer>>().GetPooledObject();
var referenceSet = pooledObject.Object;
var analyzerMapBuilder = pooledMap.Object;
// This follows what we do in DiagnosticAnalyzerInfoCache.CheckAnalyzerReferenceIdentity
using var _1 = ArrayBuilder<DiagnosticAnalyzer>.GetInstance(out var projectAnalyzerBuilder);
using var _2 = ArrayBuilder<DiagnosticAnalyzer>.GetInstance(out var hostAnalyzerBuilder);
foreach (var reference in _project.Solution.AnalyzerReferences)
{
if (!referenceSet.Add(reference.Id))
{
continue;
}
var analyzers = reference.GetAnalyzers(_project.Language);
// At times some Host analyzers should be treated as project analyzers and
// not be given access to the Host fallback options. In particular when we
// replace SDK CodeStyle analyzers with the Features analyzers.
if (ShouldRedirectAnalyzers(_project, reference))
{
projectAnalyzerBuilder.AddRange(analyzers);
}
else
{
hostAnalyzerBuilder.AddRange(analyzers);
}
analyzerMapBuilder.AppendAnalyzerMap(analyzers);
}
// Evaluate project analyzers after host analyzers to ensure duplicates in analyzerMapBuilder are
// overwritten with project analyzers if/when applicable.
foreach (var reference in _project.AnalyzerReferences)
{
if (!referenceSet.Add(reference.Id))
{
continue;
}
var analyzers = reference.GetAnalyzers(_project.Language);
projectAnalyzerBuilder.AddRange(analyzers);
hostAnalyzerBuilder.AddRange(analyzers.WhereAsArray(static a => a is DiagnosticSuppressor));
analyzerMapBuilder.AppendAnalyzerMap(analyzers);
}
var compilationWithAnalyzers = projectAnalyzerBuilder.Count > 0 || hostAnalyzerBuilder.Count > 0
? await CreateCompilationWithAnalyzerAsync(projectAnalyzerBuilder.ToImmutable(), hostAnalyzerBuilder.ToImmutable(), cancellationToken).ConfigureAwait(false)
: null;
var analyzerToIdMap = new BidirectionalMap<string, DiagnosticAnalyzer>(analyzerMapBuilder);
return new CompilationWithAnalyzersCacheEntry(_solutionChecksum, _project, compilationWithAnalyzers, analyzerToIdMap);
static bool ShouldRedirectAnalyzers(Project project, AnalyzerReference reference)
{
// When replacing SDK CodeStyle analyzers we should redirect Features analyzers
// so they are treated as project analyzers.
return project.State.HasSdkCodeStyleAnalyzers && reference.IsFeaturesAnalyzer();
}
}
private async Task<CompilationWithAnalyzersPair> CreateCompilationWithAnalyzerAsync(ImmutableArray<DiagnosticAnalyzer> projectAnalyzers, ImmutableArray<DiagnosticAnalyzer> hostAnalyzers, CancellationToken cancellationToken)
{
Contract.ThrowIfFalse(!projectAnalyzers.IsEmpty || !hostAnalyzers.IsEmpty);
// Always run analyzers concurrently in OOP
const bool concurrentAnalysis = true;
// Get original compilation
var compilation = await _project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
// Fork compilation with concurrent build. this is okay since WithAnalyzers will fork compilation
// anyway to attach event queue. This should make compiling compilation concurrent and make things
// faster
compilation = compilation.WithOptions(compilation.Options.WithConcurrentBuild(concurrentAnalysis));
// Run analyzers concurrently, with performance logging and reporting suppressed diagnostics.
// This allows all client requests with or without performance data and/or suppressed diagnostics to be satisfied.
// TODO: can we support analyzerExceptionFilter in remote host?
// right now, host doesn't support watson, we might try to use new NonFatal watson API?
var projectAnalyzerOptions = new CompilationWithAnalyzersOptions(
options: _project.AnalyzerOptions,
onAnalyzerException: null,
analyzerExceptionFilter: null,
concurrentAnalysis: concurrentAnalysis,
logAnalyzerExecutionTime: true,
reportSuppressedDiagnostics: true);
var hostAnalyzerOptions = new CompilationWithAnalyzersOptions(
options: _project.HostAnalyzerOptions,
onAnalyzerException: null,
analyzerExceptionFilter: null,
concurrentAnalysis: concurrentAnalysis,
logAnalyzerExecutionTime: true,
reportSuppressedDiagnostics: true);
return new CompilationWithAnalyzersPair(
projectAnalyzers.Any() ? compilation.WithAnalyzers(projectAnalyzers, projectAnalyzerOptions) : null,
hostAnalyzers.Any() ? compilation.WithAnalyzers(hostAnalyzers, hostAnalyzerOptions) : null);
}
private sealed class CompilationWithAnalyzersCacheEntry
{
public Checksum SolutionChecksum { get; }
public Project Project { get; }
public CompilationWithAnalyzersPair? CompilationWithAnalyzers { get; }
public BidirectionalMap<string, DiagnosticAnalyzer> AnalyzerToIdMap { get; }
public CompilationWithAnalyzersCacheEntry(Checksum solutionChecksum, Project project, CompilationWithAnalyzersPair? compilationWithAnalyzers, BidirectionalMap<string, DiagnosticAnalyzer> analyzerToIdMap)
{
SolutionChecksum = solutionChecksum;
Project = project;
CompilationWithAnalyzers = compilationWithAnalyzers;
AnalyzerToIdMap = analyzerToIdMap;
}
}
}
|