File: CodeActions\CodeAction.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Tags;
using Microsoft.CodeAnalysis.Telemetry;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CodeActions;
 
/// <summary>
/// An action produced by a <see cref="CodeFixProvider"/> or a <see cref="CodeRefactoringProvider"/>.
/// </summary>
public abstract partial class CodeAction
{
    private static readonly Dictionary<Type, bool> s_isNonProgressGetChangedSolutionAsyncOverridden = [];
    private static readonly Dictionary<Type, bool> s_isNonProgressComputeOperationsAsyncOverridden = [];
 
    /// <summary>
    /// Special tag that indicates that it's this is a privileged code action that is allowed to use the <see
    /// cref="CodeActionPriority.High"/> priority class.
    /// </summary>
    internal static readonly string CanBeHighPriorityTag = Guid.NewGuid().ToString();
 
    /// <summary>
    /// Tag we use to convey that this code action should only be shown if it's in a host that allows for
    /// non-document changes.  For example if it needs to make project changes, or if will show host-specific UI.
    /// <para>
    /// Note: if the bulk of code action is just document changes, and it does some optional things beyond that
    /// (like navigating the user somewhere) this should not be set.  Such a code action is still usable in all
    /// hosts and should be shown to the user.  It's only if the code action can truly not function should this
    /// tag be provided.
    /// </para>
    /// <para>
    /// Currently, this also means that we presume that all 3rd party code actions do not require non-document
    /// changes and we will show them all in all hosts.
    /// </para>
    /// </summary>
    internal const string RequiresNonDocumentChange = nameof(RequiresNonDocumentChange);
    private protected static ImmutableArray<string> RequiresNonDocumentChangeTags = [RequiresNonDocumentChange];
 
    /// <summary>
    /// A short title describing the action that may appear in a menu.
    /// </summary>
    public abstract string Title { get; }
 
    internal virtual string Message => Title;
 
    /// <summary>
    /// Two code actions are treated as equivalent if they have equal non-null <see cref="EquivalenceKey"/> values and were generated
    /// by the same <see cref="CodeFixProvider"/> or <see cref="CodeRefactoringProvider"/>.
    /// </summary>
    /// <remarks>
    /// Equivalence of code actions affects some Visual Studio behavior. For example, if multiple equivalent
    /// code actions result from code fixes or refactorings for a single Visual Studio light bulb instance,
    /// the light bulb UI will present only one code action from each set of equivalent code actions.
    /// Additionally, a Fix All operation will apply only code actions that are equivalent to the original code action.
    ///
    /// If two code actions that could be treated as equivalent do not have equal <see cref="EquivalenceKey"/> values, Visual Studio behavior
    /// may be less helpful than would be optimal. If two code actions that should be treated as distinct have
    /// equal <see cref="EquivalenceKey"/> values, Visual Studio behavior may appear incorrect.
    /// </remarks>
    public virtual string? EquivalenceKey => null;
 
    /// <summary>
    /// Priority of this particular action within a group of other actions.  Less relevant actions should override
    /// this and specify a lower priority so that more important actions are easily accessible to the user.  Returns
    /// <see cref="CodeActionPriority.Default"/> if not overridden.
    /// </summary>
    public CodeActionPriority Priority
    {
        get
        {
            var priority = ComputePriority();
            if (priority < CodeActionPriority.Lowest)
                priority = CodeActionPriority.Lowest;
 
            if (priority > CodeActionPriority.High)
                priority = CodeActionPriority.High;
 
            if (priority == CodeActionPriority.High && !this.CustomTags.Contains(CanBeHighPriorityTag))
                priority = CodeActionPriority.Default;
 
            return priority;
        }
    }
 
    private bool IsNonProgressApiOverridden(Dictionary<Type, bool> dictionary, Func<CodeAction, bool> computeResult)
    {
        var type = this.GetType();
        lock (dictionary)
        {
            return dictionary.GetOrAdd(type, computeResult(this));
        }
    }
 
    private bool IsNonProgressComputeOperationsAsyncOverridden()
    {
#pragma warning disable RS0030 // Do not use banned APIs
        return IsNonProgressApiOverridden(
            s_isNonProgressComputeOperationsAsyncOverridden,
            static codeAction => new Func<CancellationToken, Task<IEnumerable<CodeActionOperation>>>(codeAction.ComputeOperationsAsync).Method.DeclaringType != typeof(CodeAction));
#pragma warning restore RS0030 // Do not use banned APIs
    }
 
