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 Analyzer & Codefix - CA1847 - Use String.Contains(Char) instead of String.Contains(String) with single characters #4908

Merged
merged 1 commit into from
Jun 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.NetCore.Analyzers.Performance;

namespace Microsoft.NetCore.CSharp.Analyzers.Performance
{

public sealed class CSharpUseStringContainsCharOverloadWithSingleCharactersFixer : UseStringContainsCharOverloadWithSingleCharactersCodeFix
{
protected override bool TryGetArgumentName(SyntaxNode violatingNode, out string argumentName)
{
argumentName = string.Empty;
if (violatingNode is ArgumentSyntax argumentSyntax)
{
if (argumentSyntax.NameColon is null)
return false;

argumentName = argumentSyntax.NameColon.Name.Identifier.ValueText;
return true;
}
return false;
}

protected override bool TryGetLiteralValueFromNode(SyntaxNode violatingNode, out char charLiteral)
{
charLiteral = default;
if (violatingNode is LiteralExpressionSyntax literalExpressionSyntax)
{
return TryGetCharFromLiteralExpressionSyntax(literalExpressionSyntax, out charLiteral);
}
else if (violatingNode is ArgumentSyntax argumentSyntax
&& argumentSyntax.Expression is LiteralExpressionSyntax containedLiteralExpressionSyntax)
{
return TryGetCharFromLiteralExpressionSyntax(containedLiteralExpressionSyntax, out charLiteral);
}
return false;

static bool TryGetCharFromLiteralExpressionSyntax(LiteralExpressionSyntax sourceLiteralExpressionSyntax, out char parsedCharLiteral)
{
parsedCharLiteral = default;
if (sourceLiteralExpressionSyntax.Token.Value is string sourceLiteralValue && char.TryParse(sourceLiteralValue, out parsedCharLiteral))
{
return true;
}
return false;
}
}
}
}
1 change: 1 addition & 0 deletions src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ CA1843 | Performance | Info | DoNotUseWhenAllOrWaitAllWithSingleArgument, [Docum
CA1844 | Performance | Info | ProvideStreamMemoryBasedAsyncOverrides, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1844)
CA1845 | Performance | Info | UseSpanBasedStringConcat, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1845)
CA1846 | Performance | Info | PreferAsSpanOverSubstring, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1846)
CA1847 | Performance | Info | UseStringContainsCharOverloadWithSingleCharactersAnalyzer, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1847)
CA2250 | Usage | Info | UseCancellationTokenThrowIfCancellationRequested, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2250)
CA2251 | Usage | Hidden | UseStringEqualsOverStringCompare, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2251)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1662,4 +1662,16 @@
<data name="MakeParameterlessConstructorPublic" xml:space="preserve">
<value>Make the constructor that takes zero parameters 'public'</value>
</data>
<data name="UseStringContainsCharOverloadWithSingleCharactersDescription" xml:space="preserve">
<value>'string.Contains(char)' is available as a better performing overload for single char lookup.</value>
MeikTranel marked this conversation as resolved.
Show resolved Hide resolved
</data>
<data name="UseStringContainsCharOverloadWithSingleCharactersMessage" xml:space="preserve">
<value>Use 'string.Contains(char)' instead of 'string.Contains(string)' when searching for a single character</value>
</data>
<data name="UseStringContainsCharOverloadWithSingleCharactersTitle" xml:space="preserve">
<value>Use char literal for a single character lookup</value>
</data>
<data name="ReplaceStringLiteralWithCharLiteralCodeActionTitle" xml:space="preserve">
<value>Replace string literal with char literal</value>
buyaa-n marked this conversation as resolved.
Show resolved Hide resolved
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;

namespace Microsoft.NetCore.Analyzers.Performance
{
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared]
public abstract class UseStringContainsCharOverloadWithSingleCharactersCodeFix : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(
UseStringContainsCharOverloadWithSingleCharactersAnalyzer.CA1847);

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

if (TryGetLiteralValueFromNode(violatingNode, out var sourceCharLiteral))
{
if (TryGetArgumentName(violatingNode, out var argumentName))
{
context.RegisterCodeFix(new ReplaceStringLiteralWithCharLiteralCodeAction(context.Document, violatingNode, sourceCharLiteral, argumentName), context.Diagnostics);
}
else
{
context.RegisterCodeFix(new ReplaceStringLiteralWithCharLiteralCodeAction(context.Document, violatingNode, sourceCharLiteral), context.Diagnostics);
}
}
}

