Skip to content
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,55 @@
namespace Creedengo.Core.Analyzers;

/// <summary>GC2333: Remove redundant 'ToCharArray' call.</summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RemoveRedundantToCharArrayCallFixer)), Shared]
public sealed class RemoveRedundantToCharArrayCallFixer : CodeFixProvider
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds => _fixableDiagnosticIds;
private static readonly ImmutableArray<string> _fixableDiagnosticIds = ImmutableArray.Create(RemoveRedundantToCharArrayCall.Descriptor.Id);

/// <inheritdoc/>
[ExcludeFromCodeCoverage]
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

/// <inheritdoc/>
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
if (context.Diagnostics.Length == 0)
return;

var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null) return;

var nodeToFix = root.FindNode(context.Span, getInnermostNodeForTie: true);
context.RegisterCodeFix(
CodeAction.Create(
title: "Remove redundant 'ToCharArray' call",
createChangedDocument: token => RefactorAsync(context.Document, nodeToFix, token),
equivalenceKey: "Remove redundant 'ToCharArray' call"),
context.Diagnostics);
}

private static async Task<Document> RefactorAsync(Document document, SyntaxNode nodeToFix, CancellationToken token)
{
var editor = await DocumentEditor.CreateAsync(document, token).ConfigureAwait(false);

// nodeToFix is the IdentifierNameSyntax "ToCharArray"; climb up to the InvocationExpressionSyntax
if (nodeToFix.Parent is not MemberAccessExpressionSyntax memberAccess ||
memberAccess.Parent is not InvocationExpressionSyntax invocationSyntax)
{
return document;
}

if (editor.SemanticModel.GetOperation(invocationSyntax, token) is not IInvocationOperation invocation ||
invocation.Arguments.Length != 0 ||
invocation.Instance is null ||
invocation.TargetMethod.Name != "ToCharArray")
{
return document;
}

editor.ReplaceNode(invocationSyntax, invocation.Instance.Syntax);
return editor.GetChangedDocument();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace Creedengo.Core.Analyzers;

/// <summary>GC2333: Remove redundant 'ToCharArray' call.</summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class RemoveRedundantToCharArrayCall : DiagnosticAnalyzer
{
private static readonly ImmutableArray<SyntaxKind> SyntaxKinds = ImmutableArray.Create(
SyntaxKind.ForEachStatement);

/// <summary>The diagnostic descriptor.</summary>
public static DiagnosticDescriptor Descriptor { get; } = Rule.CreateDescriptor(
id: Rule.Ids.GCI2333_RemoveRedundantToCharArrayCall,
title: "Remove redundant 'ToCharArray' call",
message: "The 'ToCharArray' call is redundant",
category: Rule.Categories.Performance,
severity: DiagnosticSeverity.Warning,
description: "The 'ToCharArray' call is redundant and can be removed.");

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => _supportedDiagnostics;
private static readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics = ImmutableArray.Create(Descriptor);

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(static context => AnalyzeLoopNode(context), SyntaxKinds);
}

private static void AnalyzeLoopNode(SyntaxNodeAnalysisContext context)
{
var forEachStatement = (ForEachStatementSyntax)context.Node;

if (forEachStatement.Expression is not InvocationExpressionSyntax invocation)
return;

if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
return;

if (memberAccess.Name.Identifier.Text != "ToCharArray")
return;

var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccess);
if (symbolInfo.Symbol is not IMethodSymbol methodSymbol)
return;

if (methodSymbol.ContainingType.SpecialType != SpecialType.System_String)
return;

Comment thread
PendingChanges marked this conversation as resolved.
if (methodSymbol.Parameters.Length != 0)
return;

context.ReportDiagnostic(Diagnostic.Create(Descriptor, memberAccess.Name.GetLocation()));
}
}
1 change: 1 addition & 0 deletions src/Creedengo.Core/Models/Rule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static class Ids
public const string GCI95_UseIsOperatorInsteadOfAsOperator = "GCI95";
public const string GCIXX_UnnecessaryAssignment = "GCIXX";
public const string GCI96_UseEventArgsDotEmpty = "GCI96";
public const string GCI2333_RemoveRedundantToCharArrayCall = "GCI2333";
}

/// <summary>Creates a diagnostic descriptor.</summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
namespace Creedengo.Tests.Tests;

