Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CA1872: Prefer 'Convert.ToHexString' over 'BitConverter.ToString' #6967

Merged
merged 13 commits into from
Mar 19, 2024
Merged
1 change: 1 addition & 0 deletions src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ Rule ID | Category | Severity | Notes
--------|----------|----------|-------
CA1514 | Maintainability | Info | AvoidLengthCheckWhenSlicingToEndAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1514)
CA1515 | Maintainability | Disabled | MakeTypesInternal, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1515)
CA1872 | Performance | Info | PreferConvertToHexStringOverBitConverterAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1872)
CA2262 | Usage | Info | ProvideHttpClientHandlerMaxResponseHeaderLengthValueCorrectly, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2262)
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,18 @@
<data name="DoNotUseCountWhenAnyCanBeUsedTitle" xml:space="preserve">
<value>Do not use Count() or LongCount() when Any() can be used</value>
</data>
<data name="PreferConvertToHexStringOverBitConverterTitle" xml:space="preserve">
<value>Prefer 'Convert.ToHexString' over 'BitConverter.ToString'</value>
</data>
<data name="PreferConvertToHexStringOverBitConverterDescription" xml:space="preserve">
<value>'Convert.ToHexString' is more efficient and allocation-friendly than 'BitConverter.ToString' in combination with 'string.Replace'.</value>
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
</data>
<data name="PreferConvertToHexStringOverBitConverterMessage" xml:space="preserve">
<value>Prefer '{0}' over '{1}'</value>
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
</data>
<data name="PreferConvertToHexStringOverBitConverterCodeFixTitle" xml:space="preserve">
<value>Replace with 'Convert.ToHexString'</value>
</data>
<data name="DoNotUseTimersThatPreventPowerStateChangesTitle" xml:space="preserve">
<value>Do not use timers that prevent power state changes</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Performance
{
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared]
public sealed class PreferConvertToHexStringOverBitConverterFixer : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create(PreferConvertToHexStringOverBitConverterAnalyzer.RuleId);

public sealed override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var node = root.FindNode(context.Span, getInnermostNodeForTie: true);

if (node is null)
{
return;
}

var semanticModel = await context.Document.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
var operation = semanticModel.GetOperation(node, context.CancellationToken);

if (operation is not IInvocationOperation replaceInvocation ||
replaceInvocation.Instance is not IInvocationOperation instanceInvocation ||
!PreferConvertToHexStringOverBitConverterAnalyzer.RequiredSymbols.TryGetSymbols(semanticModel.Compilation, out var symbols) ||
!symbols.TryGetBitConverterToStringInvocation(instanceInvocation, out var bitConverterInvocation, out var toLowerInvocation))
{
return;
}

var codeAction = CodeAction.Create(
MicrosoftNetCoreAnalyzersResources.PreferConvertToHexStringOverBitConverterCodeFixTitle,
ReplaceWithConvertToHexStringCall,
nameof(MicrosoftNetCoreAnalyzersResources.PreferConvertToHexStringOverBitConverterCodeFixTitle));

context.RegisterCodeFix(codeAction, context.Diagnostics);

async Task<Document> ReplaceWithConvertToHexStringCall(CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(context.Document, cancellationToken).ConfigureAwait(false);
var generator = editor.Generator;
var bitConverterArgumentsInParameterOrder = bitConverterInvocation.Arguments.GetArgumentsInParameterOrder();

var convertToHexStringSymbol = bitConverterArgumentsInParameterOrder.Length == 1
? symbols.ConvertToHexString!
: symbols.ConvertToHexStringStartLength!;
var typeExpression = generator.TypeExpressionForStaticMemberAccess(convertToHexStringSymbol.ContainingType);
var methodExpression = generator.MemberAccessExpression(typeExpression, convertToHexStringSymbol.Name);
var methodInvocation = bitConverterArgumentsInParameterOrder.Length switch
{
// BitConverter.ToString(data).Replace("-", "") => Convert.ToHexString(data)
1 => generator.InvocationExpression(methodExpression, bitConverterArgumentsInParameterOrder[0].Value.Syntax),
// BitConverter.ToString(data, start).Replace("-", "") => Convert.ToHexString(data, start, data.Length - start)
2 => generator.InvocationExpression(
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
methodExpression,
bitConverterArgumentsInParameterOrder[0].Value.Syntax,
bitConverterArgumentsInParameterOrder[1].Value.Syntax,
generator.SubtractExpression(
generator.MemberAccessExpression(
bitConverterArgumentsInParameterOrder[0].Value.Syntax,
WellKnownMemberNames.LengthPropertyName),
bitConverterArgumentsInParameterOrder[1].Value.Syntax)),
// BitConverter.ToString(data, start, length).Replace("-", "") => Convert.ToHexString(data, start, length)
3 => generator.InvocationExpression(methodExpression, bitConverterArgumentsInParameterOrder.Select(a => a.Value.Syntax).ToArray()),
_ => throw new NotImplementedException()
};

// BitConverter.ToString(data).ToLower().Replace("-", "") => Convert.ToHexString(data).ToLower()
if (toLowerInvocation is not null)
{
methodInvocation = generator.InvocationExpression(
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
generator.MemberAccessExpression(methodInvocation, toLowerInvocation.TargetMethod.Name),
toLowerInvocation.Arguments.Select(a => a.Value.Syntax).ToArray());
}

editor.ReplaceNode(replaceInvocation.Syntax, methodInvocation.WithTriviaFrom(replaceInvocation.Syntax));

return context.Document.WithSyntaxRoot(editor.GetChangedRoot());
}
}
}
}