// 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(); } } |