[TestClass]
public sealed class RemoveRedundantToCharArrayCallTests
{
private static readonly CodeFixerDlg VerifyAsync = TestRunner.VerifyAsync<RemoveRedundantToCharArrayCall, RemoveRedundantToCharArrayCallFixer>;

[TestMethod]
public Task EmptyCodeAsync() => VerifyAsync("");

// --- No-diagnostic cases ---

[TestMethod] // foreach expression is not an invocation (plain variable) — first guard
public Task ForeachOverPlainStringVariableNoDiagnosticAsync() => VerifyAsync("""
public class Test
{
public void Run()
{
string s = "test";

foreach (char c in s)
System.Console.WriteLine(c);
}
}
""");

[TestMethod] // foreach expression is not an invocation (plain char array) — first guard
public Task ForeachOverCharArrayNoDiagnosticAsync() => VerifyAsync("""
public class Test
{
public void Run()
{
char[] chars = new char[] { 'a', 'b' };

foreach (char c in chars)
System.Console.WriteLine(c);
}
}
""");

[TestMethod] // invocation is not a member access (bare method call) — second guard
public Task ForeachOverBareMethodCallNoDiagnosticAsync() => VerifyAsync("""
public class Test
{
private static char[] GetChars() => new char[] { 'a', 'b' };

public void Run()
{
foreach (char c in GetChars())
System.Console.WriteLine(c);
}
}
""");

[TestMethod] // method name is not "ToCharArray" — third guard
public Task ForeachOverOtherStringMethodNoDiagnosticAsync() => VerifyAsync("""
public class Test
{
public void Run()
{
string s = "hello world";

foreach (string part in s.Split(' '))
System.Console.WriteLine(part);
}
}
""");

[TestMethod] // "ToCharArray" on a non-string type — fifth guard (ContainingType.SpecialType check)
public Task ForeachOverToCharArrayOnCustomTypeNoDiagnosticAsync() => VerifyAsync("""
public class MyBuffer
{
public char[] ToCharArray() => new char[] { 'x' };
}

public class Test
{
public void Run()
{
var buf = new MyBuffer();

foreach (char c in buf.ToCharArray())
System.Console.WriteLine(c);
}
}
""");

// --- Positive cases (diagnostic + fix) ---

[TestMethod]
public Task ToCharArrayOnVariableShouldBeRemovedAsync() => VerifyAsync("""
public class Test
{
public void Run()
{
string s = "test";

foreach (char c in s.[|ToCharArray|]())
System.Console.WriteLine(c);
}
}
""",
"""
public class Test
{
public void Run()
{
string s = "test";

foreach (char c in s)
System.Console.WriteLine(c);
}
}
""");
Comment thread
PendingChanges marked this conversation as resolved.

[TestMethod]
public Task ToCharArrayOnStringLiteralShouldBeRemovedAsync() => VerifyAsync("""
public class Test
{
public void Run()
{
foreach (char c in "hello".[|ToCharArray|]())
System.Console.WriteLine(c);
}
}
""",
"""
public class Test
{
public void Run()
{
foreach (char c in "hello")
System.Console.WriteLine(c);
}
}
""");

[TestMethod]
public Task ToCharArrayOnMethodReturnValueShouldBeRemovedAsync() => VerifyAsync("""
public class Test
{
private static string GetText() => "test";

public void Run()
{
foreach (char c in GetText().[|ToCharArray|]())
System.Console.WriteLine(c);
}
}
""",
"""
public class Test
{
private static string GetText() => "test";

public void Run()
{
foreach (char c in GetText())
System.Console.WriteLine(c);
}
}
""");

[TestMethod] // ToCharArray(int, int) overload is not redundant — sixth guard (Parameters.Length != 0)
public Task ToCharArrayWithArgumentsNoDiagnosticAsync() => VerifyAsync("""
public class Test
{
public void Run()
{
string s = "test";

foreach (char c in s.ToCharArray(0, 2))
System.Console.WriteLine(c);
}
}
""");
}
1 change: 0 additions & 1 deletion src/Creedengo.Tool/Creedengo.Tool.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
<PackageTags>EcoCode, Creedengo, GCI, Green Code Initiative, Analyzers, Environment, Green</PackageTags>
<PackageReleaseNotes></PackageReleaseNotes>
<RestoreSources>https://api.nuget.org/v3/index.json</RestoreSources>

<NoWarn>${NoWarn};CA1031;CA1812</NoWarn>
</PropertyGroup>

Expand Down