protected abstract bool TryGetArgumentName(SyntaxNode violatingNode, out string argumentName);
protected abstract bool TryGetLiteralValueFromNode(SyntaxNode violatingNode, out char charLiteral);

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

private class ReplaceStringLiteralWithCharLiteralCodeAction : CodeAction
{
private readonly Document _document;
private readonly SyntaxNode _nodeToBeFixed;
private readonly char _sourceCharLiteral;
private readonly string? _argumentName;

public override string Title => MicrosoftNetCoreAnalyzersResources.ReplaceStringLiteralWithCharLiteralCodeActionTitle;

public override string EquivalenceKey => nameof(ReplaceStringLiteralWithCharLiteralCodeAction);
public ReplaceStringLiteralWithCharLiteralCodeAction(Document document, SyntaxNode nodeToBeFixed, char sourceCharLiteral)
{
_document = document;
_nodeToBeFixed = nodeToBeFixed;
_sourceCharLiteral = sourceCharLiteral;
}

public ReplaceStringLiteralWithCharLiteralCodeAction(Document document, SyntaxNode nodeToBeFixed, char sourceCharLiteral, string? argumentName) : this(document, nodeToBeFixed, sourceCharLiteral)
{
_argumentName = argumentName;
}

protected override async Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false);
var newExpression = editor.Generator.LiteralExpression(_sourceCharLiteral);
if (_argumentName is not null)
{
newExpression = editor.Generator.Argument(_argumentName, RefKind.None, newExpression);
}
editor.ReplaceNode(_nodeToBeFixed, newExpression);

return editor.GetChangedDocument();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
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
{
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class UseStringContainsCharOverloadWithSingleCharactersAnalyzer : DiagnosticAnalyzer
{
internal const string CA1847 = nameof(CA1847);
private static readonly LocalizableString s_localizableTitle_CA1847 = new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.UseStringContainsCharOverloadWithSingleCharactersTitle), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources));
private static readonly LocalizableString s_localizableMessage_CA1847 = new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.UseStringContainsCharOverloadWithSingleCharactersMessage), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources));
private static readonly LocalizableString s_localizableDescription_CA1847 = new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.UseStringContainsCharOverloadWithSingleCharactersDescription), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources));

internal static readonly DiagnosticDescriptor s_rule_CA1847 = DiagnosticDescriptorHelper.Create(
CA1847,
s_localizableTitle_CA1847,
s_localizableMessage_CA1847,
DiagnosticCategory.Performance,
RuleLevel.IdeSuggestion,
description: s_localizableDescription_CA1847,
isPortedFxCopRule: false,
isDataflowRule: false);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(s_rule_CA1847);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(CheckIfRuleIsApplicableAndRegister);
}

private void CheckIfRuleIsApplicableAndRegister(CompilationStartAnalysisContext context)
{
var stringType = context.Compilation.GetSpecialType(SpecialType.System_String);
var charType = context.Compilation.GetSpecialType(SpecialType.System_Char);

// Bail out of further evaluations in special cases where string/char types arent even available
if (stringType is null || charType is null)
return;

var ruleIsApplicable = stringType.GetMembers("Contains").OfType<IMethodSymbol>().Any(m =>
{
if (m.Parameters.Length > 0)
{
return m.Parameters[0].Type.SpecialType == SpecialType.System_Char;
}
return false;
});

if (ruleIsApplicable)
context.RegisterOperationAction(AnalyseContainsUsage, OperationKind.Invocation);
}

