File: CodeFixes\Service\CodeFixService.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.Composition;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes.Suppression;
using Microsoft.CodeAnalysis.CodeFixesAndRefactorings;
using Microsoft.CodeAnalysis.Copilot;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ErrorLogger;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Telemetry;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Threading;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CodeFixes;
 
using DiagnosticId = String;
using LanguageKind = String;
 
[Export(typeof(ICodeFixService)), Shared]
internal sealed partial class CodeFixService : ICodeFixService
{
    private readonly IDiagnosticAnalyzerService _diagnosticService;
    private readonly ImmutableArray<Lazy<CodeFixProvider, CodeChangeProviderMetadata>> _fixers;
    private readonly ImmutableDictionary<string, ImmutableArray<Lazy<CodeFixProvider, CodeChangeProviderMetadata>>> _fixersPerLanguageMap;
 
    private readonly ConditionalWeakTable<IReadOnlyList<AnalyzerReference>, ImmutableDictionary<DiagnosticId, ImmutableArray<CodeFixProvider>>> _projectFixersMap = new();
 
    // Shared by project fixers and workspace fixers.
    private readonly ImmutableDictionary<LanguageKind, Lazy<ImmutableArray<IConfigurationFixProvider>>> _configurationProvidersMap;
    private readonly ImmutableArray<Lazy<IErrorLoggerService>> _errorLoggers;
 
    private ImmutableDictionary<LanguageKind, Lazy<ImmutableDictionary<DiagnosticId, ImmutableArray<CodeFixProvider>>>>? _lazyWorkspaceFixersMap;
    private ImmutableDictionary<LanguageKind, Lazy<ImmutableDictionary<CodeFixProvider, int>>>? _lazyFixerPriorityMap;
 
    private ImmutableDictionary<CodeFixProvider, ImmutableArray<DiagnosticId>> _fixerToFixableIdsMap = ImmutableDictionary<CodeFixProvider, ImmutableArray<DiagnosticId>>.Empty;
    private ImmutableDictionary<object, FixAllProviderInfo?> _fixAllProviderMap = ImmutableDictionary<object, FixAllProviderInfo?>.Empty;
    private ImmutableDictionary<CodeFixProvider, CodeChangeProviderMetadata?> _fixerToMetadataMap = ImmutableDictionary<CodeFixProvider, CodeChangeProviderMetadata?>.Empty;
 
    [ImportingConstructor]
    [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
    public CodeFixService(
        IDiagnosticAnalyzerService diagnosticAnalyzerService,
        [ImportMany] IEnumerable<Lazy<IErrorLoggerService>> loggers,
        [ImportMany] IEnumerable<Lazy<CodeFixProvider, CodeChangeProviderMetadata>> fixers,
        [ImportMany] IEnumerable<Lazy<IConfigurationFixProvider, CodeChangeProviderMetadata>> configurationProviders)
    {
        _diagnosticService = diagnosticAnalyzerService;
        _errorLoggers = [.. loggers];
 
        _fixers = [.. fixers];
        _fixersPerLanguageMap = _fixers.ToPerLanguageMapWithMultipleLanguages();
 
        _configurationProvidersMap = GetConfigurationProvidersPerLanguageMap(configurationProviders);
    }
 
    private Func<string, bool>? GetShouldIncludeDiagnosticPredicate(
        TextDocument document,
        ICodeActionRequestPriorityProvider priorityProvider)
    {
        // For Normal or Low priority, we only need to execute analyzers which can report at least one fixable
        // diagnostic that can have a non-suppression/configuration fix.
        //
        // For CodeActionPriorityRequest.High, we only run compiler analyzer, which always has fixable diagnostics,
        // so we can return a null predicate here to include all diagnostics.
 
        if (!(priorityProvider.Priority is CodeActionRequestPriority.Default or CodeActionRequestPriority.Low))
            return null;
 
        var hasWorkspaceFixers = TryGetWorkspaceFixersMap(document, out var workspaceFixersMap);
        var projectFixersMap = GetProjectFixers(document);
 
        return id =>
        {
            if (hasWorkspaceFixers && workspaceFixersMap!.ContainsKey(id))
                return true;
 
            return projectFixersMap.ContainsKey(id);
        };
    }
 
    public async Task<CodeFixCollection?> GetMostSevereFixAsync(
        TextDocument document, TextSpan range, ICodeActionRequestPriorityProvider priorityProvider, CancellationToken cancellationToken)
    {
        using var _ = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.CodeFix_Summary, $"Pri{priorityProvider.Priority.GetPriorityInt()}.{nameof(GetMostSevereFixAsync)}");
 
        ImmutableArray<DiagnosticData> allDiagnostics;
 
        using (TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.CodeFix_Summary, $"Pri{priorityProvider.Priority.GetPriorityInt()}.{nameof(GetMostSevereFixAsync)}.{nameof(_diagnosticService.GetDiagnosticsForSpanAsync)}"))
        {
            allDiagnostics = await _diagnosticService.GetDiagnosticsForSpanAsync(
                document, range, GetShouldIncludeDiagnosticPredicate(document, priorityProvider),
                priorityProvider, DiagnosticKind.All, cancellationToken).ConfigureAwait(false);
 
            // NOTE(cyrusn): We do not include suppressed diagnostics here as they are effectively hidden from the
            // user in the editor.  As far as the user is concerned, there is no squiggle for it and no lightbulb
            // entries either.
            allDiagnostics = allDiagnostics.WhereAsArray(d => !d.IsSuppressed);
        }
 
        var copilotDiagnostics = await GetCopilotDiagnosticsAsync(document, range, priorityProvider.Priority, cancellationToken).ConfigureAwait(false);
        allDiagnostics = allDiagnostics.AddRange(copilotDiagnostics);
 
        var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        var spanToDiagnostics = ConvertToMap(text, allDiagnostics);
 
        using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        var linkedToken = linkedTokenSource.Token;
 
