File: Features\Diagnostics\EngineV2\DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs
Web Access
Project: src\src\LanguageServer\Protocol\Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol)
// 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.CodeActions;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Telemetry;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2
{
    internal partial class DiagnosticIncrementalAnalyzer
    {
        public async Task<ImmutableArray<DiagnosticData>> GetDiagnosticsForSpanAsync(
            TextDocument document,
            TextSpan? range,
            Func<string, bool>? shouldIncludeDiagnostic,
            bool includeSuppressedDiagnostics,
            bool includeCompilerDiagnostics,
            ICodeActionRequestPriorityProvider priorityProvider,
            DiagnosticKind diagnosticKinds,
            bool isExplicit,
            CancellationToken cancellationToken)
        {
            using var _ = ArrayBuilder<DiagnosticData>.GetInstance(out var list);
 
            var getter = await LatestDiagnosticsForSpanGetter.CreateAsync(
                this, document, range, includeSuppressedDiagnostics, includeCompilerDiagnostics,
                priorityProvider, shouldIncludeDiagnostic, diagnosticKinds, isExplicit, cancellationToken).ConfigureAwait(false);
            await getter.GetAsync(list, cancellationToken).ConfigureAwait(false);
 
            return list.ToImmutableAndClear();
        }
 
        /// <summary>
        /// Get diagnostics for given span either by using cache or calculating it on the spot.
        /// </summary>
        private sealed class LatestDiagnosticsForSpanGetter
        {
            // PERF: Cache the last Project and corresponding CompilationWithAnalyzers used to compute analyzer diagnostics for span.
            //       This is now required as async lightbulb will query and execute different priority buckets of analyzers with multiple
            //       calls, and we want to reuse CompilationWithAnalyzers instance if possible. 
            private static readonly WeakReference<ProjectAndCompilationWithAnalyzers?> s_lastProjectAndCompilationWithAnalyzers = new(null);
 
            private readonly DiagnosticIncrementalAnalyzer _owner;
            private readonly TextDocument _document;
            private readonly SourceText _text;
 
            private readonly ImmutableArray<StateSet> _stateSets;
            private readonly CompilationWithAnalyzersPair? _compilationWithAnalyzers;
 
            private readonly TextSpan? _range;
            private readonly bool _includeSuppressedDiagnostics;
            private readonly ICodeActionRequestPriorityProvider _priorityProvider;
            private readonly Func<string, bool>? _shouldIncludeDiagnostic;
            private readonly bool _includeCompilerDiagnostics;
            private readonly bool _isExplicit;
            private readonly bool _logPerformanceInfo;
            private readonly bool _incrementalAnalysis;
            private readonly DiagnosticKind _diagnosticKind;
 
            private delegate Task<IEnumerable<DiagnosticData>> DiagnosticsGetterAsync(DiagnosticAnalyzer analyzer, DocumentAnalysisExecutor executor, CancellationToken cancellationToken);
 
            public static async Task<LatestDiagnosticsForSpanGetter> CreateAsync(
                 DiagnosticIncrementalAnalyzer owner,
                 TextDocument document,
                 TextSpan? range,
                 bool includeSuppressedDiagnostics,
                 bool includeCompilerDiagnostics,
                 ICodeActionRequestPriorityProvider priorityProvider,
                 Func<string, bool>? shouldIncludeDiagnostic,
                 DiagnosticKind diagnosticKinds,
                 bool isExplicit,
                 CancellationToken cancellationToken)
            {
                var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
                var unfilteredStateSets = await owner._stateManager
                    .GetOrCreateStateSetsAsync(document.Project, cancellationToken)
                    .ConfigureAwait(false);
                var stateSets = unfilteredStateSets
                    .Where(s => DocumentAnalysisExecutor.IsAnalyzerEnabledForProject(s.Analyzer, document.Project, owner.GlobalOptions))
                    .ToImmutableArray();
 
                // 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.
                if (range == new TextSpan(0, text.Length))
                    range = null;
 
                // We log performance info when we are computing diagnostics for a span
                var logPerformanceInfo = range.HasValue;
                var compilationWithAnalyzers = await GetOrCreateCompilationWithAnalyzersAsync(document.Project, stateSets, includeSuppressedDiagnostics, owner.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 };
 
                return new LatestDiagnosticsForSpanGetter(
                    owner, compilationWithAnalyzers, document, text, stateSets, shouldIncludeDiagnostic, includeCompilerDiagnostics,
                    range, includeSuppressedDiagnostics, priorityProvider,
                    isExplicit, logPerformanceInfo, incrementalAnalysis, diagnosticKinds);
            }
 
            private static async Task<CompilationWithAnalyzersPair?> GetOrCreateCompilationWithAnalyzersAsync(
                Project project,
                ImmutableArray<StateSet> stateSets,
                bool includeSuppressedDiagnostics,
                bool crashOnAnalyzerException,
                CancellationToken cancellationToken)
            {
                if (s_lastProjectAndCompilationWithAnalyzers.TryGetTarget(out var projectAndCompilationWithAnalyzers) &&
                    projectAndCompilationWithAnalyzers?.Project == project)
                {
                    if (projectAndCompilationWithAnalyzers.CompilationWithAnalyzers == null)
                    {
                        return null;
                    }
 
                    if (HasAllAnalyzers(stateSets, projectAndCompilationWithAnalyzers.CompilationWithAnalyzers))
                    {
                        return projectAndCompilationWithAnalyzers.CompilationWithAnalyzers;
                    }
                }
 
                var compilationWithAnalyzers = await CreateCompilationWithAnalyzersAsync(project, stateSets, includeSuppressedDiagnostics, crashOnAnalyzerException, cancellationToken).ConfigureAwait(false);
                s_lastProjectAndCompilationWithAnalyzers.SetTarget(new ProjectAndCompilationWithAnalyzers(project, compilationWithAnalyzers));
                return compilationWithAnalyzers;
 
                static bool HasAllAnalyzers(IEnumerable<StateSet> stateSets, CompilationWithAnalyzersPair compilationWithAnalyzers)
                {
                    foreach (var stateSet in stateSets)
                    {
                        if (stateSet.IsHostAnalyzer && !compilationWithAnalyzers.HostAnalyzers.Contains(stateSet.Analyzer))
                            return false;
                        else if (!stateSet.IsHostAnalyzer && !compilationWithAnalyzers.ProjectAnalyzers.Contains(stateSet.Analyzer))
                            return false;
                    }
 
                    return true;
                }
            }
 
            private LatestDiagnosticsForSpanGetter(
                DiagnosticIncrementalAnalyzer owner,
                CompilationWithAnalyzersPair? compilationWithAnalyzers,
                TextDocument document,
                SourceText text,
                ImmutableArray<StateSet> stateSets,
                Func<string, bool>? shouldIncludeDiagnostic,
                bool includeCompilerDiagnostics,
                TextSpan? range,
                bool includeSuppressedDiagnostics,
                ICodeActionRequestPriorityProvider priorityProvider,
                bool isExplicit,
                bool logPerformanceInfo,
                bool incrementalAnalysis,
                DiagnosticKind diagnosticKind)
            {
                _owner = owner;
                _compilationWithAnalyzers = compilationWithAnalyzers;
                _document = document;
                _text = text;
                _stateSets = stateSets;
                _shouldIncludeDiagnostic = shouldIncludeDiagnostic;
                _includeCompilerDiagnostics = includeCompilerDiagnostics;
                _range = range;
                _includeSuppressedDiagnostics = includeSuppressedDiagnostics;
                _priorityProvider = priorityProvider;
                _isExplicit = isExplicit;
                _logPerformanceInfo = logPerformanceInfo;
                _incrementalAnalysis = incrementalAnalysis;
                _diagnosticKind = diagnosticKind;
            }
 
            public async Task GetAsync(ArrayBuilder<DiagnosticData> list, CancellationToken cancellationToken)
            {
                try
                {
                    // Try to get cached diagnostics, and also compute non-cached state sets that need diagnostic computation.
                    using var _1 = ArrayBuilder<AnalyzerWithState>.GetInstance(out var syntaxAnalyzers);
 
                    // If we are performing incremental member edit analysis to compute diagnostics incrementally,
                    // we divide the analyzers into those that support span-based incremental analysis and
                    // those that do not support incremental analysis and must be executed for the entire document.
                    // Otherwise, if we are not performing incremental analysis, all semantic analyzers are added
                    // to the span-based analyzer set as we want to compute diagnostics only for the given span.
                    using var _2 = ArrayBuilder<AnalyzerWithState>.GetInstance(out var semanticSpanBasedAnalyzers);
                    using var _3 = ArrayBuilder<AnalyzerWithState>.GetInstance(out var semanticDocumentBasedAnalyzers);
 
                    using var _4 = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"Pri{_priorityProvider.Priority.GetPriorityInt()}");
 
                    foreach (var stateSet in _stateSets)
                    {
                        var analyzer = stateSet.Analyzer;
                        if (!ShouldIncludeAnalyzer(analyzer, _shouldIncludeDiagnostic, _priorityProvider, _owner))
                            continue;
 
                        bool includeSyntax = true, includeSemantic = true;
                        if (_diagnosticKind != DiagnosticKind.All)
                        {
                            var isCompilerAnalyzer = analyzer.IsCompilerAnalyzer();
                            includeSyntax = isCompilerAnalyzer
                                ? _diagnosticKind == DiagnosticKind.CompilerSyntax
                                : _diagnosticKind == DiagnosticKind.AnalyzerSyntax;
                            includeSemantic = isCompilerAnalyzer
                                ? _diagnosticKind == DiagnosticKind.CompilerSemantic
                                : _diagnosticKind == DiagnosticKind.AnalyzerSemantic;
                        }
 
                        includeSyntax = includeSyntax && analyzer.SupportAnalysisKind(AnalysisKind.Syntax);
                        includeSemantic = includeSemantic && analyzer.SupportAnalysisKind(AnalysisKind.Semantic) && _document is Document;
 
                        if (includeSyntax || includeSemantic)
                        {
                            var state = stateSet.GetOrCreateActiveFileState(_document.Id);
 
                            if (includeSyntax)
                            {
                                var existingData = state.GetAnalysisData(AnalysisKind.Syntax);
                                if (!await TryAddCachedDocumentDiagnosticsAsync(stateSet.Analyzer, AnalysisKind.Syntax, existingData, list, cancellationToken).ConfigureAwait(false))
                                    syntaxAnalyzers.Add(new AnalyzerWithState(stateSet.Analyzer, stateSet.IsHostAnalyzer, state, existingData));
                            }
 
                            if (includeSemantic)
                            {
                                var existingData = state.GetAnalysisData(AnalysisKind.Semantic);
                                if (!await TryAddCachedDocumentDiagnosticsAsync(stateSet.Analyzer, AnalysisKind.Semantic, existingData, list, cancellationToken).ConfigureAwait(false))
                                {
                                    var stateSets = GetSemanticAnalysisSelectedStates(
                                        stateSet.Analyzer, _incrementalAnalysis,
                                        semanticSpanBasedAnalyzers, semanticDocumentBasedAnalyzers);
 
                                    stateSets.Add(new AnalyzerWithState(stateSet.Analyzer, stateSet.IsHostAnalyzer, state, existingData));
                                }
                            }
                        }
                    }
 
                    // Compute diagnostics for non-cached state sets.
                    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;
                }
                catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
                {
                    throw ExceptionUtilities.Unreachable();
                }
 
                // Local functions
                static bool ShouldIncludeAnalyzer(
                    DiagnosticAnalyzer analyzer,
                    Func<string, bool>? shouldIncludeDiagnostic,
                    ICodeActionRequestPriorityProvider priorityProvider,
                    DiagnosticIncrementalAnalyzer owner)
                {
                    // Skip executing analyzer if its priority does not match the request priority.
                    if (!priorityProvider.MatchesPriority(analyzer))
                        return false;
 
                    // Special case DocumentDiagnosticAnalyzer to never skip these document analyzers
                    // based on 'shouldIncludeDiagnostic' predicate. More specifically, TS has special document
                    // analyzer which report 0 supported diagnostics, but we always want to execute it.
                    if (analyzer is DocumentDiagnosticAnalyzer)
                        return true;
 
                    // Special case GeneratorDiagnosticsPlaceholderAnalyzer to never skip it based on
                    // 'shouldIncludeDiagnostic' predicate. More specifically, this is a placeholder analyzer
                    // for threading through all source generator reported diagnostics, but this special analyzer
                    // reports 0 supported diagnostics, and we always want to execute it.
                    if (analyzer is GeneratorDiagnosticsPlaceholderAnalyzer)
                        return true;
 
                    // Skip analyzer if none of its reported diagnostics should be included.
                    if (shouldIncludeDiagnostic != null &&
                        !owner.DiagnosticAnalyzerInfoCache.GetDiagnosticDescriptors(analyzer).Any(static (a, shouldIncludeDiagnostic) => shouldIncludeDiagnostic(a.Id), shouldIncludeDiagnostic))
                    {
                        return false;
                    }
 
                    return true;
                }
 
                static ArrayBuilder<AnalyzerWithState> GetSemanticAnalysisSelectedStates(
                    DiagnosticAnalyzer analyzer,
                    bool incrementalAnalysis,
                    ArrayBuilder<AnalyzerWithState> semanticSpanBasedAnalyzers,
                    ArrayBuilder<AnalyzerWithState> semanticDocumentBasedAnalyzers)
                {
                    if (!incrementalAnalysis)
                    {
                        // For non-incremental analysis, we always attempt to compute all
                        // analyzer diagnostics for the requested span.
                        return semanticSpanBasedAnalyzers;
                    }
                    else
                    {
                        // We can perform incremental analysis only for analyzers that support
                        // span-based semantic diagnostic analysis.
                        return analyzer.SupportsSpanBasedSemanticDiagnosticAnalysis()
                            ? semanticSpanBasedAnalyzers
                            : semanticDocumentBasedAnalyzers;
                    }
                }
            }
 
            /// <summary>
            /// Returns <see langword="true"/> if we were able to add the cached diagnostics and we do not need to compute them fresh.
            /// </summary>
            private async Task<bool> TryAddCachedDocumentDiagnosticsAsync(
                DiagnosticAnalyzer analyzer,
                AnalysisKind kind,
                DocumentAnalysisData existingData,
                ArrayBuilder<DiagnosticData> list,
                CancellationToken cancellationToken)
            {
                Debug.Assert(analyzer.SupportAnalysisKind(kind));
                Debug.Assert(_priorityProvider.MatchesPriority(analyzer));
 
                // see whether we can use existing info
                var version = await GetDiagnosticVersionAsync(_document.Project, cancellationToken).ConfigureAwait(false);
                if (existingData.Version == version)
                {
                    foreach (var item in existingData.Items)
                    {
                        if (ShouldInclude(item))
                            list.Add(item);
                    }
 
                    return true;
                }
 
                return false;
            }
 
            private async Task ComputeDocumentDiagnosticsAsync(
                ImmutableArray<AnalyzerWithState> analyzersWithState,
                AnalysisKind kind,
                TextSpan? span,
                ArrayBuilder<DiagnosticData> builder,
                bool incrementalAnalysis,
                CancellationToken cancellationToken)
            {
                Debug.Assert(!incrementalAnalysis || kind == AnalysisKind.Semantic);
                Debug.Assert(!incrementalAnalysis || analyzersWithState.All(analyzerWithState => analyzerWithState.Analyzer.SupportsSpanBasedSemanticDiagnosticAnalysis()));
 
                using var _ = ArrayBuilder<AnalyzerWithState>.GetInstance(analyzersWithState.Length, out var filteredAnalyzersWithStateBuilder);
                foreach (var analyzerWithState in analyzersWithState)
                {
                    Debug.Assert(_priorityProvider.MatchesPriority(analyzerWithState.Analyzer));
 
                    // Check if this is an expensive analyzer that needs to be de-prioritized to a lower priority bucket.
                    // If so, we skip this analyzer from execution in the current priority bucket.
                    // We will subsequently execute this analyzer in the lower priority bucket.
                    if (await TryDeprioritizeAnalyzerAsync(analyzerWithState.Analyzer, analyzerWithState.ExistingData).ConfigureAwait(false))
                    {
                        continue;
                    }
 
                    filteredAnalyzersWithStateBuilder.Add(analyzerWithState);
                }
 
                if (filteredAnalyzersWithStateBuilder.Count == 0)
                    return;
 
                analyzersWithState = filteredAnalyzersWithStateBuilder.ToImmutable();
 
                var projectAnalyzers = analyzersWithState.SelectAsArray(stateSet => !stateSet.IsHostAnalyzer, stateSet => stateSet.Analyzer);
                var hostAnalyzers = analyzersWithState.SelectAsArray(stateSet => stateSet.IsHostAnalyzer, stateSet => stateSet.Analyzer);
                var analysisScope = new DocumentAnalysisScope(_document, span, projectAnalyzers, hostAnalyzers, kind);
                var executor = new DocumentAnalysisExecutor(analysisScope, _compilationWithAnalyzers, _owner._diagnosticAnalyzerRunner, _isExplicit, _logPerformanceInfo);
                var version = await GetDiagnosticVersionAsync(_document.Project, cancellationToken).ConfigureAwait(false);
 
                ImmutableDictionary<DiagnosticAnalyzer, ImmutableArray<DiagnosticData>> diagnosticsMap;
                if (incrementalAnalysis)
                {
                    using var _2 = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"Pri{_priorityProvider.Priority.GetPriorityInt()}.Incremental");
 
                    diagnosticsMap = await _owner._incrementalMemberEditAnalyzer.ComputeDiagnosticsAsync(
                        executor,
                        analyzersWithState,
                        version,
                        ComputeDocumentDiagnosticsForAnalyzerCoreAsync,
                        ComputeDocumentDiagnosticsCoreAsync,
                        cancellationToken).ConfigureAwait(false);
                }
                else
                {
                    using var _2 = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"Pri{_priorityProvider.Priority.GetPriorityInt()}.Document");
 
                    diagnosticsMap = await ComputeDocumentDiagnosticsCoreAsync(executor, cancellationToken).ConfigureAwait(false);
                }
 
                foreach (var analyzerWithState in analyzersWithState)
                {
                    var diagnostics = diagnosticsMap[analyzerWithState.Analyzer];
                    builder.AddRange(diagnostics.Where(ShouldInclude));
                }
 
                if (incrementalAnalysis)
                    _owner._incrementalMemberEditAnalyzer.UpdateDocumentWithCachedDiagnostics((Document)_document);
 
                async Task<bool> TryDeprioritizeAnalyzerAsync(DiagnosticAnalyzer analyzer, DocumentAnalysisData existingData)
                {
                    // PERF: In order to improve lightbulb performance, we perform de-prioritization optimization for certain analyzers
                    // that moves the analyzer to a lower priority bucket. However, to ensure that de-prioritization happens for very rare cases,
                    // we only perform this optimizations when following conditions are met:
                    //  1. We are performing semantic span-based analysis.
                    //  2. We are processing 'CodeActionRequestPriority.Normal' priority request.
                    //  3. Analyzer registers certain actions that are known to lead to high performance impact due to its broad analysis scope,
                    //     such as SymbolStart/End actions and SemanticModel actions.
                    //  4. Analyzer did not report a diagnostic on the same line in prior document snapshot.
 
                    // Conditions 1. and 2.
                    if (kind != AnalysisKind.Semantic ||
                        !span.HasValue ||
                        _priorityProvider.Priority != CodeActionRequestPriority.Default)
                    {
                        return false;
                    }
 
                    Debug.Assert(span.Value.Length < _text.Length);
 
                    // Condition 3.
                    // Check if this is a candidate analyzer that can be de-prioritized into a lower priority bucket based on registered actions.
                    if (!await IsCandidateForDeprioritizationBasedOnRegisteredActionsAsync(analyzer).ConfigureAwait(false))
                    {
                        return false;
                    }
 
                    // Condition 4.
                    // We do not want to de-prioritize this analyzer if it reported a diagnostic on a prior document snapshot,
                    // such that diagnostic's start/end lines intersect the current analysis span's start/end lines.
                    // If an analyzer reported such a diagnostic, it is highly likely that the user intends to invoke the code fix
                    // for this diagnostic. Additionally, it is also highly likely that this analyzer will report a diagnostic
                    // on the current snapshot. So, we deem this as an important analyzer that should not be de-prioritized here.
                    // Note that we only perform this analysis if the prior document, whose existingData is cached, had same number
                    // of source lines as the current document snapshot. Otherwise, the start/end lines comparison across
                    // snapshots is not meaningful.
                    if (existingData.LineCount == _text.Lines.Count &&
                        !existingData.Items.IsEmpty)
                    {
                        _text.GetLinesAndOffsets(span.Value, out var startLineNumber, out var _, out var endLineNumber, out var _);
 
                        foreach (var diagnostic in existingData.Items)
                        {
                            if (diagnostic.DataLocation.UnmappedFileSpan.StartLinePosition.Line <= endLineNumber &&
                                diagnostic.DataLocation.UnmappedFileSpan.EndLinePosition.Line >= startLineNumber)
                            {
                                return false;
                            }
                        }
                    }
 
                    // 'LightbulbSkipExecutingDeprioritizedAnalyzers' option determines if we want to execute this analyzer
                    // in low priority bucket or skip it completely. If the option is not set, track the de-prioritized
                    // analyzer to be executed in low priority bucket.
                    // Note that 'AddDeprioritizedAnalyzerWithLowPriority' call below mutates the state in the provider to
                    // track this analyzer. This ensures that when the owner of this provider calls us back to execute
                    // the low priority bucket, we can still get back to this analyzer and execute it that time.
                    if (!_owner.GlobalOptions.GetOption(DiagnosticOptionsStorage.LightbulbSkipExecutingDeprioritizedAnalyzers))
                        _priorityProvider.AddDeprioritizedAnalyzerWithLowPriority(analyzer);
 
                    return true;
                }
 
                // Returns true if this is an analyzer that is a candidate to be de-prioritized to
                // 'CodeActionRequestPriority.Low' priority for improvement in analyzer
                // execution performance for priority buckets above 'Low' priority.
                // Based on performance measurements, currently only analyzers which register SymbolStart/End actions
                // or SemanticModel actions are considered candidates to be de-prioritized. However, these semantics
                // could be changed in future based on performance measurements.
                async Task<bool> IsCandidateForDeprioritizationBasedOnRegisteredActionsAsync(DiagnosticAnalyzer analyzer)
                {
                    // We deprioritize SymbolStart/End and SemanticModel analyzers from 'Normal' to 'Low' priority bucket,
                    // as these are computationally more expensive.
                    // Note that we never de-prioritize compiler analyzer, even though it registers a SemanticModel action.
                    if (_compilationWithAnalyzers == null ||
                        analyzer.IsWorkspaceDiagnosticAnalyzer() ||
                        analyzer.IsCompilerAnalyzer())
                    {
                        return false;
                    }
 
                    var telemetryInfo = await _compilationWithAnalyzers.GetAnalyzerTelemetryInfoAsync(analyzer, cancellationToken).ConfigureAwait(false);
                    if (telemetryInfo == null)
                        return false;
 
                    return telemetryInfo.SymbolStartActionsCount > 0 || telemetryInfo.SemanticModelActionsCount > 0;
                }
            }
 
            private async Task<ImmutableDictionary<DiagnosticAnalyzer, ImmutableArray<DiagnosticData>>> ComputeDocumentDiagnosticsCoreAsync(
                DocumentAnalysisExecutor executor,
                CancellationToken cancellationToken)
            {
                using var _ = PooledDictionary<DiagnosticAnalyzer, ImmutableArray<DiagnosticData>>.GetInstance(out var builder);
                foreach (var analyzer in executor.AnalysisScope.ProjectAnalyzers.ConcatFast(executor.AnalysisScope.HostAnalyzers))
                {
                    var diagnostics = await ComputeDocumentDiagnosticsForAnalyzerCoreAsync(analyzer, executor, cancellationToken).ConfigureAwait(false);
                    builder.Add(analyzer, diagnostics);
                }
 
                return builder.ToImmutableDictionary();
            }
 
            private async Task<ImmutableArray<DiagnosticData>> ComputeDocumentDiagnosticsForAnalyzerCoreAsync(
                DiagnosticAnalyzer analyzer,
                DocumentAnalysisExecutor executor,
                CancellationToken cancellationToken)
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                var diagnostics = await executor.ComputeDiagnosticsAsync(analyzer, cancellationToken).ConfigureAwait(false);
                return diagnostics?.ToImmutableArrayOrEmpty() ?? [];
            }
 
            private bool ShouldInclude(DiagnosticData diagnostic)
            {
                return diagnostic.DocumentId == _document.Id &&
                    (_range == null || _range.Value.IntersectsWith(diagnostic.DataLocation.UnmappedFileSpan.GetClampedTextSpan(_text)))
                    && (_includeSuppressedDiagnostics || !diagnostic.IsSuppressed)
                    && (_includeCompilerDiagnostics || !diagnostic.CustomTags.Any(static t => t is WellKnownDiagnosticTags.Compiler))
                    && (_shouldIncludeDiagnostic == null || _shouldIncludeDiagnostic(diagnostic.Id));
            }
        }
 
        private sealed record class AnalyzerWithState(DiagnosticAnalyzer Analyzer, bool IsHostAnalyzer, ActiveFileState State, DocumentAnalysisData ExistingData);
    }
}