// 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.Diagnostics.CodeAnalysis; using System.Linq; using Analyzer.Utilities; using Analyzer.Utilities.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; namespace Microsoft.NetCore.Analyzers.Performance { using static MicrosoftNetCoreAnalyzersResources; /// <summary> /// CA1872: <inheritdoc cref="PreferConvertToHexStringOverBitConverterTitle"/> /// </summary> [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] public sealed class PreferConvertToHexStringOverBitConverterAnalyzer : DiagnosticAnalyzer { internal const string RuleId = "CA1872"; internal const string ReplacementPropertiesKey = nameof(ReplacementPropertiesKey); private const string Empty = nameof(Empty); private const string Replace = nameof(Replace); private const string ToHexString = nameof(ToHexString); private const string ToHexStringLower = nameof(ToHexStringLower); private const string ToLower = nameof(ToLower); private const string ToLowerInvariant = nameof(ToLowerInvariant); internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( RuleId, CreateLocalizableResourceString(nameof(PreferConvertToHexStringOverBitConverterTitle)), CreateLocalizableResourceString(nameof(PreferConvertToHexStringOverBitConverterMessage)), DiagnosticCategory.Performance, RuleLevel.IdeSuggestion, CreateLocalizableResourceString(nameof(PreferConvertToHexStringOverBitConverterDescription)), isPortedFxCopRule: false, isDataflowRule: false); public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); public sealed override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.RegisterCompilationStartAction(OnCompilationStart); } private void OnCompilationStart(CompilationStartAnalysisContext context) { if (!RequiredSymbols.TryGetSymbols(context.Compilation, out var symbols)) { return; } context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); void AnalyzeInvocation(OperationAnalysisContext context) { var invocation = (IInvocationOperation)context.Operation; if (!symbols.TryGetBitConverterInvocationChain( invocation, out var bitConverterInvocation, out var outerInvocation, out var toLowerInvocation, out var convertToHexStringReplacementMethod)) { return; } var additionalLocationsBuilder = ImmutableArray.CreateBuilder<Location>(); additionalLocationsBuilder.Add(bitConverterInvocation.Syntax.GetLocation()); if (toLowerInvocation is not null) { additionalLocationsBuilder.Add(toLowerInvocation.Syntax.GetLocation()); } context.ReportDiagnostic(outerInvocation.CreateDiagnostic( Rule, additionalLocations: additionalLocationsBuilder.ToImmutable(), properties: ImmutableDictionary<string, string?>.Empty.Add(ReplacementPropertiesKey, convertToHexStringReplacementMethod.Name), args:[convertToHexStringReplacementMethod.ToDisplayString(), bitConverterInvocation.TargetMethod.ToDisplayString()])); } } private sealed class RequiredSymbols { private RequiredSymbols( ImmutableArray<IMethodSymbol> stringReplaceMethods, ImmutableArray<IMethodSymbol> stringToLowerMethods, IFieldSymbol? stringEmptyField, ImmutableDictionary<IMethodSymbol, IMethodSymbol> bitConverterReplacements, ImmutableDictionary<IMethodSymbol, IMethodSymbol> bitConverterReplacementsToLower) { _stringReplaceMethods = stringReplaceMethods; _stringToLowerMethods = stringToLowerMethods; _stringEmptyField = stringEmptyField; _bitConverterReplacements = bitConverterReplacements; _bitConverterReplacementsToLower = bitConverterReplacementsToLower; } public static bool TryGetSymbols(Compilation compilation, [NotNullWhen(true)] out RequiredSymbols? symbols) { symbols = default; var bitConverterType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemBitConverter); var convertType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemConvert); if (bitConverterType is null || convertType is null) { return false; } var byteType = compilation.GetSpecialType(SpecialType.System_Byte); var byteArrayType = compilation.CreateArrayTypeSymbol(byteType); var int32Type = compilation.GetSpecialType(SpecialType.System_Int32); var stringType = compilation.GetSpecialType(SpecialType.System_String); var rosByteType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemReadOnlySpan1)?.Construct(byteType); var stringReplaceMethods = stringType.GetMembers(Replace) .OfType<IMethodSymbol>() .WhereAsArray(FilterStringReplaceMethods); if (stringReplaceMethods.IsEmpty) { return false; } var stringToLowerMethods = stringType.GetMembers(ToLower) .AddRange(stringType.GetMembers(ToLowerInvariant)) .OfType<IMethodSymbol>() .ToImmutableArray(); var stringEmptyField = stringType.GetMembers(Empty) .OfType<IFieldSymbol>() .FirstOrDefault(); var bitConverterToStringMethods = bitConverterType.GetMembers(WellKnownMemberNames.ObjectToString) .OfType<IMethodSymbol>() .ToImmutableArray(); var bitConverterToString = bitConverterToStringMethods.GetFirstOrDefaultMemberWithParameterTypes(byteArrayType); var bitConverterToStringStart = bitConverterToStringMethods.GetFirstOrDefaultMemberWithParameterTypes(byteArrayType, int32Type); var bitConverterToStringStartLength = bitConverterToStringMethods.GetFirstOrDefaultMemberWithParameterTypes(byteArrayType, int32Type, int32Type); var convertToHexStringMethods = convertType.GetMembers(ToHexString) .OfType<IMethodSymbol>() .ToImmutableArray(); var convertToHexString = convertToHexStringMethods.GetFirstOrDefaultMemberWithParameterTypes(byteArrayType); var convertToHexStringRos = rosByteType is not null ? convertToHexStringMethods.GetFirstOrDefaultMemberWithParameterTypes(rosByteType) : null; var convertToHexStringStartLength = convertToHexStringMethods.GetFirstOrDefaultMemberWithParameterTypes(byteArrayType, int32Type, int32Type); var bitConverterReplacementsBuilder = ImmutableDictionary.CreateBuilder<IMethodSymbol, IMethodSymbol>(); // BitConverter.ToString(data).Replace("-", "") => Convert.ToHexString(data) bitConverterReplacementsBuilder.AddKeyValueIfNotNull(bitConverterToString, convertToHexString); // BitConverter.ToString(data, start).Replace("-", "") => Convert.ToHexString(data.AsSpan().Slice(start)) bitConverterReplacementsBuilder.AddKeyValueIfNotNull(bitConverterToStringStart, convertToHexStringRos); // BitConverter.ToString(data, start, length).Replace("-", "") => Convert.ToHexString(data, start, length) bitConverterReplacementsBuilder.AddKeyValueIfNotNull(bitConverterToStringStartLength, convertToHexStringStartLength); var bitConverterReplacements = bitConverterReplacementsBuilder.ToImmutableDictionary(); // Bail out if we have no valid replacement pair from BitConverter.ToString to Convert.ToHexString. if (bitConverterReplacements.IsEmpty) { return false; } var convertToHexStringLowerMethods = convertType.GetMembers(ToHexStringLower) .OfType<IMethodSymbol>() .ToImmutableArray(); var convertToHexStringLower = convertToHexStringLowerMethods.GetFirstOrDefaultMemberWithParameterTypes(byteArrayType); var convertToHexStringLowerRos = rosByteType is not null ? convertToHexStringLowerMethods.GetFirstOrDefaultMemberWithParameterTypes(rosByteType) : null; var convertToHexStringLowerStartLength = convertToHexStringLowerMethods.GetFirstOrDefaultMemberWithParameterTypes(byteArrayType, int32Type, int32Type); // The following replacements are optional: Convert.ToHexStringLower is available as of .NET 9. var bitConverterReplacementsToLowerBuilder = ImmutableDictionary.CreateBuilder<IMethodSymbol, IMethodSymbol>(); // BitConverter.ToString(data).Replace("-", "").ToLower() => Convert.ToHexStringLower(data) bitConverterReplacementsToLowerBuilder.AddKeyValueIfNotNull(bitConverterToString, convertToHexStringLower); // BitConverter.ToString(data, start).Replace("-", "").ToLower() => Convert.ToHexStringLower(data.AsSpan().Slice(start)) bitConverterReplacementsToLowerBuilder.AddKeyValueIfNotNull(bitConverterToStringStart, convertToHexStringLowerRos); // BitConverter.ToString(data, start, length).Replace("-", "").ToLower() => Convert.ToHexStringLower(data, start, length) bitConverterReplacementsToLowerBuilder.AddKeyValueIfNotNull(bitConverterToStringStartLength, convertToHexStringLowerStartLength); var bitConverterReplacementsToLower = bitConverterReplacementsToLowerBuilder.ToImmutableDictionary(); symbols = new RequiredSymbols(stringReplaceMethods, stringToLowerMethods, stringEmptyField, bitConverterReplacements, bitConverterReplacementsToLower); return true; bool FilterStringReplaceMethods(IMethodSymbol stringReplaceMethod) { return stringReplaceMethod.Parameters.Length >= 2 && SymbolEqualityComparer.Default.Equals(stringReplaceMethod.Parameters[0].Type, stringType) && SymbolEqualityComparer.Default.Equals(stringReplaceMethod.Parameters[1].Type, stringType); } } /// <summary> /// Attempts to obtain a complete BitConverter.ToString invocation chain that can be replaced with a call to Convert.ToHexString*. /// To do this, the following steps are performed: /// 1. Check if invocation is a BitConverter.ToString invocation and abort if it is not. /// 2. Continue with the parent invocation or abort if none exists. /// 3. Check if invocation is a string.ToLower* invocation. /// If so, continue with the parent invocation or abort if none exists. /// 4. Check if invocation is a string.Replace("-", "") invocation and abort if it is not. /// If so, continue with the parent invocation or skip the next step if none exists. /// 5. Check if invocation is a string.ToLower* invocation. /// 6. Get the appropriate replacement method (from BitConverter.ToString to Convert.ToHexString or Convert.ToHexStringLower) or abort if none exists. /// Note that <paramref name="toLowerInvocation"/> is only set if a string.ToLower* invocation is found and Convert.ToHexStringLower is not available. /// </summary> /// <param name="invocation">The starting invocation to analyze (must be BitConverter.ToString for this method to return true)</param> /// <param name="bitConverterInvocation">The extracted BitConverter.ToString invocation, or null if unsuccessful</param> /// <param name="outerInvocation">The outer invocation that is used for creating the diagnostic (and that will be replaced in the fixer)</param> /// <param name="toLowerInvocation">The extracted string.ToLower* invocation, or null if none exists or Convert.ToHexStringLower is used as a replacement</param> /// <param name="replacementMethod">The Convert.ToHexString* method to use as a replacement</param> /// <returns></returns> public bool TryGetBitConverterInvocationChain( IInvocationOperation invocation, [NotNullWhen(true)] out IInvocationOperation? bitConverterInvocation, [NotNullWhen(true)] out IInvocationOperation? outerInvocation, out IInvocationOperation? toLowerInvocation, [NotNullWhen(true)] out IMethodSymbol? replacementMethod) { bitConverterInvocation = default; outerInvocation = default; toLowerInvocation = default; replacementMethod = default; // Bail out if the invocation is not a BitConverter.ToString invocation. if (!IsAnyBitConverterMethod(invocation.TargetMethod)) { return false; } bitConverterInvocation = invocation; // Bail out if there is no parent invocation. if (!TryGetParentInvocation(invocation, out invocation!)) { return false; } // Check for an optional string.ToLower invocation, e.g. in the case of // BitConverter.ToString(data).ToLower().Replace("-", "") if (IsAnyStringToLowerMethod(invocation.TargetMethod)) { toLowerInvocation = invocation; // Bail out if there is no parent invocation as we still need the string.Replace invocation. if (!TryGetParentInvocation(invocation, out invocation!)) { return false; } } // Bail out if there is no appropriate string.Replace invocation. if (!IsAnyStringReplaceMethod(invocation.TargetMethod) || !HasStringReplaceArgumentsToRemoveHyphen(invocation.Arguments)) { return false; } outerInvocation = invocation; // Check for an optional string.ToLower invocation, e.g. in the case of // BitConverter.ToString(data).Replace("-", "").ToLower() if (TryGetParentInvocation(invocation, out invocation!)) { if (IsAnyStringToLowerMethod(invocation.TargetMethod)) { toLowerInvocation = invocation; outerInvocation = invocation; } } // At this point we have a complete BitConverter.ToString invocation chain. // Get the appropriate replacement method from BitConverter.ToString to Convert.ToHexStringLower or Convert.ToHexString. if (toLowerInvocation is not null && _bitConverterReplacementsToLower.TryGetValue(bitConverterInvocation.TargetMethod, out var replacementToLower)) { replacementMethod = replacementToLower; // Reset toLowerInvocation as we no longer need a string.ToLower after using Convert.ToHexStringLower. toLowerInvocation = default; return true; } else if (_bitConverterReplacements.TryGetValue(bitConverterInvocation.TargetMethod, out var replacement)) { replacementMethod = replacement; return true; } // No replacement was found. return false; static bool TryGetParentInvocation(IInvocationOperation invocation, [NotNullWhen(true)] out IInvocationOperation? parentInvocation) { parentInvocation = invocation.Parent as IInvocationOperation; return parentInvocation is not null; } } private bool IsAnyBitConverterMethod(IMethodSymbol method) { return _bitConverterReplacements.ContainsKey(method) || _bitConverterReplacementsToLower.ContainsKey(method); } private bool IsAnyStringReplaceMethod(IMethodSymbol? method) { return _stringReplaceMethods.Any(m => SymbolEqualityComparer.Default.Equals(m, method)); } private bool IsAnyStringToLowerMethod(IMethodSymbol? method) { return _stringToLowerMethods.Any(m => SymbolEqualityComparer.Default.Equals(m, method)); } private bool IsStringEmptyField(IOperation operation) { return operation is IFieldReferenceOperation fieldReferenceOperation && SymbolEqualityComparer.Default.Equals(fieldReferenceOperation.Field, _stringEmptyField); } private bool HasStringReplaceArgumentsToRemoveHyphen(ImmutableArray<IArgumentOperation> arguments) { var argumentsInParameterOrder = arguments.GetArgumentsInParameterOrder(); var oldValue = argumentsInParameterOrder[0].Value; var newValue = argumentsInParameterOrder[1].Value; bool oldValueIsConstantHyphenString = oldValue.ConstantValue.HasValue && oldValue.ConstantValue.Value is string oldValueString && oldValueString.Equals("-", StringComparison.Ordinal); bool newValueIsConstantNullOrEmptyString = newValue.ConstantValue.HasValue && newValue.ConstantValue.Value is string newValueString && string.IsNullOrEmpty(newValueString); return oldValueIsConstantHyphenString && (newValueIsConstantNullOrEmptyString || newValue.HasNullConstantValue() || IsStringEmptyField(newValue)); } private readonly ImmutableArray<IMethodSymbol> _stringReplaceMethods; private readonly ImmutableArray<IMethodSymbol> _stringToLowerMethods; private readonly IFieldSymbol? _stringEmptyField; private readonly ImmutableDictionary<IMethodSymbol, IMethodSymbol> _bitConverterReplacements; private readonly ImmutableDictionary<IMethodSymbol, IMethodSymbol> _bitConverterReplacementsToLower; } } }