File: Microsoft.NetCore.Analyzers\Usage\DoNotPassNonNullableValueToArgumentNullExceptionThrowIfNull.Fixer.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.NetAnalyzers;

namespace Microsoft.NetCore.Analyzers.Usage
{
    public abstract class DoNotPassNonNullableValueToArgumentNullExceptionThrowIfNullFixer<TInvocationExpression> : CodeFixProvider
        where TInvocationExpression : SyntaxNode
    {
        protected const string HasValue = nameof(Nullable<int>.HasValue);
        protected const string ArgumentNullException = nameof(System.ArgumentNullException);

        public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(
            DoNotPassNonNullableValueToArgumentNullExceptionThrowIfNull.NonNullableValueRuleId,
            DoNotPassNonNullableValueToArgumentNullExceptionThrowIfNull.NullableStructRuleId
        );

        // One title per rule, so a fix-all pass has to apply only the one the user invoked it from.
        public override FixAllProvider GetFixAllProvider()
            => SyntaxEditorFixAllProvider.Create<string?>(
                static fixAllContext => fixAllContext.CodeActionEquivalenceKey,
                (document, diagnostic, editor, equivalenceKey, cancellationToken) =>
                {
                    ApplyFix(diagnostic, editor, equivalenceKey);
                    return Task.CompletedTask;
                });

        public override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            SyntaxNode root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

            if (root.FindNode(context.Span, getInnermostNodeForTie: true) is not TInvocationExpression { Parent: not null })
            {
                return;
            }

            Document document = context.Document;

            foreach (Diagnostic diagnostic in context.Diagnostics)
            {
                if (GetTitle(diagnostic.Id) is not string title)
                {
                    continue;
                }

                ImmutableArray<Diagnostic> diagnostics = ImmutableArray.Create(diagnostic);

                context.RegisterCodeFix(
                    CodeAction.Create(
                        title,
                        cancellationToken => SyntaxEditorFixAllProvider.ApplyFixesAsync(
                            document,
                            diagnostics,
                            (_, diagnostic, editor, _) =>
                            {
                                ApplyFix(diagnostic, editor, title);
                                return Task.CompletedTask;
                            },
                            cancellationToken),
                        equivalenceKey: title),
                    diagnostic);
            }
        }

        private void ApplyFix(Diagnostic diagnostic, SyntaxEditor editor, string? equivalenceKey)
        {
            if (equivalenceKey is not null && equivalenceKey != GetTitle(diagnostic.Id))
            {
                return;
            }

            // Both fixes target the statement the call sits in, and a `ThrowIfNull` call returns void, so no
            // two of this rule's diagnostics can nest and the statement needs no re-reading.
            if (editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) is not TInvocationExpression { Parent: SyntaxNode statement } invocation)
            {
                return;
            }

            if (diagnostic.Id == DoNotPassNonNullableValueToArgumentNullExceptionThrowIfNull.NonNullableValueRuleId)
            {
                editor.RemoveNode(statement, SyntaxRemoveOptions.KeepNoTrivia);
            }
            else
            {
                ReplaceWithNullableStructCheck(invocation, statement, editor);
            }
        }

        private static string? GetTitle(string ruleId) => ruleId switch
        {
            DoNotPassNonNullableValueToArgumentNullExceptionThrowIfNull.NonNullableValueRuleId => MicrosoftNetCoreAnalyzersResources.DoNotPassNonNullableValueToArgumentNullExceptionThrowIfNullCodeFixTitle,
            DoNotPassNonNullableValueToArgumentNullExceptionThrowIfNull.NullableStructRuleId => MicrosoftNetCoreAnalyzersResources.DoNotPassNullableStructToArgumentNullExceptionThrowIfNullCodeFixTitle,
            _ => null,
        };

        /// <summary>
        /// Replaces <paramref name="statement"/> — the statement <paramref name="invocation"/> sits in — with an
        /// explicit <c>HasValue</c> check that throws.
        /// </summary>
        protected abstract void ReplaceWithNullableStructCheck(TInvocationExpression invocation, SyntaxNode statement, SyntaxEditor editor);
    }
}