File: Diagnostics\Service\DiagnosticAnalyzerService_GetDiagnosticsForSpan.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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;
 
internal sealed partial class DiagnosticAnalyzerService
{
    private static async Task<ImmutableDictionary<DiagnosticAnalyzer, ImmutableArray<DiagnosticData>>> ComputeDocumentDiagnosticsCoreInProcessAsync(
        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 executor.ComputeDiagnosticsInProcessAsync(analyzer, cancellationToken).ConfigureAwait(false);
            builder.Add(analyzer, diagnostics);
        }
 
        return builder.ToImmutableDictionary();
    }
 
    public async Task<ImmutableArray<DiagnosticData>> GetDiagnosticsForSpanInProcessAsync(
        TextDocument document,
        TextSpan? range,
        DiagnosticIdFilter diagnosticIdFilter,
        CodeActionRequestPriority? priority,
        DiagnosticKind diagnosticKind,
        CancellationToken cancellationToken)
    {
        var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
 
        var project = document.Project;
        var unfilteredAnalyzers = GetProjectAnalyzers_OnlyCallInProcess(project);
        var analyzers = unfilteredAnalyzers
            .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.
        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;
 
        // 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 is null && document is Document { SupportsSyntaxTree: true };
 
        var (syntaxAnalyzers, semanticSpanAnalyzers, semanticDocumentAnalyzers) = await GetAllAnalyzersAsync().ConfigureAwait(false);
        syntaxAnalyzers = await FilterAnalyzersAsync(syntaxAnalyzers, AnalysisKind.Syntax, range).ConfigureAwait(false);
        semanticSpanAnalyzers = await FilterAnalyzersAsync(semanticSpanAnalyzers, AnalysisKind.Semantic, range).ConfigureAwait(false);
        semanticDocumentAnalyzers = await FilterAnalyzersAsync(semanticDocumentAnalyzers, AnalysisKind.Semantic, span: null).ConfigureAwait(false);
 
        var allDiagnostics = await this.ComputeDiagnosticsInProcessAsync(
            document, range, analyzers, syntaxAnalyzers, semanticSpanAnalyzers, semanticDocumentAnalyzers,
            incrementalAnalysis, logPerformanceInfo,
            cancellationToken).ConfigureAwait(false);
        return allDiagnostics.WhereAsArray(ShouldInclude);
 
        async ValueTask<(
            ImmutableArray<DiagnosticAnalyzer> syntaxAnalyzers,
            ImmutableArray<DiagnosticAnalyzer> semanticSpanAnalyzers,
            ImmutableArray<DiagnosticAnalyzer> semanticDocumentAnalyzers)> GetAllAnalyzersAsync()
        {
            try
            {
                using var _1 = ArrayBuilder<DiagnosticAnalyzer>.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<DiagnosticAnalyzer>.GetInstance(out var semanticSpanBasedAnalyzers);
                using var _3 = ArrayBuilder<DiagnosticAnalyzer>.GetInstance(out var semanticDocumentBasedAnalyzers);
 
                using var _4 = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.RequestDiagnostics_Summary, $"Pri{priority.GetPriorityInt()}");
 
                foreach (var analyzer in analyzers)
                {
                    if (!await ShouldIncludeAnalyzerAsync(analyzer).ConfigureAwait(false))
                        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)
                    {
                        if (includeSyntax)
                        {
                            syntaxAnalyzers.Add(analyzer);
                        }
 
                        if (includeSemantic)
                        {
                            if (!incrementalAnalysis)
                            {
                                // For non-incremental analysis, we always attempt to compute all
                                // analyzer diagnostics for the requested span.
                                semanticSpanBasedAnalyzers.Add(analyzer);
                            }
                            else if (analyzer.SupportsSpanBasedSemanticDiagnosticAnalysis())
                            {
                                // We can perform incremental analysis only for analyzers that support
                                // span-based semantic diagnostic analysis.
                                semanticSpanBasedAnalyzers.Add(analyzer);
                            }
                            else
                            {
                                semanticDocumentBasedAnalyzers.Add(analyzer);
                            }
                        }
                    }
                }
 
                return (
                    syntaxAnalyzers.ToImmutableAndClear(),
                    semanticSpanBasedAnalyzers.ToImmutableAndClear(),
                    semanticDocumentBasedAnalyzers.ToImmutableAndClear());
            }
            catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
            {
                throw ExceptionUtilities.Unreachable();
            }
        }
 
        async ValueTask<bool> ShouldIncludeAnalyzerAsync(DiagnosticAnalyzer analyzer)
        {
            // Skip executing analyzer if its priority does not match the request priority.
            if (!await MatchesPriorityAsync(analyzer).ConfigureAwait(false))
                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.  This also applies to our special built in
            // analyzers 'FileContentLoadAnalyzer' and 'GeneratorDiagnosticsPlaceholderAnalyzer'.
            if (analyzer is DocumentDiagnosticAnalyzer)
                return true;
 
            // Skip analyzer if none of its reported diagnostics should be included.
            if (diagnosticIdFilter != DiagnosticIdFilter.All)
            {
                var descriptors = _analyzerInfoCache.GetDiagnosticDescriptors(analyzer);
                return diagnosticIdFilter.Allow(descriptors.Select(d => d.Id));
            }
 
            return true;
        }
 
        // <summary>
        // Returns true if the given <paramref name="analyzer"/> can report diagnostics that can have fixes from a code
        // fix provider with <see cref="CodeFixProvider.RequestPriority"/> matching <see
        // cref="ICodeActionRequestPriorityProvider.Priority"/>. This method is useful for performing a performance
        // optimization for lightbulb diagnostic computation, wherein we can reduce the set of analyzers to be executed
        // when computing fixes for a specific <see cref="ICodeActionRequestPriorityProvider.Priority"/>.
        // </summary>
        async Task<bool> MatchesPriorityAsync(DiagnosticAnalyzer analyzer)
        {
            // If caller isn't asking for prioritized result, then run all analyzers.
            if (priority is null)
                return true;
 
            // 'CodeActionRequestPriority.Lowest' is used for suppression/configuration fixes,
            // which requires all analyzer diagnostics.
            if (priority == CodeActionRequestPriority.Lowest)
                return true;
 
            // The compiler analyzer always counts for any priority.  It's diagnostics may be fixed
            // by high pri or normal pri fixers.
            if (analyzer.IsCompilerAnalyzer())
                return true;
 
            // Check if we are computing diagnostics for 'CodeActionRequestPriority.Low' and
            // this analyzer was de-prioritized to low priority bucket.
            if (priority == CodeActionRequestPriority.Low &&
                await this.IsDeprioritizedAnalyzerAsync(project, analyzer, cancellationToken).ConfigureAwait(false))
            {
                return true;
            }
 
            // Now compute this analyzer's priority and compare it with the provider's request 'Priority'.
            // Our internal 'IBuiltInAnalyzer' can specify custom request priority, while all
            // the third-party analyzers are assigned 'Medium' priority.
            var analyzerPriority = analyzer is IBuiltInAnalyzer { IsHighPriority: true }
                ? CodeActionRequestPriority.High
                : CodeActionRequestPriority.Default;
 
            return priority == analyzerPriority;
        }
 
        async Task<ImmutableArray<DiagnosticAnalyzer>> FilterAnalyzersAsync(
            ImmutableArray<DiagnosticAnalyzer> analyzers,
            AnalysisKind kind,
            TextSpan? span)
        {
            using var _1 = ArrayBuilder<DiagnosticAnalyzer>.GetInstance(analyzers.Length, out var filteredAnalyzers);
 
            foreach (var analyzer in analyzers)
            {
                // 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 ShouldDeprioritizeAnalyzerAsync(analyzer, kind, span).ConfigureAwait(false))
                    continue;
 
                filteredAnalyzers.Add(analyzer);
            }
 
            return filteredAnalyzers.ToImmutableAndClear();
        }
 
        async ValueTask<bool> ShouldDeprioritizeAnalyzerAsync(
            DiagnosticAnalyzer analyzer, AnalysisKind kind, TextSpan? span)
        {
            // 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.
 
            // Conditions 1. and 2.
            if (kind != AnalysisKind.Semantic ||
                !span.HasValue ||
                priority != CodeActionRequestPriority.Default)
            {
                return false;
            }
 
            // Condition 3.
            // Check if this is a candidate analyzer that can be de-prioritized into a lower priority bucket based on registered actions.
            return await this.IsDeprioritizedAnalyzerAsync(project, analyzer, cancellationToken).ConfigureAwait(false);
        }
 
        bool ShouldInclude(DiagnosticData diagnostic)
        {
            if (diagnostic.DocumentId != document.Id)
                return false;
 
            if (range != null && !range.Value.IntersectsWith(diagnostic.DataLocation.UnmappedFileSpan.GetClampedTextSpan(text)))
                return false;
 
            return diagnosticIdFilter.Allow(diagnostic.Id);
        }
    }
 
    private async Task<ImmutableArray<DiagnosticData>> ComputeDiagnosticsInProcessAsync(
        TextDocument document,
        TextSpan? range,
        ImmutableArray<DiagnosticAnalyzer> allAnalyzers,
        ImmutableArray<DiagnosticAnalyzer> syntaxAnalyzers,
        ImmutableArray<DiagnosticAnalyzer> semanticSpanAnalyzers,
        ImmutableArray<DiagnosticAnalyzer> 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 = GetOrCreateHostAnalyzerInfo_OnlyCallInProcess(project);
        var compilationWithAnalyzers = await GetOrCreateCompilationWithAnalyzers_OnlyCallInProcessAsync(
            document.Project, allAnalyzers, hostAnalyzerInfo, this.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false);
 
        using var _ = ArrayBuilder<DiagnosticData>.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<DiagnosticAnalyzer> 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(this, analysisScope, compilationWithAnalyzers, logPerformanceInfo);
            var version = await GetDiagnosticVersionAsync(document.Project, cancellationToken).ConfigureAwait(false);
 
            var computeTask = incrementalAnalysis
                ? _incrementalMemberEditAnalyzer.ComputeDiagnosticsInProcessAsync(executor, analyzers, version, cancellationToken)
                : ComputeDocumentDiagnosticsCoreInProcessAsync(executor, cancellationToken);
            var diagnosticsMap = await computeTask.ConfigureAwait(false);
 
            if (incrementalAnalysis)
                _incrementalMemberEditAnalyzer.UpdateDocumentWithCachedDiagnostics((Document)document);
 
            list.AddRange(diagnosticsMap.SelectMany(kvp => kvp.Value));
        }
    }
}