File: src\Workspaces\SharedUtilitiesAndExtensions\Workspace\Core\CodeFixes\SyntaxEditorBasedCodeFixProvider.cs
Web Access
Project: src\src\CodeStyle\Core\CodeFixes\Microsoft.CodeAnalysis.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CodeStyle.Fixes)
// 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.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixesAndRefactorings;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.CodeFixes;
 
internal abstract partial class SyntaxEditorBasedCodeFixProvider(bool supportsFixAll = true) : CodeFixProvider
{
    private static readonly ImmutableArray<FixAllScope> s_defaultSupportedFixAllScopes =
        [FixAllScope.Document, FixAllScope.Project, FixAllScope.Solution, FixAllScope.ContainingMember, FixAllScope.ContainingType];
 
    private readonly bool _supportsFixAll = supportsFixAll;
 
    public sealed override FixAllProvider? GetFixAllProvider()
    {
        if (!_supportsFixAll)
            return null;
 
        return FixAllProvider.Create(
            async (fixAllContext, document, diagnostics) =>
            {
                // Ensure that diagnostics for this document are always in document location order.  This provides a
                // consistent and deterministic order for fixers that want to update a document.
                //
                // Also ensure that we do not pass in duplicates by invoking Distinct.  See
                // https://github.com/dotnet/roslyn/issues/31381, that seems to be causing duplicate diagnostics.
                var filteredDiagnostics = diagnostics.Distinct()
                                                     .WhereAsArray(d => this.IncludeDiagnosticDuringFixAll(d, document, fixAllContext.CodeActionEquivalenceKey, fixAllContext.CancellationToken))
                                                     .Sort((d1, d2) => d1.Location.SourceSpan.Start - d2.Location.SourceSpan.Start);
 
                if (filteredDiagnostics.Length == 0)
                    return document;
 
                return await FixAllAsync(document, filteredDiagnostics, fixAllContext.CancellationToken).ConfigureAwait(false);
            },
            s_defaultSupportedFixAllScopes);
    }
 
    protected void RegisterCodeFix(CodeFixContext context, string title, string equivalenceKey, Diagnostic? diagnostic = null)
        => context.RegisterCodeFix(CodeAction.Create(title, GetDocumentUpdater(context, diagnostic), equivalenceKey), context.Diagnostics);
 
    protected void RegisterCodeFix(CodeFixContext context, string title, string equivalenceKey, CodeActionPriority priority, Diagnostic? diagnostic = null)
        => context.RegisterCodeFix(CodeAction.Create(title, GetDocumentUpdater(context, diagnostic), equivalenceKey, priority), context.Diagnostics);
 
    protected Func<CancellationToken, Task<Document>> GetDocumentUpdater(CodeFixContext context, Diagnostic? diagnostic = null)
    {
        var diagnostics = ImmutableArray.Create(diagnostic ?? context.Diagnostics[0]);
        return cancellationToken => FixAllAsync(context.Document, diagnostics, cancellationToken);
    }
 
    private Task<Document> FixAllAsync(
        Document document, ImmutableArray<Diagnostic> diagnostics, CancellationToken cancellationToken)
    {
        return FixAllWithEditorAsync(
            document,
            editor => FixAllAsync(document, diagnostics, editor, cancellationToken),
            cancellationToken);
    }
 
    internal static async Task<Document> FixAllWithEditorAsync(
        Document document,
        Func<SyntaxEditor, Task> editAsync,
        CancellationToken cancellationToken)
    {
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var editor = new SyntaxEditor(root, document.Project.Solution.Services);
 
        await editAsync(editor).ConfigureAwait(false);
 
        var newRoot = editor.GetChangedRoot();
        return document.WithSyntaxRoot(newRoot);
    }
 
    /// <summary>
    /// Fixes all <paramref name="diagnostics"/> in the specified <paramref name="editor"/>.
    /// The fixes are applied to the <paramref name="document"/>'s syntax tree via <paramref name="editor"/>.
    /// The implementation may query options of any document in the <paramref name="document"/>'s solution.
    /// </summary>
    protected abstract Task FixAllAsync(
        Document document, ImmutableArray<Diagnostic> diagnostics, SyntaxEditor editor, CancellationToken cancellationToken);
 
    /// <summary>
    /// Whether or not this diagnostic should be included when performing a FixAll.  This is useful for providers that
    /// create multiple diagnostics for the same issue (For example, one main diagnostic and multiple 'faded out code'
    /// diagnostics).  FixAll can be invoked from any of those, but we'll only want perform an edit for only one
    /// diagnostic for each of those sets of diagnostics.
    /// <para/>
    /// This overload differs from <see cref="IncludeDiagnosticDuringFixAll(Diagnostic)"/> in that it also passes along
    /// the <see cref="FixAllState"/> in case that would be useful (for example if the <see
    /// cref="IFixAllState.CodeActionEquivalenceKey"/> is used.
    /// <para/>
    /// Only one of these two overloads needs to be overridden if you want to customize behavior.
    /// </summary>
    protected virtual bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic, Document document, string? equivalenceKey, CancellationToken cancellationToken)
        => IncludeDiagnosticDuringFixAll(diagnostic);
 
    /// <summary>
    /// Whether or not this diagnostic should be included when performing a FixAll.  This is useful for providers that
    /// create multiple diagnostics for the same issue (For example, one main diagnostic and multiple 'faded out code'
    /// diagnostics).  FixAll can be invoked from any of those, but we'll only want perform an edit for only one
    /// diagnostic for each of those sets of diagnostics.
    /// <para/>
    /// By default, all diagnostics will be included in fix-all unless they are filtered out here. If only the
    /// diagnostic needs to be queried to make this determination, only this overload needs to be overridden.  However,
    /// if information from <see cref="FixAllState"/> is needed (for example <see
    /// cref="IFixAllState.CodeActionEquivalenceKey"/>), then <see cref="IncludeDiagnosticDuringFixAll(Diagnostic,
    /// Document, string, CancellationToken)"/> should be overridden instead.
    /// <para/>
    /// Only one of these two overloads needs to be overridden if you want to customize behavior.
    /// </summary>
    protected virtual bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic)
        => true;
}