// 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.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; 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 async Task RegisterCodeFixesAsync(CodeFixContext context) { context.RegisterCodeFix(CodeAction.Create( AnalyzersResources.Remove_unnecessary_suppression, cancellationToken => FixSingleDocumentAsync(context.Document, context.Diagnostics, cancellationToken), nameof(AnalyzersResources.Remove_unnecessary_suppression)), context.Diagnostics); } private static async Task<Document> FixSingleDocumentAsync(Document document, ImmutableArray<Diagnostic> diagnostics, CancellationToken cancellationToken) { var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); // Keep track of the unnecessary spans in all linked documents. If we're trying to fix something that would // cause a problem in another linked document, then we'll still fix it (since the analyzer did report it), we'll // just report a warning on it letting the user know in the preview window. using var _1 = ArrayBuilder<HashSet<TextSpan>>.GetInstance(out var linkedSpansArray); foreach (var linkedDocument in document.GetLinkedDocuments()) { using var _2 = ArrayBuilder<PostfixUnaryExpressionSyntax>.GetInstance(out var unnecessaryNodes); var linkedSemanticModel = await linkedDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); UnnecessaryNullableWarningSuppressionsUtilities.AddUnnecessaryNodes( linkedSemanticModel, unnecessaryNodes, cancellationToken); linkedSpansArray.Add([.. unnecessaryNodes.Select(n => n.Span)]); } var editor = new SyntaxEditor(root, document.Project.Solution.Services); FixAllInDocument( editor, diagnostics.Select(static d => d.AdditionalLocations[0].SourceSpan), expr => { // If we don't see this span in any of the linked documents, then that means analyzing that document // didn't reveal this suppression as unnecessary. Therefore, we need to add a warning annotation // message to inform the user that removing this suppression may cause warnings in other projects. foreach (var linkedSpans in linkedSpansArray) { if (!linkedSpans.Contains(expr.Span)) return WarningAnnotation.Create(CodeFixesResources.May_be_neccessary_in_other_project_this_document_is_linked_in); } return null; }); return document.WithSyntaxRoot(editor.GetChangedRoot()); } private static void FixAllInDocument( SyntaxEditor editor, IEnumerable<TextSpan> spans, Func<PostfixUnaryExpressionSyntax, SyntaxAnnotation?>? getAnnotation) { var root = editor.OriginalRoot; foreach (var span in spans.OrderByDescending(d => d.Start)) { if (root.FindNode(span, getInnermostNodeForTie: true) is PostfixUnaryExpressionSyntax unaryExpression) { var annotation = getAnnotation?.Invoke(unaryExpression); editor.ReplaceNode( unaryExpression, (current, _) => { var result = ((PostfixUnaryExpressionSyntax)current).Operand.WithTriviaFrom(current); return annotation != null ? result.WithAdditionalAnnotations(annotation) : result; }); } } } public override FixAllProvider? GetFixAllProvider() => new RemoveUnnecessaryNullableWarningSuppressionsFixAllProvider(); /// <summary> /// 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. /// </summary> private sealed class RemoveUnnecessaryNullableWarningSuppressionsFixAllProvider : MultiProjectSafeFixAllProvider { #if !CODE_STYLE internal override CodeActionCleanup Cleanup => CodeActionCleanup.SyntaxOnly; #endif protected override void FixAll(SyntaxEditor editor, IEnumerable<TextSpan> commonSpans) => FixAllInDocument(editor, commonSpans, getAnnotation: null); } }