    private bool IsNonProgressGetChangedSolutionAsyncOverridden()
    {
        return IsNonProgressApiOverridden(
            s_isNonProgressGetChangedSolutionAsyncOverridden,
            static codeAction => new Func<CancellationToken, Task<Solution?>>(codeAction.GetChangedSolutionAsync).Method.DeclaringType != typeof(CodeAction));
    }
 
    /// <summary>
    /// Computes the <see cref="CodeActionPriority"/> group this code action should be presented in. Legal values
    /// this can be must be between <see cref="CodeActionPriority.Lowest"/> and <see cref="CodeActionPriority.High"/>.
    /// </summary>
    /// <remarks>
    /// Values outside of this range will be clamped to be within that range.  Requests for <see
    /// cref="CodeActionPriority.High"/> may be downgraded to <see cref="CodeActionPriority.Default"/> as they
    /// poorly behaving high-priority items can cause a negative user experience.
    /// </remarks>
    protected virtual CodeActionPriority ComputePriority()
        => CodeActionPriority.Default;
 
    /// <summary>
    /// Descriptive tags from <see cref="WellKnownTags"/>.
    /// These tags may influence how the item is displayed.
    /// </summary>
    public virtual ImmutableArray<string> Tags => [];
 
    /// <summary>
    /// Child actions contained within this <see cref="CodeAction"/>.  Can be presented in a host to provide more
    /// potential solution actions to a particular problem.  To create a <see cref="CodeAction"/> with nested
    /// actions, use <see cref="Create(string, ImmutableArray{CodeAction}, bool)"/>.
    /// </summary>
    public virtual ImmutableArray<CodeAction> NestedActions
        => [];
 
    /// <summary>
    /// Code actions that should be presented as hyperlinks in the code action preview pane,
    /// similar to FixAll scopes and Preview Changes but may not apply to ALL CodeAction types.
    /// </summary>
    internal virtual ImmutableArray<CodeAction> AdditionalPreviewFlavors => [];
 
    /// <summary>
    /// Bridge method for sdk. https://github.com/dotnet/roslyn-sdk/issues/1136 tracks removing this.
    /// </summary>
    internal ImmutableArray<CodeAction> NestedCodeActions
        => NestedActions;
 
    /// <summary>
    /// If this code action contains <see cref="NestedActions"/>, this property provides a hint to hosts as to
    /// whether or not it's ok to elide this code action and just present the nested actions instead.  When a host
    /// already has a lot of top-level actions to show, it should consider <em>not</em> inlining this action, to
    /// keep the number of options presented to the user low.  However, if there are few options to show to the
    /// user, inlining this action could be beneficial as it would allow the user to see and choose one of the
    /// nested options with less steps.  To create a <see cref="CodeAction"/> with nested actions, use <see
    /// cref="Create(string, ImmutableArray{CodeAction}, bool)"/>.
    /// </summary>
    public virtual bool IsInlinable => false;
 
    /// <summary>
    /// Gets custom tags for the CodeAction.
    /// </summary>
    internal ImmutableArray<string> CustomTags { get; set; } = [];
 
    /// <summary>
    /// Lazily set provider type that registered this code action.
    /// Used for telemetry purposes only.
    /// </summary>
    private Type? _providerTypeForTelemetry;
 
    /// <summary>
    /// Used by the CodeFixService and CodeRefactoringService to add the Provider Name as a CustomTag.
    /// </summary>
    internal void AddCustomTagAndTelemetryInfo(CodeChangeProviderMetadata? providerMetadata, object provider)
    {
        Contract.ThrowIfFalse(provider is CodeFixProvider or CodeRefactoringProvider);
 
        // Add the provider name to the parent CodeAction's CustomTags.
        // Always add a name even in cases of 3rd party fixers/refactorings that do not export
        // name metadata.
        var tag = providerMetadata?.Name ?? provider.GetTypeDisplayName();
        CustomTags = CustomTags.Add(tag);
 
        // Set the provider type to use for logging telemetry.
        _providerTypeForTelemetry = provider.GetType();
    }
 
