File: CodeFixesAndRefactorings\DocumentBasedFixAllProviderHelpers.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.Immutable;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CodeFixesAndRefactorings;
 
/// <summary>
/// Helper methods for DocumentBasedFixAllProvider common to code fixes and refactorings.
/// </summary>
internal static class DocumentBasedFixAllProviderHelpers
{
    public static async Task<Solution?> FixAllContextsAsync<TFixAllContext>(
        TFixAllContext originalFixAllContext,
        ImmutableArray<TFixAllContext> fixAllContexts,
        IProgress<CodeAnalysisProgress> progressTracker,
        string progressTrackerDescription,
        Func<TFixAllContext, Func<Document, Document?, ValueTask>, Task> getFixedDocumentsAsync)
        where TFixAllContext : IFixAllContext
    {
        var cancellationToken = originalFixAllContext.CancellationToken;
 
        progressTracker.Report(CodeAnalysisProgress.Description(progressTrackerDescription));
 
        var originalSolution = originalFixAllContext.Solution;
 
        // One work item for each context.
        progressTracker.AddItems(fixAllContexts.Length);
 
        // Do the initial pass to fixup documents.
        var dirtySolution = await GetInitialUncleanedSolutionAsync(originalSolution).ConfigureAwait(false);
 
        // Now do a pass to clean the fixed documents.
        progressTracker.Report(CodeAnalysisProgress.Clear());
        progressTracker.Report(CodeAnalysisProgress.Description(WorkspacesResources.Running_code_cleanup_on_fixed_documents));
 
        var cleanedSolution = await CodeAction.CleanSyntaxAndSemanticsAsync(
            originalSolution,
            dirtySolution,
            progressTracker,
            cancellationToken).ConfigureAwait(false);
 
        // Once we clean the document, we get the text of it and insert that back into the final solution.  This way we
        // can release both the original fixed tree, and the cleaned tree (both of which can be much more expensive than
        // just text).
        var cleanedTexts = await CodeAction.GetAllChangedOrAddedDocumentIds(originalSolution, cleanedSolution)
            .SelectAsArrayAsync(async documentId => (documentId, await cleanedSolution.GetRequiredDocument(documentId).GetTextAsync(cancellationToken).ConfigureAwait(false)))
            .ConfigureAwait(false);
 
        var finalSolution = cleanedSolution.WithDocumentTexts(cleanedTexts);
        return finalSolution;
 
        async Task<Solution> GetInitialUncleanedSolutionAsync(Solution originalSolution)
        {
            // First, iterate over all contexts, and collect all the changes for each of them.  We'll be making a lot of
            // calls to the remote server to compute diagnostics and changes.  So keep a single connection alive to it
            // so we never resync or recompute anything.
            using var _ = await RemoteKeepAliveSession.CreateAsync(originalSolution, cancellationToken).ConfigureAwait(false);
 
            var changedRootsAndTexts = await ProducerConsumer<(DocumentId documentId, (SyntaxNode? node, SourceText? text))>.RunParallelAsync(
                source: fixAllContexts,
                produceItems: static async (fixAllContext, callback, args, cancellationToken) =>
                {
                    var (getFixedDocumentsAsync, progressTracker) = args;
 
                    // Update our progress for each fixAllContext we process.
                    using var _ = progressTracker.ItemCompletedScope();
 
                    Contract.ThrowIfFalse(
                        fixAllContext.Scope is FixAllScope.Document or FixAllScope.Project or FixAllScope.ContainingMember or FixAllScope.ContainingType);
 
                    // Defer to the FixAllProvider to actually compute each fixed document.
                    await getFixedDocumentsAsync(
                        fixAllContext,
                        async (originalDocument, newDocument) =>
                        {
                            if (newDocument == null || newDocument == originalDocument)
                                return;
 
                            var newRoot = newDocument.SupportsSyntaxTree ? await newDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false) : null;
                            var newText = newDocument.SupportsSyntaxTree ? null : await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
                            callback((newDocument.Id, (newRoot, newText)));
                        }).ConfigureAwait(false);
                },
                args: (getFixedDocumentsAsync, progressTracker),
                cancellationToken).ConfigureAwait(false);
 
            // Next, go and insert those all into the solution so all the docs in this particular project point
            // at the new trees (or text).  At this point though, the trees have not been semantically cleaned
            // up. We don't cleanup the documents as they are created, or one at a time as we add them, as that
            // would cause us to run semantic cleanup on N different solution forks (which would be very
            // expensive as we'd fork, produce semantics, fork, produce semantics, etc. etc.). Instead, by
            // adding all the changed documents to one solution, and then cleaning *those* we only perform
            // cleanup semantics on one forked solution.
            var changedRoots = changedRootsAndTexts.SelectAsArray(t => t.Item2.node != null, t => (t.documentId, t.Item2.node!));
            var changedTexts = changedRootsAndTexts.SelectAsArray(t => t.Item2.text != null, t => (t.documentId, t.Item2.text!));
 
            return originalSolution
                .WithDocumentSyntaxRoots(changedRoots)
                .WithDocumentTexts(changedTexts);
        }
    }
}