// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CodeFixes.Suppression { internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider { internal abstract partial class RemoveSuppressionCodeAction { public static BatchFixAllProvider GetBatchFixer(AbstractSuppressionCodeFixProvider suppressionFixProvider) { return new BatchFixer(suppressionFixProvider); } /// /// Batch fixer for pragma suppression removal code action. /// private sealed class BatchFixer : BatchFixAllProvider { private readonly AbstractSuppressionCodeFixProvider _suppressionFixProvider; public BatchFixer(AbstractSuppressionCodeFixProvider suppressionFixProvider) { _suppressionFixProvider = suppressionFixProvider; } protected override async Task AddDocumentFixesAsync( Document document, ImmutableArray diagnostics, ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> fixes, FixAllState fixAllState, CancellationToken cancellationToken) { // Batch all the pragma remove suppression fixes by executing them sequentially for the document. var pragmaActionsBuilder = ArrayBuilder.GetInstance(); var pragmaDiagnosticsBuilder = ArrayBuilder.GetInstance(); foreach (var diagnostic in diagnostics.Where(d => d.Location.IsInSource && d.IsSuppressed)) { var span = diagnostic.Location.SourceSpan; var removeSuppressionFixes = await _suppressionFixProvider.GetFixesAsync( document, span, SpecializedCollections.SingletonEnumerable(diagnostic), cancellationToken).ConfigureAwait(false); var removeSuppressionFix = removeSuppressionFixes.SingleOrDefault(); if (removeSuppressionFix != null) { if (removeSuppressionFix.Action is RemoveSuppressionCodeAction codeAction) { if (fixAllState.IsFixMultiple) { codeAction = codeAction.CloneForFixMultipleContext(); } if (codeAction is PragmaRemoveAction pragmaRemoveAction) { pragmaActionsBuilder.Add(pragmaRemoveAction); pragmaDiagnosticsBuilder.Add(diagnostic); } else { fixes.Add((diagnostic, codeAction)); } } } } // Get the pragma batch fix. if (pragmaActionsBuilder.Count > 0) { var pragmaBatchFix = PragmaBatchFixHelpers.CreateBatchPragmaFix( _suppressionFixProvider, document, pragmaActionsBuilder.ToImmutableAndFree(), pragmaDiagnosticsBuilder.ToImmutableAndFree(), fixAllState, cancellationToken); fixes.Add((diagnostic: null, pragmaBatchFix)); } } protected async override Task AddProjectFixesAsync( Project project, ImmutableArray diagnostics, ConcurrentBag<(Diagnostic diagnostic, CodeAction action)> bag, FixAllState fixAllState, CancellationToken cancellationToken) { foreach (var diagnostic in diagnostics.Where(d => !d.Location.IsInSource && d.IsSuppressed)) { var removeSuppressionFixes = await _suppressionFixProvider.GetFixesAsync( project, SpecializedCollections.SingletonEnumerable(diagnostic), cancellationToken).ConfigureAwait(false); if (removeSuppressionFixes.SingleOrDefault()?.Action is RemoveSuppressionCodeAction removeSuppressionCodeAction) { if (fixAllState.IsFixMultiple) { removeSuppressionCodeAction = removeSuppressionCodeAction.CloneForFixMultipleContext(); } bag.Add((diagnostic, removeSuppressionCodeAction)); } } } public override async Task TryGetMergedFixAsync( ImmutableArray<(Diagnostic diagnostic, CodeAction action)> batchOfFixes, FixAllState fixAllState, CancellationToken cancellationToken) { // Batch all the attribute removal fixes into a single fix. // Pragma removal fixes have already been batch for each document AddDocumentFixes method. // This ensures no merge conflicts in merging all fixes by our base implementation. var oldSolution = fixAllState.Project.Solution; var currentSolution = oldSolution; var attributeRemoveFixes = new List(); var newBatchOfFixes = new List<(Diagnostic diagnostic, CodeAction action)>(); foreach (var codeAction in batchOfFixes) { if (codeAction.action is AttributeRemoveAction attributeRemoveFix) { attributeRemoveFixes.Add(attributeRemoveFix); } else { newBatchOfFixes.Add(codeAction); } } if (attributeRemoveFixes.Count > 0) { // Batch all of attribute removal fixes. foreach (var removeSuppressionFixesForTree in attributeRemoveFixes.GroupBy(fix => fix.SyntaxTreeToModify)) { var tree = removeSuppressionFixesForTree.Key; var attributeRemoveFixesForTree = removeSuppressionFixesForTree.OfType().ToImmutableArray(); var attributesToRemove = await GetAttributeNodesToFixAsync(attributeRemoveFixesForTree, cancellationToken).ConfigureAwait(false); var document = oldSolution.GetDocument(tree); var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var newRoot = root.RemoveNodes(attributesToRemove, SyntaxRemoveOptions.KeepLeadingTrivia | SyntaxRemoveOptions.AddElasticMarker); currentSolution = currentSolution.WithDocumentSyntaxRoot(document.Id, newRoot); } // This is a temporary generated code action, which doesn't need telemetry, hence suppressing RS0005. #pragma warning disable RS0005 // Do not use generic CodeAction.Create to create CodeAction var batchAttributeRemoveFix = Create( attributeRemoveFixes.First().Title, createChangedSolution: ct => Task.FromResult(currentSolution), equivalenceKey: fixAllState.CodeActionEquivalenceKey); #pragma warning restore RS0005 // Do not use generic CodeAction.Create to create CodeAction newBatchOfFixes.Insert(0, (diagnostic: null, batchAttributeRemoveFix)); } return await base.TryGetMergedFixAsync( newBatchOfFixes.ToImmutableArray(), fixAllState, cancellationToken).ConfigureAwait(false); } private static async Task> GetAttributeNodesToFixAsync(ImmutableArray attributeRemoveFixes, CancellationToken cancellationToken) { using var builder = ArrayBuilder.GetInstance(attributeRemoveFixes.Length); foreach (var attributeRemoveFix in attributeRemoveFixes) { var attributeToRemove = await attributeRemoveFix.GetAttributeToRemoveAsync(cancellationToken).ConfigureAwait(false); builder.Add(attributeToRemove); } return builder.ToImmutable(); } } } } }