File: src\Analyzers\CSharp\CodeFixes\RemoveUnnecessarySuppressions\CSharpRemoveUnnecessaryNullableWarningSuppressionsCodeFixProvider.cs
Web Access
Project: src\src\CodeStyle\CSharp\CodeFixes\Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes)
// 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.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessarySuppressions;
 
[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.RemoveUnnecessaryNullableWarningSuppressions), Shared]
[method: ImportingConstructor]
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
internal sealed class CSharpRemoveUnnecessaryNullableWarningSuppressionsCodeFixProvider() : CodeFixProvider
{
    public override ImmutableArray<string> FixableDiagnosticIds => [IDEDiagnosticIds.RemoveUnnecessaryNullableWarningSuppression];
 
    public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        context.RegisterCodeFix(CodeAction.Create(
            AnalyzersResources.Remove_unnecessary_suppression,
            cancellationToken => FixAllAsync(context.Document, context.Diagnostics, cancellationToken),
            nameof(AnalyzersResources.Remove_unnecessary_suppression)),
            context.Diagnostics);
 
        return Task.CompletedTask;
    }
 
    private static async Task<Document> FixAllAsync(Document document, ImmutableArray<Diagnostic> diagnostics, CancellationToken cancellationToken)
    {
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
        var newRoot = FixAll(
            document.Project.Solution.Services, root, diagnostics.Select(static d => d.AdditionalLocations[0].SourceSpan));
 
        return document.WithSyntaxRoot(newRoot);
    }
 
    private static SyntaxNode FixAll(
        SolutionServices services,
        SyntaxNode root,
        IEnumerable<TextSpan> spans)
    {
        var editor = new SyntaxEditor(root, services);
 
        foreach (var span in spans.OrderByDescending(d => d.Start))
        {
            if (root.FindNode(span, getInnermostNodeForTie: true) is PostfixUnaryExpressionSyntax unaryExpression)
            {
                editor.ReplaceNode(
                    unaryExpression,
                    (current, _) => ((PostfixUnaryExpressionSyntax)current).Operand.WithTriviaFrom(current));
            }
        }
 
        return editor.GetChangedRoot();
    }
 
    public override FixAllProvider? GetFixAllProvider()
        => new RemoveUnnecessaryNullableWarningSuppressionsFixAllProvider();
 
    private sealed class RemoveUnnecessaryNullableWarningSuppressionsFixAllProvider : FixAllProvider
    {
#if !CODE_STYLE
        internal override CodeActionCleanup Cleanup => CodeActionCleanup.SyntaxOnly;
#endif
 
        public override async Task<CodeAction?> GetFixAsync(FixAllContext fixAllContext)
        {
            var cancellationToken = fixAllContext.CancellationToken;
 
            // Fix-all for removing unnecessary `!` operators works in a fairly specialized fashion.  The core problem
            // is that it's normal to have situations where a `!` operator is unnecessary in one linked document in one
            // project, but necessary in another.  Consider something as mundane as `string.IsNullOrEmpty(s)`.  In
            // projects that reference a modern, annotated, BCL, the nullable attributes on this method will allow the
            // compiler to determine that `s` is non-null after the call, allowing superfluous `!` operators to be
            // removed.  However, in projects that reference an unannotated BCL, no such determination can be made, and
            // the `!` on a following statement may be necessary.
            //
            // To deal with this, we consider all linked documents together.  If a `!` operator is unnecessary in *all*
            // linked documents, then we can remove it.  Otherwise, we must keep it.
 
            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 !CODE_STYLE
                , 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);
        }
    }
}