// 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; } } }