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

EC90 Use 'Cast' instead of 'Select' to cast: MA0078 #52

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
namespace EcoCode.Core.Analyzers
{
using System.Linq;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseCastInsteadOfSelectToCastFixer)), Shared]
public sealed class UseCastInsteadOfSelectToCastFixer : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(Rule.Ids.EC90_UseCastInsteadOfSelectToCast); }
}

public sealed override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}
ElyesFakhar marked this conversation as resolved.
Show resolved Hide resolved

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;

// Find the Select invocation node identified by the diagnostic.
var selectInvocation = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<InvocationExpressionSyntax>().First();
ElyesFakhar marked this conversation as resolved.
Show resolved Hide resolved

// Register a code action that will invoke the fix.
context.RegisterCodeFix(
CodeAction.Create(
title: "Use Cast instead of Select to cast",
createChangedDocument: c => RefactorAsync(context.Document, diagnostic, c),
equivalenceKey: "UseCastInsteadOfSelectToCast"),
diagnostic);
}

private async Task<Document> RefactorAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

// Find the Select invocation node
var diagnosticSpan = diagnostic.Location.SourceSpan;
var selectInvocation = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<InvocationExpressionSyntax>().First();

// Create a new Cast invocation node
var memberAccess = selectInvocation.Expression as MemberAccessExpressionSyntax;
var castInvocation = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
memberAccess.Expression,
SyntaxFactory.IdentifierName("Cast")));

// Replace the old Select invocation with the new Cast invocation
var newRoot = root.ReplaceNode(selectInvocation, castInvocation);

// Return the new document
return document.WithSyntaxRoot(newRoot);
}
}
}
46 changes: 46 additions & 0 deletions src/EcoCode.Core/Analyzers/EC90.UseCastInsteadOfSelectToCast.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace EcoCode.Core.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UseCastInsteadOfSelectToCast : DiagnosticAnalyzer
{
public static DiagnosticDescriptor Descriptor { get; } = Rule.CreateDescriptor(
id: Rule.Ids.EC90_UseCastInsteadOfSelectToCast,
title: "Use Cast instead of Select to cast",
message: "A Select method is used for casting instead of the Cast method",
category: Rule.Categories.Performance,
severity: DiagnosticSeverity.Warning,
description: "The Cast method should be used instead of Select for casting to improve performance.");

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

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(static context => AnalyzeSelectNode(context), SyntaxKind.InvocationExpression);
}

private static void AnalyzeSelectNode(SyntaxNodeAnalysisContext context)
{
var invocation = (InvocationExpressionSyntax)context.Node;
var memberAccess = invocation.Expression as MemberAccessExpressionSyntax;

// Check if the method being called is 'Select'
if (memberAccess?.Name.Identifier.Text != "Select") return;

// Check if the argument to 'Select' is a cast operation
var argument = invocation.ArgumentList.Arguments.FirstOrDefault();
var lambda = argument?.Expression as SimpleLambdaExpressionSyntax;
var cast = lambda?.Body as CastExpressionSyntax;

if (cast != null)
{
// Report a diagnostic if 'Select' is used for casting
var diagnostic = Diagnostic.Create(Descriptor, invocation.GetLocation());
context.ReportDiagnostic(diagnostic);
}
ElyesFakhar marked this conversation as resolved.
Show resolved Hide resolved
}

}
}
1 change: 1 addition & 0 deletions src/EcoCode.Core/Models/Rule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static class Ids
public const string EC87_UseCollectionIndexer = "EC87";
public const string EC88_DisposeResourceAsynchronously = "EC88";
public const string EC89_DoNotPassMutableStructAsRefReadonly = "EC89";
public const string EC90_UseCastInsteadOfSelectToCast = "EC90";
}

/// <summary>Creates a diagnostic descriptor.</summary>
Expand Down
100 changes: 100 additions & 0 deletions src/EcoCode.Tests/Tests/EC90.UseCastInsteadOfSelectToCast.Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using EcoCode.Core.Analyzers;
ElyesFakhar marked this conversation as resolved.
Show resolved Hide resolved

namespace EcoCode.Tests;

[TestClass]
public sealed class UseCastInsteadOfSelectToCastTests
{
private static readonly CodeFixerDlg VerifyAsync = TestRunner.VerifyAsync<UseCastInsteadOfSelectToCast, UseCastInsteadOfSelectToCastFixer>;

[TestMethod]
public async Task EmptyCodeAsync() => await VerifyAsync("").ConfigureAwait(false);

[TestMethod]
public async Task CastMethodUsedForNumberArrayAsync() => await VerifyAsync("""
using System.Linq;
public class Test
{
public void Run()
{
var numbers = new int[] { 1, 2, 3, 4, 5 };
var castedNumbers = numbers.Cast<double>().ToList();

var numbers2 = new int[] { 6, 7, 8, 9, 10 };
var correctlyCastedNumbers = numbers2.Cast<double>().ToList();
}
}
""").ConfigureAwait(false);

[TestMethod]
public async Task CastMethodUsedForStringArrayAsync() => await VerifyAsync(@"
ElyesFakhar marked this conversation as resolved.
Show resolved Hide resolved
using System.Linq;
using System.Collections.Generic;
public class Test
{
public void Run()
{
IEnumerable<string> strings = GetStrings();
var stringsAsObjects = strings.Cast<object>().ToList();
}

private IEnumerable<string> GetStrings()
{
return new List<string> { ""Hello"", ""World"" };
}
}
").ConfigureAwait(false);

[TestMethod]
public async Task CastMethodUsedForEmptyArrayAsync() => await VerifyAsync("""
using System.Linq;
public class Test
{
public void Run()
{
var numbers = new int[] { };
var castedNumbers = numbers.Cast<double>().ToList();
}
}
""").ConfigureAwait(false);

[TestMethod]
public async Task CastMethodUsedForArrayWithNullValuesAsync() => await VerifyAsync(@"
using System.Linq;
public class Test
{
public void Run()
{
var strings = new string[] { ""Hello"", null, ""World"" };
var castedStrings = strings.Cast<object>().ToList();
}
}
").ConfigureAwait(false);


[TestMethod]
public async Task CastMethodUsedForBoolArrayAsync() => await VerifyAsync("""
using System.Linq;
public class Test
{
public void Run()
{
var booleans = new bool[] { true, false, true };
var castedBooleans = booleans.Cast<object>().ToList();
}
}
""").ConfigureAwait(false);

[TestMethod]
public async Task CastMethodUsedForNestedArrayAsync() => await VerifyAsync("""
using System.Linq;
public class Test
{
public void Run()
{
var nestedArrays = new int[][] { new int[] {1, 2}, new int[] {3, 4} };
var castedNestedArrays = nestedArrays.Cast<object>().ToList();
}
}
""").ConfigureAwait(false);
}