diff --git a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs index c7068a7efadf4..8fea27df82f53 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService.cs @@ -46,6 +46,13 @@ public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) } } +/// +/// Only implementation of . Note: all methods in this class +/// should attempt to run in OOP as soon as possible. This is not always easy, especially if the apis +/// involve working with in-memory data structures that are not serializable. In those cases, we should +/// do all that work in-proc, and then send the results to OOP for further processing. Examples of this +/// are apis that take in a delegate callback to determine which analyzers to actually execute. +/// internal sealed partial class DiagnosticAnalyzerService : IDiagnosticAnalyzerService { private static readonly Option2 s_crashOnAnalyzerException = new("dotnet_crash_on_analyzer_exception", defaultValue: false); @@ -433,6 +440,44 @@ async Task IsCandidateForDeprioritizationBasedOnRegisteredActionsAsync(Dia } } + public async Task> ComputeDiagnosticsAsync( + TextDocument document, + TextSpan? range, + ImmutableArray allAnalyzers, + ImmutableArray syntaxAnalyzers, + ImmutableArray semanticSpanAnalyzers, + ImmutableArray semanticDocumentAnalyzers, + bool incrementalAnalysis, + bool logPerformanceInfo, + CancellationToken cancellationToken) + { + if (allAnalyzers.Length == 0) + return []; + + var client = await RemoteHostClient.TryGetClientAsync(document.Project, cancellationToken).ConfigureAwait(false); + if (client is not null) + { + var allAnalyzerIds = allAnalyzers.Select(a => a.GetAnalyzerId()).ToImmutableHashSet(); + var syntaxAnalyzersIds = syntaxAnalyzers.Select(a => a.GetAnalyzerId()).ToImmutableHashSet(); + var semanticSpanAnalyzersIds = semanticSpanAnalyzers.Select(a => a.GetAnalyzerId()).ToImmutableHashSet(); + var semanticDocumentAnalyzersIds = semanticDocumentAnalyzers.Select(a => a.GetAnalyzerId()).ToImmutableHashSet(); + + var result = await client.TryInvokeAsync>( + document.Project, + (service, solution, cancellationToken) => service.ComputeDiagnosticsAsync( + solution, document.Id, range, + allAnalyzerIds, syntaxAnalyzersIds, semanticSpanAnalyzersIds, semanticDocumentAnalyzersIds, + incrementalAnalysis, logPerformanceInfo, cancellationToken), + cancellationToken).ConfigureAwait(false); + + return result.HasValue ? result.Value : []; + } + + return await _incrementalAnalyzer.ComputeDiagnosticsAsync( + document, range, allAnalyzers, syntaxAnalyzers, semanticSpanAnalyzers, semanticDocumentAnalyzers, + incrementalAnalysis, logPerformanceInfo, cancellationToken).ConfigureAwait(false); + } + private sealed class DiagnosticAnalyzerComparer : IEqualityComparer { public static readonly DiagnosticAnalyzerComparer Instance = new(); diff --git a/src/Features/Core/Portable/Diagnostics/Service/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs b/src/Features/Core/Portable/Diagnostics/Service/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs index 569477bcc3789..db7b0a56f5026 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs @@ -60,13 +60,11 @@ public async Task> GetDiagnosticsForSpanAsync( var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); var project = document.Project; - var solutionState = project.Solution.SolutionState; var unfilteredAnalyzers = await StateManager - .GetOrCreateAnalyzersAsync(solutionState, project.State, cancellationToken) + .GetOrCreateAnalyzersAsync(project.Solution.SolutionState, project.State, cancellationToken) .ConfigureAwait(false); var analyzers = unfilteredAnalyzers - .WhereAsArray(a => DocumentAnalysisExecutor.IsAnalyzerEnabledForProject(a, document.Project, GlobalOptions)); - var hostAnalyzerInfo = await StateManager.GetOrCreateHostAnalyzerInfoAsync(solutionState, project.State, cancellationToken).ConfigureAwait(false); + .WhereAsArray(a => DocumentAnalysisExecutor.IsAnalyzerEnabledForProject(a, project, GlobalOptions)); // Note that some callers, such as diagnostic tagger, might pass in a range equal to the entire document span. // We clear out range for such cases as we are computing full document diagnostics. @@ -75,20 +73,30 @@ public async Task> GetDiagnosticsForSpanAsync( // We log performance info when we are computing diagnostics for a span var logPerformanceInfo = range.HasValue; - var compilationWithAnalyzers = await GetOrCreateCompilationWithAnalyzersAsync( - document.Project, analyzers, hostAnalyzerInfo, AnalyzerService.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false); // If we are computing full document diagnostics, we will attempt to perform incremental // member edit analysis. This analysis is currently only enabled with LSP pull diagnostics. - var incrementalAnalysis = !range.HasValue - && document is Document { SupportsSyntaxTree: true }; + var incrementalAnalysis = range is null && document is Document { SupportsSyntaxTree: true }; - using var _ = ArrayBuilder.GetInstance(out var list); - await GetAsync(list).ConfigureAwait(false); + using var _1 = PooledHashSet.GetInstance(out var deprioritizationCandidates); - return list.ToImmutableAndClear(); + deprioritizationCandidates.AddRange(await this.AnalyzerService.GetDeprioritizationCandidatesAsync( + project, analyzers, cancellationToken).ConfigureAwait(false)); + + var (syntaxAnalyzers, semanticSpanAnalyzers, semanticDocumentAnalyzers) = GetAllAnalyzers(); + syntaxAnalyzers = FilterAnalyzers(syntaxAnalyzers, AnalysisKind.Syntax, range, deprioritizationCandidates); + semanticSpanAnalyzers = FilterAnalyzers(semanticSpanAnalyzers, AnalysisKind.Semantic, range, deprioritizationCandidates); + semanticDocumentAnalyzers = FilterAnalyzers(semanticDocumentAnalyzers, AnalysisKind.Semantic, span: null, deprioritizationCandidates); + + var allDiagnostics = await this.AnalyzerService.ComputeDiagnosticsAsync( + document, range, analyzers, syntaxAnalyzers, semanticSpanAnalyzers, semanticDocumentAnalyzers, + incrementalAnalysis, logPerformanceInfo, + cancellationToken).ConfigureAwait(false); + return allDiagnostics.WhereAsArray(ShouldInclude); - async Task GetAsync(ArrayBuilder list) + (ImmutableArray syntaxAnalyzers, + ImmutableArray semanticSpanAnalyzers, + ImmutableArray semanticDocumentAnalyzers) GetAllAnalyzers() { try { @@ -153,11 +161,10 @@ async Task GetAsync(ArrayBuilder list) } } - await ComputeDocumentDiagnosticsAsync(syntaxAnalyzers.ToImmutable(), AnalysisKind.Syntax, range, list, incrementalAnalysis: false, cancellationToken).ConfigureAwait(false); - await ComputeDocumentDiagnosticsAsync(semanticSpanBasedAnalyzers.ToImmutable(), AnalysisKind.Semantic, range, list, incrementalAnalysis, cancellationToken).ConfigureAwait(false); - await ComputeDocumentDiagnosticsAsync(semanticDocumentBasedAnalyzers.ToImmutable(), AnalysisKind.Semantic, span: null, list, incrementalAnalysis: false, cancellationToken).ConfigureAwait(false); - - return; + return ( + syntaxAnalyzers.ToImmutableAndClear(), + semanticSpanBasedAnalyzers.ToImmutableAndClear(), + semanticDocumentBasedAnalyzers.ToImmutableAndClear()); } catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) { @@ -193,22 +200,13 @@ static bool ShouldIncludeAnalyzer( return true; } - async Task ComputeDocumentDiagnosticsAsync( + ImmutableArray FilterAnalyzers( ImmutableArray analyzers, AnalysisKind kind, TextSpan? span, - ArrayBuilder builder, - bool incrementalAnalysis, - CancellationToken cancellationToken) + HashSet deprioritizationCandidates) { - Debug.Assert(!incrementalAnalysis || kind == AnalysisKind.Semantic); - Debug.Assert(!incrementalAnalysis || analyzers.All(analyzer => analyzer.SupportsSpanBasedSemanticDiagnosticAnalysis())); - using var _1 = ArrayBuilder.GetInstance(analyzers.Length, out var filteredAnalyzers); - using var _2 = PooledHashSet.GetInstance(out var deprioritizationCandidates); - - deprioritizationCandidates.AddRange(await this.AnalyzerService.GetDeprioritizationCandidatesAsync( - project, analyzers, cancellationToken).ConfigureAwait(false)); foreach (var analyzer in analyzers) { @@ -223,43 +221,7 @@ async Task ComputeDocumentDiagnosticsAsync( filteredAnalyzers.Add(analyzer); } - if (filteredAnalyzers.Count == 0) - return; - - analyzers = filteredAnalyzers.ToImmutable(); - - var projectAnalyzers = analyzers.WhereAsArray(static (a, info) => !info.IsHostAnalyzer(a), hostAnalyzerInfo); - var hostAnalyzers = analyzers.WhereAsArray(static (a, info) => info.IsHostAnalyzer(a), hostAnalyzerInfo); - var analysisScope = new DocumentAnalysisScope(document, span, projectAnalyzers, hostAnalyzers, kind); - var executor = new DocumentAnalysisExecutor(analysisScope, compilationWithAnalyzers, _diagnosticAnalyzerRunner, logPerformanceInfo); - var version = await GetDiagnosticVersionAsync(document.Project, cancellationToken).ConfigureAwait(false); - - ImmutableDictionary> diagnosticsMap; - if (incrementalAnalysis) - { - using var _3 = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"Pri{priorityProvider.Priority.GetPriorityInt()}.Incremental"); - - diagnosticsMap = await _incrementalMemberEditAnalyzer.ComputeDiagnosticsAsync( - executor, - analyzers, - version, - cancellationToken).ConfigureAwait(false); - } - else - { - using var _3 = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"Pri{priorityProvider.Priority.GetPriorityInt()}.Document"); - - diagnosticsMap = await ComputeDocumentDiagnosticsCoreAsync(executor, cancellationToken).ConfigureAwait(false); - } - - foreach (var analyzer in analyzers) - { - var diagnostics = diagnosticsMap[analyzer]; - builder.AddRange(diagnostics.Where(ShouldInclude)); - } - - if (incrementalAnalysis) - _incrementalMemberEditAnalyzer.UpdateDocumentWithCachedDiagnostics((Document)document); + return filteredAnalyzers.ToImmutableAndClear(); } bool TryDeprioritizeAnalyzer( @@ -309,5 +271,62 @@ bool ShouldInclude(DiagnosticData diagnostic) && (shouldIncludeDiagnostic == null || shouldIncludeDiagnostic(diagnostic.Id)); } } + + public async Task> ComputeDiagnosticsAsync( + TextDocument document, + TextSpan? range, + ImmutableArray allAnalyzers, + ImmutableArray syntaxAnalyzers, + ImmutableArray semanticSpanAnalyzers, + ImmutableArray semanticDocumentAnalyzers, + bool incrementalAnalysis, + bool logPerformanceInfo, + CancellationToken cancellationToken) + { + // We log performance info when we are computing diagnostics for a span + var project = document.Project; + + var hostAnalyzerInfo = await StateManager.GetOrCreateHostAnalyzerInfoAsync( + project.Solution.SolutionState, project.State, cancellationToken).ConfigureAwait(false); + var compilationWithAnalyzers = await GetOrCreateCompilationWithAnalyzersAsync( + document.Project, allAnalyzers, hostAnalyzerInfo, AnalyzerService.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false); + + using var _ = ArrayBuilder.GetInstance(out var list); + + await ComputeDocumentDiagnosticsAsync(syntaxAnalyzers, AnalysisKind.Syntax, range, incrementalAnalysis: false).ConfigureAwait(false); + await ComputeDocumentDiagnosticsAsync(semanticSpanAnalyzers, AnalysisKind.Semantic, range, incrementalAnalysis).ConfigureAwait(false); + await ComputeDocumentDiagnosticsAsync(semanticDocumentAnalyzers, AnalysisKind.Semantic, span: null, incrementalAnalysis: false).ConfigureAwait(false); + + return list.ToImmutableAndClear(); + + async Task ComputeDocumentDiagnosticsAsync( + ImmutableArray analyzers, + AnalysisKind kind, + TextSpan? span, + bool incrementalAnalysis) + { + if (analyzers.Length == 0) + return; + + Debug.Assert(!incrementalAnalysis || kind == AnalysisKind.Semantic); + Debug.Assert(!incrementalAnalysis || analyzers.All(analyzer => analyzer.SupportsSpanBasedSemanticDiagnosticAnalysis())); + + var projectAnalyzers = analyzers.WhereAsArray(static (a, info) => !info.IsHostAnalyzer(a), hostAnalyzerInfo); + var hostAnalyzers = analyzers.WhereAsArray(static (a, info) => info.IsHostAnalyzer(a), hostAnalyzerInfo); + var analysisScope = new DocumentAnalysisScope(document, span, projectAnalyzers, hostAnalyzers, kind); + var executor = new DocumentAnalysisExecutor(analysisScope, compilationWithAnalyzers, _diagnosticAnalyzerRunner, logPerformanceInfo); + var version = await GetDiagnosticVersionAsync(document.Project, cancellationToken).ConfigureAwait(false); + + var computeTask = incrementalAnalysis + ? _incrementalMemberEditAnalyzer.ComputeDiagnosticsAsync(executor, analyzers, version, cancellationToken) + : ComputeDocumentDiagnosticsCoreAsync(executor, cancellationToken); + var diagnosticsMap = await computeTask.ConfigureAwait(false); + + if (incrementalAnalysis) + _incrementalMemberEditAnalyzer.UpdateDocumentWithCachedDiagnostics((Document)document); + + list.AddRange(diagnosticsMap.SelectMany(kvp => kvp.Value)); + } + } } } diff --git a/src/Workspaces/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs b/src/Workspaces/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs index 2a9840c22fef5..ea009d943211a 100644 --- a/src/Workspaces/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs +++ b/src/Workspaces/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Diagnostics; @@ -30,6 +31,17 @@ ValueTask> GetDeprioritizationCandidatesAsync( Checksum solutionChecksum, ProjectId projectId, ImmutableHashSet analyzerIds, CancellationToken cancellationToken); ValueTask CalculateDiagnosticsAsync(Checksum solutionChecksum, DiagnosticArguments arguments, CancellationToken cancellationToken); + + ValueTask> ComputeDiagnosticsAsync( + Checksum solutionChecksum, DocumentId documentId, TextSpan? range, + ImmutableHashSet allAnalyzerIds, + ImmutableHashSet syntaxAnalyzersIds, + ImmutableHashSet semanticSpanAnalyzersIds, + ImmutableHashSet semanticDocumentAnalyzersIds, + bool incrementalAnalysis, + bool logPerformanceInfo, + CancellationToken cancellationToken); + ValueTask> ProduceProjectDiagnosticsAsync( Checksum solutionChecksum, ProjectId projectId, ImmutableHashSet analyzerIds, diff --git a/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs b/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs index ed78aa2e3ed87..f714c306c32d0 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs @@ -10,10 +10,10 @@ using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Internal.Log; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Remote.Diagnostics; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Telemetry; +using Microsoft.CodeAnalysis.Text; using RoslynLogger = Microsoft.CodeAnalysis.Internal.Log.Logger; namespace Microsoft.CodeAnalysis.Remote; @@ -302,4 +302,35 @@ public ValueTask> GetDeprioritizationCandidatesAsync( }, cancellationToken); } + + public ValueTask> ComputeDiagnosticsAsync( + Checksum solutionChecksum, DocumentId documentId, TextSpan? range, + ImmutableHashSet allAnalyzerIds, + ImmutableHashSet syntaxAnalyzersIds, + ImmutableHashSet semanticSpanAnalyzersIds, + ImmutableHashSet semanticDocumentAnalyzersIds, + bool incrementalAnalysis, + bool logPerformanceInfo, + CancellationToken cancellationToken) + { + return RunWithSolutionAsync( + solutionChecksum, + async solution => + { + var document = solution.GetRequiredTextDocument(documentId); + var service = (DiagnosticAnalyzerService)solution.Services.GetRequiredService(); + + var allProjectAnalyzers = await service.GetProjectAnalyzersAsync(document.Project, cancellationToken).ConfigureAwait(false); + + var allAnalyzers = allProjectAnalyzers.FilterAnalyzers(allAnalyzerIds); + var syntaxAnalyzers = allProjectAnalyzers.FilterAnalyzers(syntaxAnalyzersIds); + var semanticSpanAnalyzers = allProjectAnalyzers.FilterAnalyzers(semanticSpanAnalyzersIds); + var semanticDocumentAnalyzers = allProjectAnalyzers.FilterAnalyzers(semanticDocumentAnalyzersIds); + + return await service.ComputeDiagnosticsAsync( + document, range, allAnalyzers, syntaxAnalyzers, semanticSpanAnalyzers, semanticDocumentAnalyzers, + incrementalAnalysis, logPerformanceInfo, cancellationToken).ConfigureAwait(false); + }, + cancellationToken); + } }