File: src\Workspaces\SharedUtilitiesAndExtensions\Workspace\Core\CodeFixes\MultiProjectSafeFixAllProvider.cs
Web Access
Project: src\src\RoslynAnalyzers\Text.Analyzers\Core\Text.Analyzers.csproj (Text.Analyzers)
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
 
/// <summary>
/// A FixAllProvider that can safely fix diagnostics across multiple projects by only applying fixes that are reported
/// in all the projects a linked document is found in.  This way, if a fix is only valid in some projects but not others,
/// it will not be applied during fix-all
/// </summary>
internal abstract class MultiProjectSafeFixAllProvider : FixAllProvider
{
    protected abstract void FixAll(SyntaxEditor editor, IEnumerable<TextSpan> commonSpans);
 
    public sealed override async Task<CodeAction?> GetFixAsync(FixAllContext fixAllContext)
    {
        var cancellationToken = fixAllContext.CancellationToken;
 
        var documentToDiagnostics = await FixAllContextHelper.GetDocumentDiagnosticsToFixAsync(fixAllContext).ConfigureAwait(false);
 
        // Note: we can only do this if we're doing a fix-all in the solution level.  That's the only way we can see
        // the diagnostics for other linked documents.  If someone is just asking to fix in a project we'll only
        // know about that project and thus can't make the right decision.
        var filterBasedOnScope = fixAllContext.Scope == FixAllScope.Solution;
 
        // Map from a document to all linked documents it has (not including itself).
        using var _ = PooledDictionary<DocumentId, ImmutableArray<DocumentId>>.GetInstance(out var documentToLinkedDocuments);
 
        PopulateLinkedDocumentMap();
        var updatedSolution = await ProcessLinkedDocumentMapAsync().ConfigureAwait(false);
 
        return CodeAction.Create(
            fixAllContext.GetDefaultFixAllTitle(),
            (_, _) => Task.FromResult(updatedSolution),
            equivalenceKey: null,
            CodeActionPriority.Default
#if WORKSPACE
            , this.Cleanup
#endif
            );
 
        void PopulateLinkedDocumentMap()
        {
            var solution = fixAllContext.Solution;
            foreach (var (document, _) in documentToDiagnostics)
            {
                // Note: GetLinkedDocuments does not return the document it was called on.
                var linkedDocuments = document.GetLinkedDocumentIds();
 
                // Ignore any linked documents we already saw by processing another document in that linked set.
                if (linkedDocuments.Any(id => documentToLinkedDocuments.ContainsKey(id)))
                    continue;
 
                documentToLinkedDocuments[document.Id] = linkedDocuments;
            }
        }
 
        async Task<Solution> ProcessLinkedDocumentMapAsync()
        {
            var currentSolution = fixAllContext.Solution;
 
            foreach (var (documentId, linkedDocumentIds) in documentToLinkedDocuments)
            {
                // Now, for each group of linked documents, only remove the suppression operators we see in all documents.
                var document = fixAllContext.Solution.GetRequiredDocument(documentId);
                using var _ = PooledHashSet<TextSpan>.GetInstance(out var commonSpans);
 
                var diagnostics = documentToDiagnostics[document];
 
                // Start initially with all the spans in this document.
                commonSpans.UnionWith(GetDiagnosticSpans(diagnostics));
 
                // Now, only keep those spans that are also in all other linked documents.
                if (filterBasedOnScope)
                {
                    foreach (var linkedDocumentId in linkedDocumentIds)
                    {
                        var linkedDocument = fixAllContext.Solution.GetRequiredDocument(linkedDocumentId);
                        var linkedDiagnostics = documentToDiagnostics.TryGetValue(linkedDocument, out var result) ? result : [];
 
                        commonSpans.IntersectWith(GetDiagnosticSpans(linkedDiagnostics));
                    }
                }
 
                // Now process the common spans on this initial document.  Note: we don't need to bother updating
                // the linked documents since, by definition, they will get the same changes.  And the workspace
                // will automatically edit all linked files when making a change to only one of them.
                var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
                var newRoot = FixAll(fixAllContext.Solution.Services, root, commonSpans);
 
                currentSolution = currentSolution.WithDocumentSyntaxRoot(documentId, newRoot);
            }
 
            return currentSolution;
        }
 
        static IEnumerable<TextSpan> GetDiagnosticSpans(ImmutableArray<Diagnostic> diagnostics)
            => diagnostics.Select(static d => d.AdditionalLocations[0].SourceSpan);
    }
 
    private SyntaxNode FixAll(SolutionServices services, SyntaxNode root, PooledHashSet<TextSpan> commonSpans)
    {
        var editor = new SyntaxEditor(root, services);
        FixAll(editor, commonSpans);
        return editor.GetChangedRoot();
    }
}