File: CodeFixesAndRefactorings\CodeActionRequestPriorityProvider.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.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
 
namespace Microsoft.CodeAnalysis.CodeActions;
 
internal interface ICodeActionRequestPriorityProvider
{
    /// <summary>
    /// <see langword="null"/> represents no specified priority.  i.e. any priority should match this.
    /// </summary>
    CodeActionRequestPriority? Priority { get; }
 
    /// <summary>
    /// Tracks the given <paramref name="analyzer"/> as a de-prioritized analyzer that should be moved to
    /// <see cref="CodeActionRequestPriority.Low"/> bucket.
    /// </summary>
    void AddDeprioritizedAnalyzerWithLowPriority(DiagnosticAnalyzer analyzer);
 
    bool IsDeprioritizedAnalyzerWithLowPriority(DiagnosticAnalyzer analyzer);
 
    /// <summary>
    /// Indicates whether any deprioritized analyzer supports one of the passed in diagnostic ids.
    /// </summary>
    bool HasDeprioritizedAnalyzerSupportingDiagnosticId(ImmutableArray<string> diagnosticIds);
}
 
internal static class ICodeActionRequestPriorityProviderExtensions
{
    /// <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>
    public static bool MatchesPriority(this ICodeActionRequestPriorityProvider provider, DiagnosticAnalyzer analyzer)
    {
        var priority = provider.Priority;
 
        // 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 &&
            provider.IsDeprioritizedAnalyzerWithLowPriority(analyzer))
        {
            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;
    }
 
    /// <summary>
    /// Returns true if the given <paramref name="codeFixProvider"/> should be considered a candidate when computing
    /// fixes for the given <see cref="ICodeActionRequestPriorityProvider.Priority"/>.
    /// </summary>
    public static bool MatchesPriority(this ICodeActionRequestPriorityProvider provider, CodeFixProvider codeFixProvider)
    {
        if (provider.Priority == null)
        {
            // We are computing fixes for all priorities
            return true;
        }
 
        if (provider.Priority == codeFixProvider.RequestPriority)
        {
            return true;
        }
 
        if (provider.Priority == CodeActionRequestPriority.Low
            && provider.HasDeprioritizedAnalyzerSupportingDiagnosticId(codeFixProvider.FixableDiagnosticIds)
            && codeFixProvider.RequestPriority > CodeActionRequestPriority.Low)
        {
            // 'Low' priority can be used for two types of code fixers:
            //  1. Those which explicitly set their 'RequestPriority' to 'Low' and
            //  2. Those which can fix diagnostics for expensive analyzers which were de-prioritized
            //     to 'Low' priority bucket to improve lightbulb population performance.
            // The first case is handled by the earlier check against matching priorities. For the second
            // case, we accept fixers with any RequestPriority, as long as they can fix a diagnostic from
            // an analyzer that was executed in the 'Low' bucket.
            return true;
        }
 
        return false;
    }
}
 
internal sealed class DefaultCodeActionRequestPriorityProvider(CodeActionRequestPriority? priority = null) : ICodeActionRequestPriorityProvider
{
    private readonly object _gate = new();
    private HashSet<DiagnosticAnalyzer>? _lowPriorityAnalyzers;
    private HashSet<string>? _lowPriorityAnalyzerSupportedDiagnosticIds;
 
    public CodeActionRequestPriority? Priority { get; } = priority;
 
    public void AddDeprioritizedAnalyzerWithLowPriority(DiagnosticAnalyzer analyzer)
    {
        lock (_gate)
        {
            _lowPriorityAnalyzers ??= [];
            _lowPriorityAnalyzerSupportedDiagnosticIds ??= [];
 
            _lowPriorityAnalyzers.Add(analyzer);
 
            foreach (var supportedDiagnostic in analyzer.SupportedDiagnostics)
                _lowPriorityAnalyzerSupportedDiagnosticIds.Add(supportedDiagnostic.Id);
        }
    }
 
    public bool HasDeprioritizedAnalyzerSupportingDiagnosticId(ImmutableArray<string> diagnosticIds)
    {
        lock (_gate)
        {
            if (_lowPriorityAnalyzerSupportedDiagnosticIds == null)
                return false;
 
            foreach (var diagnosticId in diagnosticIds)
            {
                if (_lowPriorityAnalyzerSupportedDiagnosticIds.Contains(diagnosticId))
                    return true;
            }
 
            return false;
        }
    }
 
    public bool IsDeprioritizedAnalyzerWithLowPriority(DiagnosticAnalyzer analyzer)
    {
        lock (_gate)
        {
            return _lowPriorityAnalyzers != null && _lowPriorityAnalyzers.Contains(analyzer);
        }
    }
}