private void AnalyseContainsUsage(OperationAnalysisContext context)
{
var invocationOperation = (IInvocationOperation)context.Operation;
if (ShouldAnalyzeInvocation(invocationOperation))
{
var argumentOperation = invocationOperation.Arguments.GetArgumentForParameterAtIndex(0);
if (CheckForViolation(argumentOperation))
context.ReportDiagnostic(argumentOperation.CreateDiagnostic(s_rule_CA1847));
}

static bool ShouldAnalyzeInvocation(IInvocationOperation invocationOperation)
{
return invocationOperation.TargetMethod is IMethodSymbol invokedMethod
&& invokedMethod.ContainingType.SpecialType.Equals(SpecialType.System_String)
&& invokedMethod.Name == "Contains"
&& invokedMethod.Parameters.Length > 0
&& invokedMethod.Parameters[0].Type.SpecialType.Equals(SpecialType.System_String);
MeikTranel marked this conversation as resolved.
Show resolved Hide resolved
}

static bool CheckForViolation(IArgumentOperation primaryArgument)
{
if (primaryArgument.Value is ILiteralOperation literalOperation
&& literalOperation.ConstantValue.HasValue
&& literalOperation.ConstantValue.Value is string constantString
MeikTranel marked this conversation as resolved.
Show resolved Hide resolved
&& constantString.Length == 1)
{
return true;
}
return false;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1852,6 +1852,11 @@
<target state="translated">Odebrat redundantní volání</target>
<note />
</trans-unit>
<trans-unit id="ReplaceStringLiteralWithCharLiteralCodeActionTitle">
<source>Replace string literal with char literal</source>
<target state="new">Replace string literal with char literal</target>
<note />
</trans-unit>
<trans-unit id="RethrowToPreserveStackDetailsDescription">
<source>An exception is rethrown and the exception is explicitly specified in the throw statement. If an exception is rethrown by specifying the exception in the throw statement, the list of method calls between the original method that threw the exception and the current method is lost.</source>
<target state="translated">Výjimka se vyvolá znovu s tím, že se explicitně určí ve výrazu throw. Pokud se výjimka znovu vyvolá tak, že se určí ve výrazu throw, ztratí se seznam volání metod mezi původní metodou, která výjimku vyvolala, a aktuální metodou.</target>
Expand Down Expand Up @@ -2462,6 +2467,21 @@
<target state="new">Use span-based 'string.Concat'</target>
<note />
</trans-unit>
<trans-unit id="UseStringContainsCharOverloadWithSingleCharactersDescription">
<source>'string.Contains(char)' is available as a better performing overload for single char lookup.</source>
<target state="new">'string.Contains(char)' is available as a better performing overload for single char lookup.</target>
<note />
</trans-unit>
<trans-unit id="UseStringContainsCharOverloadWithSingleCharactersMessage">
<source>Use 'string.Contains(char)' instead of 'string.Contains(string)' when searching for a single character</source>
<target state="new">Use 'string.Contains(char)' instead of 'string.Contains(string)' when searching for a single character</target>
<note />
</trans-unit>
<trans-unit id="UseStringContainsCharOverloadWithSingleCharactersTitle">
<source>Use char literal for a single character lookup</source>
<target state="new">Use char literal for a single character lookup</target>
<note />
</trans-unit>
<trans-unit id="UseValidPlatformStringDescription">
<source>Platform compatibility analyzer requires a valid platform name and version.</source>
<target state="new">Platform compatibility analyzer requires a valid platform name and version.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1852,6 +1852,11 @@
<target state="translated">Redundanten Aufruf entfernen</target>
<note />
</trans-unit>
<trans-unit id="ReplaceStringLiteralWithCharLiteralCodeActionTitle">
<source>Replace string literal with char literal</source>
<target state="new">Replace string literal with char literal</target>
<note />
</trans-unit>
<trans-unit id="RethrowToPreserveStackDetailsDescription">
<source>An exception is rethrown and the exception is explicitly specified in the throw statement. If an exception is rethrown by specifying the exception in the throw statement, the list of method calls between the original method that threw the exception and the current method is lost.</source>
<target state="translated">Eine Ausnahme wird erneut ausgelöst, und die Ausnahme wird in der throw-Anweisung explizit angegeben. Wenn eine Ausnahme durch Angabe in der throw-Anweisung erneut ausgelöst wird, geht die Liste von Methodenaufrufen zwischen der ursprünglichen Methode, die die Ausnahme ausgelöst hat, und der aktuellen Methode verloren.</target>
Expand Down Expand Up @@ -2462,6 +2467,21 @@
<target state="new">Use span-based 'string.Concat'</target>
<note />
</trans-unit>
<trans-unit id="UseStringContainsCharOverloadWithSingleCharactersDescription">
<source>'string.Contains(char)' is available as a better performing overload for single char lookup.</source>
<target state="new">'string.Contains(char)' is available as a better performing overload for single char lookup.</target>
<note />
</trans-unit>
<trans-unit id="UseStringContainsCharOverloadWithSingleCharactersMessage">
<source>Use 'string.Contains(char)' instead of 'string.Contains(string)' when searching for a single character</source>
<target state="new">Use 'string.Contains(char)' instead of 'string.Contains(string)' when searching for a single character</target>
<note />
</trans-unit>
<trans-unit id="UseStringContainsCharOverloadWithSingleCharactersTitle">
<source>Use char literal for a single character lookup</source>
<target state="new">Use char literal for a single character lookup</target>
<note />
</trans-unit>
<trans-unit id="UseValidPlatformStringDescription">
<source>Platform compatibility analyzer requires a valid platform name and version.</source>
<target state="new">Platform compatibility analyzer requires a valid platform name and version.</target>
Expand Down
Loading