        var spanToErrorDiagnostics = new SortedDictionary<TextSpan, List<DiagnosticData>>();
        var spanToOtherDiagnostics = new SortedDictionary<TextSpan, List<DiagnosticData>>();
 
        foreach (var (span, diagnostics) in spanToDiagnostics)
        {
            foreach (var diagnostic in diagnostics)
            {
                var preferredMap = diagnostic.Severity == DiagnosticSeverity.Error
                    ? spanToErrorDiagnostics
                    : spanToOtherDiagnostics;
 
                preferredMap.MultiAdd(span, diagnostic);
            }
        }
 
        var errorFixTask = GetFirstFixAsync(spanToErrorDiagnostics, cancellationToken);
        var otherFixTask = GetFirstFixAsync(spanToOtherDiagnostics, linkedToken);
 
        // If the error diagnostics task happens to complete with a non-null result before
        // the other diagnostics task, we can cancel the other task.
        var collection = await errorFixTask.ConfigureAwait(false) ??
                         await otherFixTask.ConfigureAwait(false);
        linkedTokenSource.Cancel();
 
        return collection;
 
        async Task<CodeFixCollection?> GetFirstFixAsync(
            SortedDictionary<TextSpan, List<DiagnosticData>> spanToDiagnostics,
            CancellationToken cancellationToken)
        {
            // Ensure we yield here so the caller can continue on.
            await Task.Yield().ConfigureAwait(false);
 
            await foreach (var collection in StreamFixesAsync(
                document, spanToDiagnostics, fixAllForInSpan: false,
                priorityProvider, cancellationToken).ConfigureAwait(false))
            {
                // Stop at the result error we see.
                return collection;
            }
 
            return null;
        }
    }
 
    public async IAsyncEnumerable<CodeFixCollection> StreamFixesAsync(
        TextDocument document,
        TextSpan range,
        ICodeActionRequestPriorityProvider priorityProvider,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        using var _ = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.CodeFix_Summary, $"Pri{priorityProvider.Priority.GetPriorityInt()}");
 
        // We only need to compute suppression/configuration fixes when request priority is
        // 'CodeActionPriorityRequest.Lowest' or no priority was provided at all (so all providers should run).
        var includeSuppressionFixes = priorityProvider.Priority is null or CodeActionRequestPriority.Lowest;
 
        // REVIEW: this is the first and simplest design. basically, when ctrl+. is pressed, it asks diagnostic
        // service to give back current diagnostics for the given span, and it will use that to get fixes.
        // internally diagnostic service will either return cached information (if it is up-to-date) or
        // synchronously do the work at the spot.
        //
        // this design's weakness is that each side don't have enough information to narrow down works to do. it
        // will most likely always do more works than needed. sometimes way more than it is needed. (compilation)
 
        // We mark requests to GetDiagnosticsForSpanAsync as 'isExplicit = true' to indicate
        // user-invoked diagnostic requests, for example, user invoked Ctrl + Dot operation for lightbulb.
        ImmutableArray<DiagnosticData> diagnostics;
 
        using (TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.CodeFix_Summary, $"Pri{priorityProvider.Priority.GetPriorityInt()}.{nameof(_diagnosticService.GetDiagnosticsForSpanAsync)}"))
        {
            diagnostics = await _diagnosticService.GetDiagnosticsForSpanAsync(
                document, range, GetShouldIncludeDiagnosticPredicate(document, priorityProvider),
                priorityProvider, DiagnosticKind.All, cancellationToken).ConfigureAwait(false);
            if (!includeSuppressionFixes)
                diagnostics = diagnostics.WhereAsArray(d => !d.IsSuppressed);
        }
 
        var copilotDiagnostics = await GetCopilotDiagnosticsAsync(document, range, priorityProvider.Priority, cancellationToken).ConfigureAwait(false);
        diagnostics = diagnostics.AddRange(copilotDiagnostics);
 
        if (diagnostics.IsEmpty)
            yield break;
 
        if (!diagnostics.IsEmpty)
        {
            var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
            var spanToDiagnostics = ConvertToMap(text, diagnostics);
 
            // 'CodeActionRequestPriority.Lowest' is used when the client only wants suppression/configuration fixes.
            if (priorityProvider.Priority != CodeActionRequestPriority.Lowest)
            {
                await foreach (var collection in StreamFixesAsync(
                    document, spanToDiagnostics, fixAllForInSpan: false,
                    priorityProvider, cancellationToken).ConfigureAwait(false))
                {
                    yield return collection;
                }
            }
        }
 
        // TODO (https://github.com/dotnet/roslyn/issues/4932): Don't restrict CodeFixes in Interactive
        if (document.Project.Solution.WorkspaceKind != WorkspaceKind.Interactive && includeSuppressionFixes)
        {
            // For build-only diagnostics, we support configuration/suppression fixes.
            var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
            var spanToDiagnostics = ConvertToMap(text, diagnostics);
 
            // Ensure that we do not register duplicate configuration fixes.
            using var _2 = PooledHashSet<string>.GetInstance(out var registeredConfigurationFixTitles);
            foreach (var (span, diagnosticList) in spanToDiagnostics)
            {
                await foreach (var codeFixCollection in StreamConfigurationFixesAsync(
                    document, span, diagnosticList, registeredConfigurationFixTitles, cancellationToken).ConfigureAwait(false))
                {
                    yield return codeFixCollection;
                }
            }
        }
    }
 
    private static async Task<ImmutableArray<DiagnosticData>> GetCopilotDiagnosticsAsync(
        TextDocument document,
        TextSpan range,
        CodeActionRequestPriority? priority,
        CancellationToken cancellationToken)
    {
        if (priority is null or CodeActionRequestPriority.Low)
            return await document.GetCachedCopilotDiagnosticsAsync(range, cancellationToken).ConfigureAwait(false);
 
        return [];
    }
 
    private static SortedDictionary<TextSpan, List<DiagnosticData>> ConvertToMap(
        SourceText text, ImmutableArray<DiagnosticData> diagnostics)
    {
        // group diagnostics by their diagnostics span
        //
        // invariant: later code gathers & runs CodeFixProviders for diagnostics with one identical diagnostics span
        // (that gets set later as CodeFixCollection's TextSpan) order diagnostics by span.
        var spanToDiagnostics = new SortedDictionary<TextSpan, List<DiagnosticData>>();
        foreach (var diagnostic in diagnostics)
        {
            if (diagnostic.IsSuppressed)
                continue;
 
            // TODO: Is it correct to use UnmappedFileSpan here?
            spanToDiagnostics.MultiAdd(diagnostic.DataLocation.UnmappedFileSpan.GetClampedTextSpan(text), diagnostic);
        }
 
        // Order diagnostics by DiagnosticId so the fixes are in a deterministic order.
        foreach (var (_, diagnosticList) in spanToDiagnostics)
            diagnosticList.Sort(static (d1, d2) => DiagnosticId.CompareOrdinal(d1.Id, d2.Id));
 
        return spanToDiagnostics;
    }
 
    public Task<CodeFixCollection?> GetDocumentFixAllForIdInSpanAsync(
        TextDocument document, TextSpan range, string diagnosticId, CancellationToken cancellationToken)
        => GetDocumentFixAllForIdInSpanAsync(document, range, diagnosticId, DiagnosticSeverity.Hidden, cancellationToken);
 
    public async Task<CodeFixCollection?> GetDocumentFixAllForIdInSpanAsync(
        TextDocument document, TextSpan range, string diagnosticId, DiagnosticSeverity minimumSeverity, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        using var _ = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.CodeFix_Summary, $"{nameof(GetDocumentFixAllForIdInSpanAsync)}");
        ImmutableArray<DiagnosticData> diagnostics;
 
        using (TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.CodeFix_Summary, $"{nameof(GetDocumentFixAllForIdInSpanAsync)}.{nameof(_diagnosticService.GetDiagnosticsForSpanAsync)}"))
        {
            diagnostics = await _diagnosticService.GetDiagnosticsForSpanAsync(
                document, range, diagnosticId, priorityProvider: new DefaultCodeActionRequestPriorityProvider(),
                DiagnosticKind.All, cancellationToken).ConfigureAwait(false);
 
            // NOTE(cyrusn): We do not include suppressed diagnostics here as they are effectively hidden from the
            // user in the editor.  As far as the user is concerned, there is no squiggle for it and no lightbulb
            // entries either.
            diagnostics = diagnostics.WhereAsArray(d => !d.IsSuppressed);
        }
 
        diagnostics = diagnostics.WhereAsArray(d => d.Severity.IsMoreSevereThanOrEqualTo(minimumSeverity));
        if (!diagnostics.Any())
            return null;
 
        using var resultDisposer = ArrayBuilder<CodeFixCollection>.GetInstance(out var result);
        var spanToDiagnostics = new SortedDictionary<TextSpan, List<DiagnosticData>>
        {
            { range, diagnostics.ToList() },
        };
 
        await foreach (var collection in StreamFixesAsync(
            document, spanToDiagnostics, fixAllForInSpan: true, new DefaultCodeActionRequestPriorityProvider(),
            cancellationToken).ConfigureAwait(false))
        {
            if (collection.FixAllState is not null && collection.SupportedScopes.Contains(FixAllScope.Document))
            {
                // TODO: Just get the first fix for now until we have a way to config user's preferred fix
                // https://github.com/dotnet/roslyn/issues/27066
                return collection;
            }
        }
 
        return null;
    }
 
    public async Task<TDocument> ApplyCodeFixesForSpecificDiagnosticIdAsync<TDocument>(
        TDocument document,
        string diagnosticId,
        DiagnosticSeverity severity,
        IProgress<CodeAnalysisProgress> progressTracker,
        CancellationToken cancellationToken)
        where TDocument : TextDocument
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        var textSpan = new TextSpan(0, text.Length);
 
        var fixCollection = await GetDocumentFixAllForIdInSpanAsync(
            document, textSpan, diagnosticId, severity, cancellationToken).ConfigureAwait(false);
        if (fixCollection == null)
        {
            return document;
        }
 
        var fixAllService = document.Project.Solution.Services.GetRequiredService<IFixAllGetFixesService>();
 
        var solution = await fixAllService.GetFixAllChangedSolutionAsync(
            new FixAllContext(fixCollection.FixAllState!, progressTracker, cancellationToken)).ConfigureAwait(false);
        Contract.ThrowIfNull(solution);
 
        return (TDocument)(solution.GetTextDocument(document.Id) ?? throw new NotSupportedException(FeaturesResources.Removal_of_document_not_supported));
    }
 
    private bool TryGetWorkspaceFixersMap(TextDocument document, [NotNullWhen(true)] out ImmutableDictionary<DiagnosticId, ImmutableArray<CodeFixProvider>>? fixerMap)
    {
        if (_lazyWorkspaceFixersMap == null)
        {
            var workspaceFixersMap = GetFixerPerLanguageMap(document.Project.Solution.Services);
            Interlocked.CompareExchange(ref _lazyWorkspaceFixersMap, workspaceFixersMap, null);
        }
 
        if (!_lazyWorkspaceFixersMap.TryGetValue(document.Project.Language, out var lazyFixerMap))
        {
            fixerMap = ImmutableDictionary<DiagnosticId, ImmutableArray<CodeFixProvider>>.Empty;
            return false;
        }
 
        using var _ = PooledDictionary<DiagnosticId, ImmutableArray<CodeFixProvider>>.GetInstance(out var builder);
        foreach (var (id, fixers) in lazyFixerMap.Value)
        {
            var filteredFixers = ProjectCodeFixProvider.FilterExtensions(document, fixers, GetExtensionInfo);
            if (!filteredFixers.IsEmpty)
                builder.Add(id, filteredFixers);
        }
 
        fixerMap = builder.ToImmutableDictionary();
        return fixerMap.Count > 0;
    }
 
    private bool TryGetWorkspaceFixersPriorityMap(TextDocument document, [NotNullWhen(true)] out Lazy<ImmutableDictionary<CodeFixProvider, int>>? fixersPriorityMap)
    {
        if (_lazyFixerPriorityMap == null)
        {
            var fixersPriorityByLanguageMap = GetFixerPriorityPerLanguageMap(document.Project.Solution.Services);
            Interlocked.CompareExchange(ref _lazyFixerPriorityMap, fixersPriorityByLanguageMap, null);
        }
 
        return _lazyFixerPriorityMap.TryGetValue(document.Project.Language, out fixersPriorityMap);
    }
 
    private bool TryGetWorkspaceFixer(
        Lazy<CodeFixProvider, CodeChangeProviderMetadata> lazyFixer,
        SolutionServices services,
        bool logExceptionWithInfoBar,
        [NotNullWhen(returnValue: true)] out CodeFixProvider? fixer)
    {
        try
        {
            fixer = lazyFixer.Value;
            return true;
        }
        catch (Exception ex)
        {
            // Gracefully handle exceptions in creating fixer instance.
            // Log exception and show info bar, if needed.
            if (logExceptionWithInfoBar)
            {
                var errorReportingService = services.GetRequiredService<IErrorReportingService>();
                var message = lazyFixer.Metadata.Name != null
                    ? string.Format(FeaturesResources.Error_creating_instance_of_CodeFixProvider_0, lazyFixer.Metadata.Name)
                    : FeaturesResources.Error_creating_instance_of_CodeFixProvider;
 
                errorReportingService.ShowGlobalErrorInfo(
                    message,
                    TelemetryFeatureName.CodeFixProvider,
                    ex,
                    new InfoBarUI(
                        WorkspacesResources.Show_Stack_Trace,
                        InfoBarUI.UIKind.HyperLink,
                        () => errorReportingService.ShowDetailedErrorInfo(ex), closeAfterAction: true));
 
                foreach (var errorLogger in _errorLoggers)
                {
                    errorLogger.Value.LogException(this, ex);
                }
            }
 
            fixer = null;
            return false;
        }
    }
 
    private async IAsyncEnumerable<CodeFixCollection> StreamFixesAsync(
        TextDocument document,
        SortedDictionary<TextSpan, List<DiagnosticData>> spanToDiagnostics,
        bool fixAllForInSpan,
        ICodeActionRequestPriorityProvider priorityProvider,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        var hasAnySharedFixer = TryGetWorkspaceFixersMap(document, out var fixerMap);
 
        var projectFixersMap = GetProjectFixers(document);
        var hasAnyProjectFixer = projectFixersMap.Any();
 
        if (!hasAnySharedFixer && !hasAnyProjectFixer)
            yield break;
 
        // TODO (https://github.com/dotnet/roslyn/issues/4932): Don't restrict CodeFixes in Interactive
        var isInteractive = document.Project.Solution.WorkspaceKind == WorkspaceKind.Interactive;
 
        // gather CodeFixProviders for all distinct diagnostics found for current span
        using var _1 = PooledDictionary<CodeFixProvider, List<(TextSpan range, List<DiagnosticData> diagnostics)>>.GetInstance(out var fixerToRangesAndDiagnostics);
        using var _2 = PooledHashSet<CodeFixProvider>.GetInstance(out var currentFixers);
 
        foreach (var (range, diagnostics) in spanToDiagnostics)
        {
            currentFixers.Clear();
 
            foreach (var diagnosticId in diagnostics.Select(d => d.Id).Distinct())
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                // Prioritize NuGet based project code fixers over VSIX based workspace code fixers.
                if (hasAnyProjectFixer && projectFixersMap.TryGetValue(diagnosticId, out var projectFixers))
                {
                    Debug.Assert(!isInteractive);
                    AddAllFixers(projectFixers, range, diagnostics, currentFixers, fixerToRangesAndDiagnostics);
                }
 
                if (hasAnySharedFixer && fixerMap!.TryGetValue(diagnosticId, out var workspaceFixers))
                {
                    if (isInteractive)
                    {
                        AddAllFixers(workspaceFixers.WhereAsArray(IsInteractiveCodeFixProvider), range, diagnostics, currentFixers, fixerToRangesAndDiagnostics);
                    }
                    else
                    {
                        AddAllFixers(workspaceFixers, range, diagnostics, currentFixers, fixerToRangesAndDiagnostics);
                    }
                }
            }
        }
 
        if (fixerToRangesAndDiagnostics.Count == 0)
            yield break;
 
        // Now, sort the fixers so that the ones that are ordered before others get their chance to run first.
        var allFixers = fixerToRangesAndDiagnostics.Keys.ToImmutableArray();
        if (TryGetWorkspaceFixersPriorityMap(document, out var fixersForLanguage))
            allFixers = allFixers.Sort(new FixerComparer(allFixers, fixersForLanguage.Value));
 
        var extensionManager = document.Project.Solution.Services.GetService<IExtensionManager>();
 
        // Run each CodeFixProvider to gather individual CodeFixes for reported diagnostics.
        // Ensure that no diagnostic has registered code actions from different code fix providers with same equivalance key.
        // This prevents duplicate registered code actions from NuGet and VSIX code fix providers.
        // See https://github.com/dotnet/roslyn/issues/18818 for details.
        var uniqueDiagosticToEquivalenceKeysMap = new Dictionary<Diagnostic, PooledHashSet<string?>>();
 
        // NOTE: For backward compatibility, we allow multiple registered code actions from the same code fix provider
        // to have the same equivalence key. See https://github.com/dotnet/roslyn/issues/44553 for details.
        // To ensure this, we track the fixer that first registered a code action to fix a diagnostic with a specific equivalence key.
        var diagnosticAndEquivalenceKeyToFixersMap = new Dictionary<(Diagnostic diagnostic, string? equivalenceKey), CodeFixProvider>();
 
        try
        {
            foreach (var fixer in allFixers)
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                if (!priorityProvider.MatchesPriority(fixer))
                    continue;
 
                foreach (var (span, diagnostics) in fixerToRangesAndDiagnostics[fixer])
                {
                    // Log an individual telemetry event for slow codefix computations to
                    // allow targeted trace notifications for further investigation. 500 ms seemed like
                    // a good value so as to not be too noisy, but if fired, indicates a potential
                    // area requiring investigation.
                    const int CodeFixTelemetryDelay = 500;
 
                    var fixerName = fixer.GetType().Name;
                    var logMessage = KeyValueLogMessage.Create(static (m, args) =>
                    {
                        var (fixerName, document) = args;
                        m[TelemetryLogging.KeyName] = fixerName;
                        m[TelemetryLogging.KeyLanguageName] = document.Project.Language;
                    }, (fixerName, document));
 
                    using var _ = TelemetryLogging.LogBlockTime(FunctionId.CodeFix_Delay, logMessage, CodeFixTelemetryDelay);
 
                    var codeFixCollection = await TryGetFixesOrConfigurationsAsync(
                        document, span, diagnostics, fixAllForInSpan, fixer,
                        hasFix: d => this.GetFixableDiagnosticIds(fixer, extensionManager).Contains(d.Id),
                        getFixes: dxs =>
                        {
                            var fixerMetadata = TryGetMetadata(fixer);
 
                            using (RoslynEventSource.LogInformationalBlock(FunctionId.CodeFixes_GetCodeFixesAsync, fixerName, cancellationToken))
                            {
                                if (fixAllForInSpan)
                                {
                                    var primaryDiagnostic = dxs.First();
                                    return GetCodeFixesAsync(document, primaryDiagnostic.Location.SourceSpan, fixer, fixerMetadata,
                                        [primaryDiagnostic], uniqueDiagosticToEquivalenceKeysMap,
                                        diagnosticAndEquivalenceKeyToFixersMap, cancellationToken);
                                }
                                else
                                {
                                    return GetCodeFixesAsync(document, span, fixer, fixerMetadata, dxs,
                                        uniqueDiagosticToEquivalenceKeysMap, diagnosticAndEquivalenceKeyToFixersMap, cancellationToken);
                                }
                            }
                        },
                        cancellationToken).ConfigureAwait(false);
 
                    if (codeFixCollection != null)
                    {
                        yield return codeFixCollection;
 
                        // Just need the first result if we are doing fix all in span
                        if (fixAllForInSpan)
                            yield break;
                    }
                }
            }
        }
        finally
        {
            foreach (var pooledSet in uniqueDiagosticToEquivalenceKeysMap.Values)
            {
                pooledSet.Free();
            }
        }
 
        yield break;
 
        static void AddAllFixers(
            ImmutableArray<CodeFixProvider> fixers,
            TextSpan range,
            List<DiagnosticData> diagnostics,
            PooledHashSet<CodeFixProvider> currentFixers,
            PooledDictionary<CodeFixProvider, List<(TextSpan range, List<DiagnosticData> diagnostics)>> fixerToRangesAndDiagnostics)
        {
            foreach (var fixer in fixers)
            {
                if (currentFixers.Add(fixer))
                    fixerToRangesAndDiagnostics.MultiAdd(fixer, (range, diagnostics));
            }
        }
    }
 
    private CodeChangeProviderMetadata? TryGetMetadata(CodeFixProvider fixer)
    {
        return ImmutableInterlocked.GetOrAdd(
            ref _fixerToMetadataMap,
            fixer,
            static (fixer, fixers) =>
            {
                foreach (var lazy in fixers)
                {
                    if (lazy.IsValueCreated && lazy.Value == fixer)
                        return lazy.Metadata;
                }
 
                // Note: it feels very strange that we could ever not find a fixer in our list.  However, this
                // occurs in testing scenarios.  I'm not sure if the tests represent a bogus potential input, or if
                // this is something that can actually occur in practice and we want to keep working.
                return null;
            },
            _fixers);
    }
 
    private static async Task<ImmutableArray<CodeFix>> GetCodeFixesAsync(
        TextDocument document, TextSpan span, CodeFixProvider fixer, CodeChangeProviderMetadata? fixerMetadata,
        ImmutableArray<Diagnostic> diagnostics,
        Dictionary<Diagnostic, PooledHashSet<string?>> uniqueDiagosticToEquivalenceKeysMap,
        Dictionary<(Diagnostic diagnostic, string? equivalenceKey), CodeFixProvider> diagnosticAndEquivalenceKeyToFixersMap,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        using var fixesDisposer = ArrayBuilder<CodeFix>.GetInstance(out var fixes);
        var context = new CodeFixContext(document, span, diagnostics,
            // TODO: Can we share code between similar lambdas that we pass to this API in BatchFixAllProvider.cs, CodeFixService.cs and CodeRefactoringService.cs?
            (action, applicableDiagnostics) =>
            {
                // Serialize access for thread safety - we don't know what thread the fix provider will call this delegate from.
                lock (fixes)
                {
                    // Filter out applicable diagnostics which already have a registered code action with same equivalence key.
                    applicableDiagnostics = FilterApplicableDiagnostics(applicableDiagnostics, action.EquivalenceKey,
                        fixer, uniqueDiagosticToEquivalenceKeysMap, diagnosticAndEquivalenceKeyToFixersMap);
 
                    if (!applicableDiagnostics.IsEmpty)
                    {
                        // Add the CodeFix Provider Name to the parent CodeAction's CustomTags.
                        // Always add a name even in cases of 3rd party fixers that do not export
                        // name metadata.
                        action.AddCustomTagAndTelemetryInfo(fixerMetadata, fixer);
 
                        fixes.Add(new CodeFix(document.Project, action, applicableDiagnostics));
                    }
                }
            },
            cancellationToken);
 
        var task = fixer.RegisterCodeFixesAsync(context) ?? Task.CompletedTask;
        await task.ConfigureAwait(false);
        return fixes.ToImmutableAndClear();
 
        static ImmutableArray<Diagnostic> FilterApplicableDiagnostics(
            ImmutableArray<Diagnostic> applicableDiagnostics,
            string? equivalenceKey,
            CodeFixProvider fixer,
            Dictionary<Diagnostic, PooledHashSet<string?>> uniqueDiagosticToEquivalenceKeysMap,
            Dictionary<(Diagnostic diagnostic, string? equivalenceKey), CodeFixProvider> diagnosticAndEquivalenceKeyToFixersMap)
        {
            using var disposer = ArrayBuilder<Diagnostic>.GetInstance(out var newApplicableDiagnostics);
            foreach (var diagnostic in applicableDiagnostics)
            {
                if (!uniqueDiagosticToEquivalenceKeysMap.TryGetValue(diagnostic, out var equivalenceKeys))
                {
                    // First code action registered to fix this diagnostic with any equivalenceKey.
                    // Record the equivalence key and the fixer that registered this action.
                    equivalenceKeys = PooledHashSet<string?>.GetInstance();
                    equivalenceKeys.Add(equivalenceKey);
                    uniqueDiagosticToEquivalenceKeysMap[diagnostic] = equivalenceKeys;
                    diagnosticAndEquivalenceKeyToFixersMap.Add((diagnostic, equivalenceKey), fixer);
                }
                else if (equivalenceKeys.Add(equivalenceKey))
                {
                    // First code action registered to fix this diagnostic with the given equivalenceKey.
                    // Record the the fixer that registered this action.
                    diagnosticAndEquivalenceKeyToFixersMap.Add((diagnostic, equivalenceKey), fixer);
                }
                else if (diagnosticAndEquivalenceKeyToFixersMap[(diagnostic, equivalenceKey)] != fixer)
                {
                    // Diagnostic already has a registered code action with same equivalence key from a different fixer.
                    // Note that we allow same fixer to register multiple such code actions with the same equivalence key
                    // for backward compatibility. See https://github.com/dotnet/roslyn/issues/44553 for details.
                    continue;
                }
 
                newApplicableDiagnostics.Add(diagnostic);
            }
 
            return newApplicableDiagnostics.Count == applicableDiagnostics.Length
                ? applicableDiagnostics
                : newApplicableDiagnostics.ToImmutable();
        }
    }
 
    private async IAsyncEnumerable<CodeFixCollection> StreamConfigurationFixesAsync(
        TextDocument document,
        TextSpan diagnosticsSpan,
        IEnumerable<DiagnosticData> diagnostics,
        PooledHashSet<string> registeredConfigurationFixTitles,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        if (!_configurationProvidersMap.TryGetValue(document.Project.Language, out var lazyConfigurationProviders) ||
            lazyConfigurationProviders.Value == null)
        {
            yield break;
        }
 
        // append CodeFixCollection for each CodeFixProvider
        foreach (var provider in lazyConfigurationProviders.Value)
        {
            using (RoslynEventSource.LogInformationalBlock(FunctionId.CodeFixes_GetCodeFixesAsync, provider, cancellationToken))
            {
                var codeFixCollection = await TryGetFixesOrConfigurationsAsync(
                    document, diagnosticsSpan, diagnostics, fixAllForInSpan: false, provider,
                    hasFix: d => provider.IsFixableDiagnostic(d),
                    getFixes: async dxs =>
                    {
                        var fixes = await provider.GetFixesAsync(document, diagnosticsSpan, dxs, cancellationToken).ConfigureAwait(false);
                        return fixes.WhereAsArray(f => registeredConfigurationFixTitles.Add(f.Action.Title));
                    },
                    cancellationToken).ConfigureAwait(false);
                if (codeFixCollection != null)
                    yield return codeFixCollection;
            }
        }
    }
 
    private async Task<CodeFixCollection?> TryGetFixesOrConfigurationsAsync<TCodeFixProvider>(
        TextDocument textDocument,
        TextSpan fixesSpan,
        IEnumerable<DiagnosticData> diagnosticsWithSameSpan,
        bool fixAllForInSpan,
        TCodeFixProvider fixer,
        Func<Diagnostic, bool> hasFix,
        Func<ImmutableArray<Diagnostic>, Task<ImmutableArray<CodeFix>>> getFixes,
        CancellationToken cancellationToken)
        where TCodeFixProvider : notnull
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        var allDiagnostics =
            await diagnosticsWithSameSpan.OrderByDescending(d => d.Severity)
                                         .ToDiagnosticsAsync(textDocument.Project, cancellationToken).ConfigureAwait(false);
        var diagnostics = allDiagnostics.WhereAsArray(hasFix);
        if (diagnostics.Length <= 0)
        {
            // this can happen for suppression case where all diagnostics can't be suppressed
            return null;
        }
 
        var extensionManager = textDocument.Project.Solution.Services.GetRequiredService<IExtensionManager>();
        var fixes = await extensionManager.PerformFunctionAsync(fixer,
            _ => getFixes(diagnostics),
            defaultValue: [], cancellationToken).ConfigureAwait(false);
 
        if (fixes.IsDefaultOrEmpty)
            return null;
 
        // If the fix provider supports fix all occurrences, then get the corresponding FixAllProviderInfo and fix all context.
        var fixAllProviderInfo = extensionManager.PerformFunction(
            fixer, () => ImmutableInterlocked.GetOrAdd(ref _fixAllProviderMap, fixer, FixAllProviderInfo.Create), defaultValue: null);
 
        FixAllState? fixAllState = null;
        var supportedScopes = ImmutableArray<FixAllScope>.Empty;
        if (fixAllProviderInfo != null && textDocument is Document document)
        {
            var diagnosticIds = diagnostics.Where(fixAllProviderInfo.CanBeFixed)
                                           .Select(d => d.Id)
                                           .ToImmutableHashSet();
 
            var diagnosticProvider = fixAllForInSpan
                ? new FixAllPredefinedDiagnosticProvider(allDiagnostics)
                : (FixAllContext.DiagnosticProvider)new FixAllDiagnosticProvider(_diagnosticService, diagnosticIds);
 
            var codeFixProvider = (fixer as CodeFixProvider) ?? new WrapperCodeFixProvider((IConfigurationFixProvider)fixer, diagnostics.Select(d => d.Id));
 
            fixAllState = new FixAllState(
                (FixAllProvider)fixAllProviderInfo.FixAllProvider,
                fixesSpan,
                document,
                document.Project,
                codeFixProvider,
                FixAllScope.Document,
                fixes[0].Action.EquivalenceKey,
                diagnosticIds,
                diagnosticProvider);
 
            supportedScopes = fixAllProviderInfo.SupportedScopes;
        }
 
        return new CodeFixCollection(
            fixer, fixesSpan, fixes, fixAllState,
            supportedScopes, diagnostics.First());
    }
 
    /// <summary> Looks explicitly for an <see cref="AbstractSuppressionCodeFixProvider"/>.</summary>
    public CodeFixProvider? GetSuppressionFixer(string language, IEnumerable<string> diagnosticIds)
    {
        if (!_configurationProvidersMap.TryGetValue(language, out var lazyConfigurationProviders) ||
            lazyConfigurationProviders.Value.IsDefault)
        {
            return null;
        }
 
        // Explicitly looks for an AbstractSuppressionCodeFixProvider
        var fixer = lazyConfigurationProviders.Value.OfType<AbstractSuppressionCodeFixProvider>().FirstOrDefault();
        if (fixer == null)
        {
            return null;
        }
 
        return new WrapperCodeFixProvider(fixer, diagnosticIds);
    }
 
    private bool IsInteractiveCodeFixProvider(CodeFixProvider provider)
    {
        // TODO (https://github.com/dotnet/roslyn/issues/4932): Don't restrict CodeFixes in Interactive
        return provider is FullyQualify.AbstractFullyQualifyCodeFixProvider or
               AddImport.AbstractAddImportCodeFixProvider;
    }
 
    private ImmutableArray<DiagnosticId> GetFixableDiagnosticIds(CodeFixProvider fixer, IExtensionManager? extensionManager)
    {
        // If we are passed a null extension manager it means we do not have access to a document so there is nothing to
        // show the user.  In this case we will log any exceptions that occur, but the user will not see them.
        if (extensionManager != null)
        {
            return extensionManager.PerformFunction(
                fixer,
                () => ImmutableInterlocked.GetOrAdd(ref _fixerToFixableIdsMap, fixer, f => GetAndTestFixableDiagnosticIds(f)),
                defaultValue: []);
        }
 
        try
        {
            return ImmutableInterlocked.GetOrAdd(ref _fixerToFixableIdsMap, fixer, f => GetAndTestFixableDiagnosticIds(f));
        }
        catch (Exception e) when (e is not OperationCanceledException)
        {
            foreach (var logger in _errorLoggers)
            {
                logger.Value.LogException(fixer, e);
            }
 
            return [];
        }
    }
 
    private static ImmutableArray<string> GetAndTestFixableDiagnosticIds(CodeFixProvider codeFixProvider)
    {
        var ids = codeFixProvider.FixableDiagnosticIds;
        if (ids.IsDefault)
        {
            throw new InvalidOperationException(
                string.Format(
                    WorkspacesResources._0_returned_an_uninitialized_ImmutableArray,
                    codeFixProvider.GetType().Name + "." + nameof(CodeFixProvider.FixableDiagnosticIds)));
        }
 
        return ids;
    }
 
    private ImmutableDictionary<LanguageKind, Lazy<ImmutableDictionary<DiagnosticId, ImmutableArray<CodeFixProvider>>>> GetFixerPerLanguageMap(
        SolutionServices services)
    {
        var fixerMap = ImmutableDictionary.Create<LanguageKind, Lazy<ImmutableDictionary<DiagnosticId, ImmutableArray<CodeFixProvider>>>>();
        var extensionManager = services.GetService<IExtensionManager>();
        foreach (var (diagnosticId, lazyFixers) in _fixersPerLanguageMap)
        {
            var lazyMap = new Lazy<ImmutableDictionary<DiagnosticId, ImmutableArray<CodeFixProvider>>>(() =>
            {
                using var _ = PooledDictionary<DiagnosticId, ArrayBuilder<CodeFixProvider>>.GetInstance(out var mutableMap);
 
                foreach (var lazyFixer in lazyFixers)
                {
                    if (!TryGetWorkspaceFixer(lazyFixer, services, logExceptionWithInfoBar: true, out var fixer))
                    {
                        continue;
                    }
 
                    foreach (var id in this.GetFixableDiagnosticIds(fixer, extensionManager))
                    {
                        if (string.IsNullOrWhiteSpace(id))
                        {
                            continue;
                        }
 
                        mutableMap.MultiAdd(id, fixer);
                    }
                }
 
                return mutableMap.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableAndFree());
            }, isThreadSafe: true);
 
            fixerMap = fixerMap.Add(diagnosticId, lazyMap);
        }
 
        return fixerMap;
    }
 
    private static ImmutableDictionary<LanguageKind, Lazy<ImmutableArray<IConfigurationFixProvider>>> GetConfigurationProvidersPerLanguageMap(
        IEnumerable<Lazy<IConfigurationFixProvider, CodeChangeProviderMetadata>> configurationProviders)
    {
        var configurationProvidersPerLanguageMap = configurationProviders.ToPerLanguageMapWithMultipleLanguages();
 
        var configurationFixerMap = ImmutableDictionary.CreateBuilder<LanguageKind, Lazy<ImmutableArray<IConfigurationFixProvider>>>();
        foreach (var (diagnosticId, lazyFixers) in configurationProvidersPerLanguageMap)
        {
            var lazyConfigurationFixers = new Lazy<ImmutableArray<IConfigurationFixProvider>>(() => GetConfigurationFixProviders(lazyFixers));
            configurationFixerMap.Add(diagnosticId, lazyConfigurationFixers);
        }
 
        return configurationFixerMap.ToImmutable();
 
        static ImmutableArray<IConfigurationFixProvider> GetConfigurationFixProviders(ImmutableArray<Lazy<IConfigurationFixProvider, CodeChangeProviderMetadata>> languageKindAndFixers)
        {
            var orderedLanguageKindAndFixers = ExtensionOrderer.Order(languageKindAndFixers);
            var builder = new FixedSizeArrayBuilder<IConfigurationFixProvider>(orderedLanguageKindAndFixers.Count);
            foreach (var languageKindAndFixersValue in orderedLanguageKindAndFixers)
                builder.Add(languageKindAndFixersValue.Value);
 
            return builder.MoveToImmutable();
        }
    }
 
    private ImmutableDictionary<LanguageKind, Lazy<ImmutableDictionary<CodeFixProvider, int>>> GetFixerPriorityPerLanguageMap(SolutionServices services)
    {
        var languageMap = ImmutableDictionary.CreateBuilder<LanguageKind, Lazy<ImmutableDictionary<CodeFixProvider, int>>>();
        foreach (var (diagnosticId, lazyFixers) in _fixersPerLanguageMap)
        {
            var lazyMap = new Lazy<ImmutableDictionary<CodeFixProvider, int>>(() =>
            {
                var priorityMap = ImmutableDictionary.CreateBuilder<CodeFixProvider, int>();
 
                var fixers = ExtensionOrderer.Order(lazyFixers);
                for (var i = 0; i < fixers.Count; i++)
                {
                    if (!TryGetWorkspaceFixer(fixers[i], services, logExceptionWithInfoBar: false, out var fixer))
                        continue;
 
                    priorityMap.Add(fixer, i);
                }
 
                return priorityMap.ToImmutable();
            }, isThreadSafe: true);
 
            languageMap.Add(diagnosticId, lazyMap);
        }
 
        return languageMap.ToImmutable();
    }
 
    private ImmutableDictionary<DiagnosticId, ImmutableArray<CodeFixProvider>> GetProjectFixers(TextDocument document)
    {
        // TODO (https://github.com/dotnet/roslyn/issues/4932): Don't restrict CodeFixes in Interactive
        return document.Project.Solution.WorkspaceKind == WorkspaceKind.Interactive
            ? ImmutableDictionary<DiagnosticId, ImmutableArray<CodeFixProvider>>.Empty
            : _projectFixersMap.GetValue(document.Project.AnalyzerReferences, _ => ComputeProjectFixers(document));
    }
 
    private ImmutableDictionary<DiagnosticId, ImmutableArray<CodeFixProvider>> ComputeProjectFixers(TextDocument document)
    {
        var extensionManager = document.Project.Solution.Services.GetService<IExtensionManager>();
 
        using var _ = PooledDictionary<DiagnosticId, ArrayBuilder<CodeFixProvider>>.GetInstance(out var builder);
        var codeFixProviders = ProjectCodeFixProvider.GetExtensions(document, GetExtensionInfo);
        foreach (var fixer in codeFixProviders)
        {
            var fixableIds = this.GetFixableDiagnosticIds(fixer, extensionManager);
            foreach (var id in fixableIds)
            {
                if (string.IsNullOrWhiteSpace(id))
                    continue;
 
                builder.MultiAdd(id, fixer);
            }
        }
 
        return builder.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableAndFree());
    }
 
    private static ProjectCodeFixProvider.ExtensionInfo GetExtensionInfo(ExportCodeFixProviderAttribute attribute)
    {
        var kinds = EnumArrayConverter.FromStringArray<TextDocumentKind>(attribute.DocumentKinds);
 
        return new(kinds, attribute.DocumentExtensions);
    }
 
    private sealed class FixerComparer : IComparer<CodeFixProvider>
    {
        private readonly Dictionary<CodeFixProvider, int> _fixerToIndex;
        private readonly ImmutableDictionary<CodeFixProvider, int> _priorityMap;
 
        public FixerComparer(
            ImmutableArray<CodeFixProvider> allFixers,
            ImmutableDictionary<CodeFixProvider, int> priorityMap)
        {
            _fixerToIndex = allFixers.Select((fixer, index) => (fixer, index)).ToDictionary(t => t.fixer, t => t.index);
            _priorityMap = priorityMap;
        }
 
        public int Compare([AllowNull] CodeFixProvider x, [AllowNull] CodeFixProvider y)
        {
            Contract.ThrowIfNull(x);
            Contract.ThrowIfNull(y);
 
            // If the fixers specify an explicit ordering between each other, then respect that.
            if (_priorityMap.TryGetValue(x, out var xOrder) &&
                _priorityMap.TryGetValue(y, out var yOrder))
            {
                var comparison = xOrder - yOrder;
                if (comparison != 0)
                    return comparison;
            }
 
            // Otherwise, keep things in the same order that they were in the list (i.e. keep things stable).
            return _fixerToIndex[x] - _fixerToIndex[y];
        }
    }
 
    public TestAccessor GetTestAccessor()
        => new(this);
 
    public readonly struct TestAccessor
    {
        private readonly CodeFixService _codeFixService;
 
        public TestAccessor(CodeFixService codeFixService)
        {
            _codeFixService = codeFixService;
        }
 
        public ImmutableDictionary<LanguageKind, Lazy<ImmutableDictionary<CodeFixProvider, int>>> GetFixerPriorityPerLanguageMap(SolutionServices services)
            => _codeFixService.GetFixerPriorityPerLanguageMap(services);
    }
}