Skip to content
106 changes: 106 additions & 0 deletions src/EcoCode.Core/Analyzers/EC90.UseCastInsteadOfSelectToCast.Fixer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
namespace EcoCode.Core.Analyzers
{
using System.Linq;
using System.Xml.Linq;

/// <summary>
/// Provides a code fix for the UseCastInsteadOfSelectToCast analyzer.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseCastInsteadOfSelectToCastFixer)), Shared]
public sealed class UseCastInsteadOfSelectToCastFixer : CodeFixProvider
{
/// <summary>
/// Gets the diagnostic IDs that this provider can fix.
/// </summary>
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(Rule.Ids.EC90_UseCastInsteadOfSelectToCast);

/// <summary>
/// Gets the provider that can fix all occurrences of diagnostics.
/// </summary>
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

/// <summary>
/// Registers the code fixes provided by this provider.
/// </summary>

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

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

var nodeToFix = root.FindNode(context.Span, getInnermostNodeForTie: true);
context.RegisterCodeFix(
CodeAction.Create(
title: "Use nameof",
createChangedDocument: token => RefactorAsync(document, context.Diagnostics.First(), token),
equivalenceKey: "Use nameof"),
context.Diagnostics);
}

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

if (root == null)
{
return document;
}

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

if (selectInvocation is null)
{
return document;
}

// Get the lambda expression from the Select method call
var selectArgument = selectInvocation.ArgumentList.Arguments[0];
var lambdaExpression = selectArgument.Expression as SimpleLambdaExpressionSyntax;

if (lambdaExpression is null)
{
return document;
}

// Get the type from the cast expression within the lambda expression
var castExpression = lambdaExpression.Body as CastExpressionSyntax;

if (castExpression is null)
{
return document;
}

var castType = castExpression.Type;

// Create a new Cast invocation node
var memberAccess = selectInvocation.Expression as MemberAccessExpressionSyntax;
if (memberAccess?.Expression == null)
{
return document;
}

var castInvocation = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
memberAccess.Expression,
SyntaxFactory.GenericName(
SyntaxFactory.Identifier("Cast"),
SyntaxFactory.TypeArgumentList(SyntaxFactory.SingletonSeparatedList(castType))
)
)
);

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

// Return the new document
return document.WithSyntaxRoot(newRoot);
}

}
}
41 changes: 41 additions & 0 deletions src/EcoCode.Core/Analyzers/EC90.UseCastInsteadOfSelectToCast.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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
if ((invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression as SimpleLambdaExpressionSyntax)?.Body is CastExpressionSyntax)
{
context.ReportDiagnostic(Diagnostic.Create(Descriptor, invocation.GetLocation()));
}

}

}
}
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";
public const string EC91_UseWhereBeforeOrderBy = "EC91";
}

Expand Down
136 changes: 136 additions & 0 deletions src/EcoCode.Tests/Tests/EC90.UseCastInsteadOfSelectToCast.Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using EcoCode.Core.Analyzers;

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 SelectMethodUsedForStringArrayAsync() => await VerifyAsync("""
using System.Linq;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
var strings = GetStrings();
var stringsAsObjects = [|strings.Select(s => (object)s)|].ToList();
}

private static IEnumerable<string> GetStrings()
{
return new List<string> { "Hello", "World" };
}
}
""", """
using System.Linq;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
var strings = GetStrings();
var stringsAsObjects = strings.Cast<object>().ToList();
}

private static IEnumerable<string> GetStrings()
{
return new List<string> { "Hello", "World" };
}
}
""").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("""
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);
}