    internal Guid GetTelemetryId(FixAllScope? fixAllScope = null)
    {
        // We need to identify the type name to use for CodeAction's telemetry ID.
        // For code actions created from 'CodeAction.Create' factory methods,
        // we use the provider type for telemetry.  For the rest of the code actions
        // created by sub-typing CodeAction type, we use the code action type for telemetry.
        // For the former case, if the provider type is not set, we fallback to the CodeAction type instead.
        var isFactoryGenerated = this is SimpleCodeAction { CreatedFromFactoryMethod: true };
        var type = isFactoryGenerated && _providerTypeForTelemetry != null
            ? _providerTypeForTelemetry
            : this.GetType();
 
        // Additionally, we also add the equivalence key and fixAllScope ID (if non-null)
        // to the telemetry ID.
        var scope = fixAllScope?.GetScopeIdForTelemetry() ?? 0;
        return type.GetTelemetryId(scope, EquivalenceKey);
    }
 
    /// <summary>
    /// The sequence of operations that define the code action.
    /// </summary>
    public Task<ImmutableArray<CodeActionOperation>> GetOperationsAsync(CancellationToken cancellationToken)
        => GetOperationsAsync(originalSolution: null!, CodeAnalysisProgress.None, cancellationToken);
 
    /// <summary>
    /// The sequence of operations that define the code action.
    /// </summary>
    public Task<ImmutableArray<CodeActionOperation>> GetOperationsAsync(
        Solution originalSolution, IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
    {
        return GetOperationsCoreAsync(originalSolution, progress, cancellationToken);
    }
 
    private protected virtual async Task<ImmutableArray<CodeActionOperation>> GetOperationsCoreAsync(
        Solution originalSolution, IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
    {
        var operations = await this.ComputeOperationsAsync(progress, cancellationToken).ConfigureAwait(false);
 
        if (operations != null)
        {
            return await PostProcessAsync(originalSolution, operations, cancellationToken).ConfigureAwait(false);
        }
 
        return [];
    }
 
    /// <summary>
    /// The sequence of operations used to construct a preview.
    /// </summary>
    public Task<ImmutableArray<CodeActionOperation>> GetPreviewOperationsAsync(CancellationToken cancellationToken)
        => GetPreviewOperationsAsync(originalSolution: null!, cancellationToken);
 
    internal async Task<ImmutableArray<CodeActionOperation>> GetPreviewOperationsAsync(
        Solution originalSolution, CancellationToken cancellationToken)
    {
        using var _ = TelemetryLogging.LogBlockTimeAggregatedHistogram(FunctionId.SuggestedAction_Preview_Summary, $"Total");
 
        var operations = await this.ComputePreviewOperationsAsync(cancellationToken).ConfigureAwait(false);
 
        if (operations != null)
        {
            return await PostProcessAsync(originalSolution, operations, cancellationToken).ConfigureAwait(false);
        }
 
        return [];
    }
 
    /// <summary>
    /// Override this method if you want to implement a <see cref="CodeAction"/> subclass that includes custom <see
    /// cref="CodeActionOperation"/>'s.
    /// </summary>
    protected virtual async Task<IEnumerable<CodeActionOperation>> ComputeOperationsAsync(CancellationToken cancellationToken)
    {
        var changedSolution = await GetChangedSolutionAsync(CodeAnalysisProgress.None, cancellationToken).ConfigureAwait(false);
        return changedSolution == null
            ? []
            : [new ApplyChangesOperation(changedSolution)];
    }
 
#pragma warning disable RS0030 // Do not use banned APIs
 
    /// <summary>
    /// Override this method if you want to implement a <see cref="CodeAction"/> subclass that includes custom <see
    /// cref="CodeActionOperation"/>'s.  Prefer overriding this method over <see
    /// cref="ComputeOperationsAsync(CancellationToken)"/> when computation is long running and progress should be
    /// shown to the user.
    /// </summary>
    protected virtual async Task<ImmutableArray<CodeActionOperation>> ComputeOperationsAsync(
        IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
    {
        // If the subclass overrode `ComputeOperationsAsync(CancellationToken)` then we must call into that in
        // order to preserve whatever logic our subclass had had for determining the new solution.
        if (IsNonProgressComputeOperationsAsyncOverridden())
        {
            var operations = await ComputeOperationsAsync(cancellationToken).ConfigureAwait(false);
            return operations.ToImmutableArrayOrEmpty();
        }
        else
        {
            var changedSolution = await GetChangedSolutionAsync(progress, cancellationToken).ConfigureAwait(false);
            return changedSolution == null
                ? []
                : [new ApplyChangesOperation(changedSolution)];
        }
    }
 
#pragma warning restore RS0030 // Do not use banned APIs
 
    /// <summary>
    /// Override this method if you want to implement a <see cref="CodeAction"/> that has a set of preview operations that are different
    /// than the operations produced by <see cref="ComputeOperationsAsync(IProgress{CodeAnalysisProgress}, CancellationToken)"/>.
    /// </summary>
    protected virtual async Task<IEnumerable<CodeActionOperation>> ComputePreviewOperationsAsync(CancellationToken cancellationToken)
        => await ComputeOperationsAsync(CodeAnalysisProgress.None, cancellationToken).ConfigureAwait(false);
 
    /// <summary>
    /// Computes all changes for an entire solution. Override this method if you want to implement a <see
    /// cref="CodeAction"/> subclass that changes more than one document.  Override <see
    /// cref="GetChangedSolutionAsync(IProgress{CodeAnalysisProgress}, CancellationToken)"/> to report progress
    /// progress while computing the operations.
    /// </summary>
    protected virtual async Task<Solution?> GetChangedSolutionAsync(CancellationToken cancellationToken)
    {
        var changedDocument = await GetChangedDocumentAsync(cancellationToken).ConfigureAwait(false);
        return changedDocument?.Project.Solution;
    }
 
    /// <summary>
    /// Computes all changes for an entire solution. Override this method if you want to implement a <see
    /// cref="CodeAction"/> subclass that changes more than one document. Prefer overriding this method over <see
    /// cref="GetChangedSolutionAsync(CancellationToken)"/> when computation is long running and progress should be
    /// shown to the user.
    /// </summary>
    protected virtual async Task<Solution?> GetChangedSolutionAsync(IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
    {
        // If the subclass overrode `GetChangedSolutionAsync(CancellationToken)` then we must call into that in
        // order to preserve whatever logic our subclass had had for determining the new solution.
        if (IsNonProgressGetChangedSolutionAsyncOverridden())
        {
            return await GetChangedSolutionAsync(cancellationToken).ConfigureAwait(false);
        }
        else
        {
            // Otherwise, attempt to determine the changed document (the same logic as GetChangedSolutionAsync), but
            // this time pass the progress information along so it is not lost.
            var changedDocument = await GetChangedDocumentAsync(progress, cancellationToken).ConfigureAwait(false);
            return changedDocument?.Project.Solution;
        }
    }
 
    internal async Task<Solution> GetRequiredChangedSolutionAsync(IProgress<CodeAnalysisProgress> progressTracker, CancellationToken cancellationToken)
    {
        var solution = await this.GetChangedSolutionAsync(progressTracker, cancellationToken).ConfigureAwait(false);
        return solution ?? throw new InvalidOperationException(string.Format(WorkspacesResources.CodeAction_0_did_not_produce_a_changed_solution, this.Title));
    }
 
    /// <summary>
    /// Computes changes for a single document. Override this method if you want to implement a <see
    /// cref="CodeAction"/> subclass that changes a single document.  Override <see
    /// cref="GetChangedDocumentAsync(IProgress{CodeAnalysisProgress}, CancellationToken)"/> to report progress
    /// progress while computing the operations.
    /// </summary>
    /// <remarks>
    /// All code actions are expected to operate on solutions. This method is a helper to simplify the
    /// implementation of <see cref="GetChangedSolutionAsync(CancellationToken)"/> for code actions that only need
    /// to change one document.
    /// </remarks>
    /// <exception cref="NotSupportedException">If this code action does not support changing a single
    /// document.</exception>
    protected virtual Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken)
        => throw new NotSupportedException(GetType().FullName);
 
    /// <summary>
    /// Computes changes for a single document. Override this method if you want to implement a <see
    /// cref="CodeAction"/> subclass that changes a single document. Prefer overriding this method over <see
    /// cref="GetChangedDocumentAsync(CancellationToken)"/> when computation is long running and progress should be
    /// shown to the user.
    /// </summary>
    /// <remarks>
    /// All code actions are expected to operate on solutions. This method is a helper to simplify the
    /// implementation of <see cref="GetChangedSolutionAsync(CancellationToken)"/> for code actions that only need
    /// to change one document.
    /// </remarks>
    /// <exception cref="NotSupportedException">If this code action does not support changing a single
    /// document.</exception>
    protected virtual Task<Document> GetChangedDocumentAsync(IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
        => GetChangedDocumentAsync(cancellationToken);
 
    /// <summary>
    /// used by batch fixer engine to get new solution
    /// </summary>
    internal async Task<Solution?> GetChangedSolutionInternalAsync(
        Solution originalSolution, IProgress<CodeAnalysisProgress> progress, bool postProcessChanges = true, CancellationToken cancellationToken = default)
    {
        var solution = await GetChangedSolutionAsync(progress, cancellationToken).ConfigureAwait(false);
        if (solution == null || !postProcessChanges)
        {
            return solution;
        }
 
        return await PostProcessChangesAsync(originalSolution, solution, cancellationToken).ConfigureAwait(false);
    }
 
    internal Task<Document> GetChangedDocumentInternalAsync(CancellationToken cancellation)
        => GetChangedDocumentAsync(cancellation);
 
    /// <summary>
    /// Apply post processing steps to any <see cref="ApplyChangesOperation"/>'s.
    /// </summary>
    /// <param name="operations">A list of operations.</param>
    /// <param name="cancellationToken">A cancellation token.</param>
    /// <returns>A new list of operations with post processing steps applied to any <see cref="ApplyChangesOperation"/>'s.</returns>
#pragma warning disable CA1822 // Mark members as static. This is a public API.
    protected Task<ImmutableArray<CodeActionOperation>> PostProcessAsync(IEnumerable<CodeActionOperation> operations, CancellationToken cancellationToken)
#pragma warning restore CA1822 // Mark members as static
        => PostProcessAsync(originalSolution: null, operations, cancellationToken);
 
    internal static async Task<ImmutableArray<CodeActionOperation>> PostProcessAsync(
        Solution? originalSolution, IEnumerable<CodeActionOperation> operations, CancellationToken cancellationToken)
    {
        using var result = TemporaryArray<CodeActionOperation>.Empty;
 
        foreach (var op in operations)
        {
            if (op is ApplyChangesOperation ac)
            {
                result.Add(new ApplyChangesOperation(await PostProcessChangesAsync(originalSolution, ac.ChangedSolution, cancellationToken).ConfigureAwait(false)));
            }
            else
            {
                result.Add(op);
            }
        }
 
        return result.ToImmutableAndClear();
    }
 
    /// <summary>
    /// Apply post processing steps to solution changes, like formatting and simplification.
    /// </summary>
    /// <param name="changedSolution">The solution changed by the <see cref="CodeAction"/>.</param>
    /// <param name="cancellationToken">A cancellation token</param>
#pragma warning disable CA1822 // Mark members as static. This is a public API.
    protected Task<Solution> PostProcessChangesAsync(Solution changedSolution, CancellationToken cancellationToken)
#pragma warning restore CA1822 // Mark members as static
        => PostProcessChangesAsync(originalSolution: null, changedSolution, cancellationToken);
 
    private static async Task<Solution> PostProcessChangesAsync(
        Solution? originalSolution,
        Solution changedSolution,
        CancellationToken cancellationToken)
    {
        // originalSolution is only null on backward compatible codepaths.  In that case, we get the workspace's
        // current solution.  This is not ideal (as that is a mutable field that could be changing out from
        // underneath us).  But it's the only option we have for the compat case with existing public extension
        // points.
        originalSolution ??= changedSolution.Workspace.CurrentSolution;
 
        return await CleanSyntaxAndSemanticsAsync(originalSolution, changedSolution, CodeAnalysisProgress.None, cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Apply post processing steps to a single document:
    ///   Reducing nodes annotated with <see cref="Simplifier.Annotation"/>
    ///   Formatting nodes annotated with <see cref="Formatter.Annotation"/>
    /// </summary>
    /// <param name="document">The document changed by the <see cref="CodeAction"/>.</param>
    /// <param name="cancellationToken">A cancellation token.</param>
    /// <returns>A document with the post processing changes applied.</returns>
    protected virtual async Task<Document> PostProcessChangesAsync(Document document, CancellationToken cancellationToken)
    {
        if (document.SupportsSyntaxTree)
        {
            var options = await document.GetCodeCleanupOptionsAsync(cancellationToken).ConfigureAwait(false);
            return await CleanupDocumentAsync(document, options, cancellationToken).ConfigureAwait(false);
        }
 
        return document;
    }
 
    #region Factories for standard code actions
 
    /// <summary>
    /// Creates a <see cref="CodeAction"/> for a change to a single <see cref="Document"/>.
    /// Use this factory when the change is expensive to compute and should be deferred until requested.
    /// </summary>
    /// <param name="title">Title of the <see cref="CodeAction"/>.</param>
    /// <param name="createChangedDocument">Function to create the <see cref="Document"/>.</param>
    /// <param name="equivalenceKey">Optional value used to determine the equivalence of the <see cref="CodeAction"/> with other <see cref="CodeAction"/>s. See <see cref="CodeAction.EquivalenceKey"/>.</param>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public static CodeAction Create(string title, Func<CancellationToken, Task<Document>> createChangedDocument, string? equivalenceKey)
        => Create(title, (_, c) => createChangedDocument(c), equivalenceKey);
 
    [EditorBrowsable(EditorBrowsableState.Never)]
    internal static CodeAction Create(string title, Func<IProgress<CodeAnalysisProgress>, CancellationToken, Task<Document>> createChangedDocument, string? equivalenceKey)
        => Create(title, createChangedDocument, equivalenceKey, CodeActionPriority.Default);
 
    /// <inheritdoc cref="Create(string, Func{CancellationToken, Task{Document}}, string?)"/>
    /// <param name="priority">Code action priority</param>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "This is source compatible")]
    public static CodeAction Create(string title, Func<CancellationToken, Task<Document>> createChangedDocument, string? equivalenceKey = null, CodeActionPriority priority = CodeActionPriority.Default)
        => Create(title, (_, c) => createChangedDocument(c), equivalenceKey, priority);
 
    /// <inheritdoc cref="Create(string, Func{CancellationToken, Task{Document}}, string?, CodeActionPriority)"/>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "This is source compatible")]
    public static CodeAction Create(string title, Func<IProgress<CodeAnalysisProgress>, CancellationToken, Task<Document>> createChangedDocument, string? equivalenceKey = null, CodeActionPriority priority = CodeActionPriority.Default)
    {
        if (title == null)
            throw new ArgumentNullException(nameof(title));
 
        if (createChangedDocument == null)
            throw new ArgumentNullException(nameof(createChangedDocument));
 
        return DocumentChangeAction.New(title, createChangedDocument, equivalenceKey, priority);
    }
 
    /// <summary>
    /// Creates a <see cref="CodeAction"/> for a change to more than one <see cref="Document"/> within a <see cref="Solution"/>.
    /// Use this factory when the change is expensive to compute and should be deferred until requested.
    /// </summary>
    /// <param name="title">Title of the <see cref="CodeAction"/>.</param>
    /// <param name="createChangedSolution">Function to create the <see cref="Solution"/>.</param>
    /// <param name="equivalenceKey">Optional value used to determine the equivalence of the <see cref="CodeAction"/> with other <see cref="CodeAction"/>s. See <see cref="CodeAction.EquivalenceKey"/>.</param>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public static CodeAction Create(string title, Func<CancellationToken, Task<Solution>> createChangedSolution, string? equivalenceKey)
        => Create(title, createChangedSolution, equivalenceKey, CodeActionPriority.Default);
 
    /// <summary>
    /// Creates a <see cref="CodeAction"/> for a change to more than one <see cref="Document"/> within a <see cref="Solution"/>.
    /// Use this factory when the change is expensive to compute and should be deferred until requested.
    /// </summary>
    /// <param name="title">Title of the <see cref="CodeAction"/>.</param>
    /// <param name="createChangedSolution">Function to create the <see cref="Solution"/>.</param>
    /// <param name="equivalenceKey">Optional value used to determine the equivalence of the <see cref="CodeAction"/> with other <see cref="CodeAction"/>s. See <see cref="CodeAction.EquivalenceKey"/>.</param>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "This is source compatible")]
    public static CodeAction Create(string title, Func<CancellationToken, Task<Solution>> createChangedSolution, string? equivalenceKey = null, CodeActionPriority priority = CodeActionPriority.Default)
        => Create(title, (_, c) => createChangedSolution(c), equivalenceKey, priority);
 
    /// <inheritdoc cref="Create(string, Func{CancellationToken, Task{Solution}}, string?, CodeActionPriority)"/>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "This is source compatible")]
    public static CodeAction Create(string title, Func<IProgress<CodeAnalysisProgress>, CancellationToken, Task<Solution>> createChangedSolution, string? equivalenceKey = null, CodeActionPriority priority = CodeActionPriority.Default)
    {
        if (title == null)
            throw new ArgumentNullException(nameof(title));
 
        if (createChangedSolution == null)
            throw new ArgumentNullException(nameof(createChangedSolution));
 
        return SolutionChangeAction.New(title, createChangedSolution, equivalenceKey, priority);
    }
 
    /// <summary>
    /// Creates a <see cref="CodeAction"/> representing a group of code actions.
    /// </summary>
    /// <param name="title">Title of the <see cref="CodeAction"/> group.</param>
    /// <param name="nestedActions">The code actions within the group.</param>
    /// <param name="isInlinable"><see langword="true"/> to allow inlining the members of the group into the parent;
    /// otherwise, <see langword="false"/> to require that this group appear as a group with nested actions.</param>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public static CodeAction Create(string title, ImmutableArray<CodeAction> nestedActions, bool isInlinable)
        => Create(title, nestedActions, isInlinable, priority: CodeActionPriority.Default);
 
    /// <inheritdoc cref="Create(string, ImmutableArray{CodeAction}, bool)"/>
    /// <param name="priority">Priority of the code action</param>
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "This is source compatible")]
    public static CodeAction Create(string title, ImmutableArray<CodeAction> nestedActions, bool isInlinable, CodeActionPriority priority = CodeActionPriority.Default)
    {
        if (title is null)
            throw new ArgumentNullException(nameof(title));
 
        if (nestedActions == null)
            throw new ArgumentNullException(nameof(nestedActions));
 
        return CodeActionWithNestedActions.Create(title, nestedActions, isInlinable, priority);
    }
 
    internal abstract class SimpleCodeAction(
        string title,
        string? equivalenceKey,
        CodeActionPriority priority,
        bool createdFromFactoryMethod) : CodeAction
    {
        public sealed override string Title { get; } = title;
        public sealed override string? EquivalenceKey { get; } = equivalenceKey;
 
        protected sealed override CodeActionPriority ComputePriority()
            => priority;
 
        /// <summary>
        /// Indicates if this CodeAction was created using one of the 'CodeAction.Create' factory methods.
        /// This is used in <see cref="GetTelemetryId(FixAllScope?)"/> to determine the appropriate type
        /// name to log in the CodeAction telemetry.
        /// </summary>
        public bool CreatedFromFactoryMethod { get; } = createdFromFactoryMethod;
    }
 
    internal class CodeActionWithNestedActions : SimpleCodeAction
    {
        private CodeActionWithNestedActions(
            string title,
            ImmutableArray<CodeAction> nestedActions,
            bool isInlinable,
            CodeActionPriority priority,
            bool createdFromFactoryMethod)
            : base(title, ComputeEquivalenceKey(nestedActions), priority, createdFromFactoryMethod)
        {
            Debug.Assert(nestedActions.Length > 0);
            NestedActions = nestedActions;
            IsInlinable = isInlinable;
        }
 
        protected CodeActionWithNestedActions(
           string title,
           ImmutableArray<CodeAction> nestedActions,
           bool isInlinable,
           CodeActionPriority priority = CodeActionPriority.Default)
           : this(title, nestedActions, isInlinable, priority, createdFromFactoryMethod: false)
        {
        }
 
        public static new CodeActionWithNestedActions Create(
           string title,
           ImmutableArray<CodeAction> nestedActions,
           bool isInlinable,
           CodeActionPriority priority = CodeActionPriority.Default)
            => new(title, nestedActions, isInlinable, priority, createdFromFactoryMethod: true);
 
        public sealed override bool IsInlinable { get; }
 
        public sealed override ImmutableArray<CodeAction> NestedActions { get; }
 
        private static string? ComputeEquivalenceKey(ImmutableArray<CodeAction> nestedActions)
        {
            var equivalenceKey = StringBuilderPool.Allocate();
            try
            {
                foreach (var action in nestedActions)
                {
                    equivalenceKey.Append((action.EquivalenceKey ?? action.GetHashCode().ToString()) + ";");
                }
 
                return equivalenceKey.Length > 0 ? equivalenceKey.ToString() : null;
            }
            finally
            {
                StringBuilderPool.ReturnAndFree(equivalenceKey);
            }
        }
    }
 
    internal class DocumentChangeAction : SimpleCodeAction
    {
        private readonly Func<IProgress<CodeAnalysisProgress>, CancellationToken, Task<Document>> _createChangedDocument;
 
        private DocumentChangeAction(
            string title,
            Func<IProgress<CodeAnalysisProgress>, CancellationToken, Task<Document>> createChangedDocument,
            string? equivalenceKey,
            CodeActionPriority priority,
            bool createdFromFactoryMethod)
            : base(title, equivalenceKey, priority, createdFromFactoryMethod)
        {
            _createChangedDocument = createChangedDocument;
        }
 
        protected DocumentChangeAction(
            string title,
            Func<IProgress<CodeAnalysisProgress>, CancellationToken, Task<Document>> createChangedDocument,
            string? equivalenceKey,
            CodeActionPriority priority = CodeActionPriority.Default)
            : this(title, createChangedDocument, equivalenceKey, priority, createdFromFactoryMethod: false)
        {
        }
 
        public static DocumentChangeAction New(
            string title,
            Func<IProgress<CodeAnalysisProgress>, CancellationToken, Task<Document>> createChangedDocument,
            string? equivalenceKey,
            CodeActionPriority priority = CodeActionPriority.Default)
            => new(title, createChangedDocument, equivalenceKey, priority, createdFromFactoryMethod: true);
 
        protected sealed override Task<Document> GetChangedDocumentAsync(IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
            => _createChangedDocument(progress, cancellationToken);
    }
 
    internal class SolutionChangeAction : SimpleCodeAction
    {
        private readonly Func<IProgress<CodeAnalysisProgress>, CancellationToken, Task<Solution>> _createChangedSolution;
 
        protected SolutionChangeAction(
            string title,
            Func<IProgress<CodeAnalysisProgress>, CancellationToken, Task<Solution>> createChangedSolution,
            string? equivalenceKey,
            CodeActionPriority priority,
            bool createdFromFactoryMethod)
            : base(title, equivalenceKey, priority, createdFromFactoryMethod)
        {
            _createChangedSolution = createChangedSolution;
        }
 
        protected SolutionChangeAction(
            string title,
            Func<IProgress<CodeAnalysisProgress>, CancellationToken, Task<Solution>> createChangedSolution,
            string? equivalenceKey,
            CodeActionPriority priority = CodeActionPriority.Default)
            : this(title, createChangedSolution, equivalenceKey, priority, createdFromFactoryMethod: false)
        {
        }
 
        public static SolutionChangeAction New(
            string title,
            Func<IProgress<CodeAnalysisProgress>, CancellationToken, Task<Solution>> createChangedSolution,
            string? equivalenceKey,
            CodeActionPriority priority = CodeActionPriority.Default)
            => new(title, createChangedSolution, equivalenceKey, priority, createdFromFactoryMethod: true);
 
        protected sealed override Task<Solution?> GetChangedSolutionAsync(IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
            => _createChangedSolution(progress, cancellationToken).AsNullable();
    }
 
    internal sealed class NoChangeAction : SimpleCodeAction
    {
        private NoChangeAction(
            string title,
            string? equivalenceKey,
            CodeActionPriority priority,
            bool createdFromFactoryMethod)
            : base(title, equivalenceKey, priority, createdFromFactoryMethod)
        {
        }
 
        public static NoChangeAction Create(
            string title,
            string? equivalenceKey,
            CodeActionPriority priority = CodeActionPriority.Default)
            => new(title, equivalenceKey, priority, createdFromFactoryMethod: true);
 
        protected sealed override Task<Solution?> GetChangedSolutionAsync(IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
             => SpecializedTasks.Null<Solution>();
    }
 
    #endregion
}