File: Microsoft.NetCore.Analyzers\Performance\PreferConvertToHexStringOverBitConverter.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.Composition;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.NetAnalyzers;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.NetCore.Analyzers.Performance
{
    using static MicrosoftNetCoreAnalyzersResources;

    /// <summary>
    /// CA1872: <inheritdoc cref="PreferConvertToHexStringOverBitConverterTitle"/>
    /// </summary>
    [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared]
    public sealed class PreferConvertToHexStringOverBitConverterFixer : SyntaxEditorBasedCodeFixProvider
    {
        private static readonly SyntaxAnnotation s_asSpanSymbolAnnotation = new("SymbolId", WellKnownTypeNames.SystemMemoryExtensions);

        public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
            ImmutableArray.Create(PreferConvertToHexStringOverBitConverterAnalyzer.RuleId);

        public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var diagnostic = context.Diagnostics.FirstOrDefault();

            if (diagnostic is null ||
                GetReplacementMethodName(diagnostic) is not string convertToHexStringName)
            {
                return;
            }

            var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
            var semanticModel = await context.Document.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);

            if (GetInvocation(root, semanticModel, diagnostic.AdditionalLocations[0].SourceSpan, context.CancellationToken) is null ||
                GetInvocation(root, semanticModel, context.Span, context.CancellationToken) is null)
            {
                return;
            }

            RegisterCodeFix(
                context,
                string.Format(CultureInfo.CurrentCulture, PreferConvertToHexStringOverBitConverterCodeFixTitle, convertToHexStringName),
                nameof(PreferConvertToHexStringOverBitConverterCodeFixTitle));
        }

        protected sealed override async Task ApplyFixAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor, CancellationToken cancellationToken)
        {
            if (GetReplacementMethodName(diagnostic) is not string convertToHexStringName)
            {
                return;
            }

            var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            var root = editor.OriginalRoot;

            if (GetInvocation(root, semanticModel, diagnostic.AdditionalLocations[0].SourceSpan, cancellationToken) is not IInvocationOperation bitConverterInvocation ||
                GetInvocation(root, semanticModel, diagnostic.Location.SourceSpan, cancellationToken) is not IInvocationOperation outerInvocation)
            {
                return;
            }

            var toLowerInvocation = diagnostic.AdditionalLocations.Count == 2
                ? GetInvocation(root, semanticModel, diagnostic.AdditionalLocations[1].SourceSpan, cancellationToken)
                : null;

            var bitConverterArgumentsInParameterOrder = bitConverterInvocation.Arguments.GetArgumentsInParameterOrder();
            var carriedOver = bitConverterArgumentsInParameterOrder.Select(a => a.Value.Syntax)
                .Concat(toLowerInvocation?.Arguments.Select(a => a.Value.Syntax) ?? Enumerable.Empty<SyntaxNode>())
                .ToImmutableArray();

            // The replacement carries over syntax from inside the invocation it replaces, so that syntax has to
            // be read as the fixes nested inside it left it rather than off the original tree.
            foreach (var node in carriedOver)
            {
                editor.TrackNode(node);
            }

            editor.ReplaceNode(outerInvocation.Syntax, (currentOuterInvocation, generator) =>
            {
                SyntaxNode Current(SyntaxNode original) => currentOuterInvocation.GetCurrentNode(original) ?? original;

                var typeExpression = generator.DottedName(WellKnownTypeNames.SystemConvert);
                var methodExpression = generator.MemberAccessExpression(typeExpression, convertToHexStringName);
                var methodInvocation = bitConverterArgumentsInParameterOrder.Length switch
                {
                    // BitConverter.ToString(data).Replace("-", "") => Convert.ToHexString(data)
                    1 => generator.InvocationExpression(methodExpression, Current(bitConverterArgumentsInParameterOrder[0].Value.Syntax)),
                    // BitConverter.ToString(data, start).Replace("-", "") => Convert.ToHexString(data.AsSpan().Slice(start))
                    2 => generator.InvocationExpression(
                        methodExpression,
                        generator.InvocationExpression(generator.MemberAccessExpression(
                            generator.InvocationExpression(generator.MemberAccessExpression(
                                Current(bitConverterArgumentsInParameterOrder[0].Value.Syntax),
                                nameof(MemoryExtensions.AsSpan))),
                            WellKnownMemberNames.SliceMethodName),
                        Current(bitConverterArgumentsInParameterOrder[1].Value.Syntax)))
                            .WithAddImportsAnnotation()
                            .WithAdditionalAnnotations(s_asSpanSymbolAnnotation),
                    // BitConverter.ToString(data, start, length).Replace("-", "") => Convert.ToHexString(data, start, length)
                    3 => generator.InvocationExpression(methodExpression, bitConverterArgumentsInParameterOrder.Select(a => Current(a.Value.Syntax)).ToArray()),
                    _ => throw new NotImplementedException()
                };

                // This branch is hit when string.ToLower* is used and Convert.ToHexStringLower is not available.
                if (toLowerInvocation is not null)
                {
                    methodInvocation = generator.InvocationExpression(
                        generator.MemberAccessExpression(methodInvocation, toLowerInvocation.TargetMethod.Name),
                        toLowerInvocation.Arguments.Select(a => Current(a.Value.Syntax)).ToArray());
                }

                return methodInvocation.WithTriviaFrom(currentOuterInvocation);
            });
        }

        private static string? GetReplacementMethodName(Diagnostic diagnostic)
        {
            return diagnostic is { AdditionalLocations.Count: > 0, Properties.Count: 1 } &&
                diagnostic.Properties.TryGetValue(PreferConvertToHexStringOverBitConverterAnalyzer.ReplacementPropertiesKey, out var name)
                ? name
                : null;
        }

        private static IInvocationOperation? GetInvocation(SyntaxNode root, SemanticModel semanticModel, TextSpan span, CancellationToken cancellationToken)
        {
            var node = root.FindNode(span, getInnermostNodeForTie: true);

            return node is null ? null : semanticModel.GetOperation(node, cancellationToken) as IInvocationOperation;
        }
    }
}