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
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

Check failure on line 4 in src/EcoCode.Core/Analyzers/EC90.UseCastInsteadOfSelectToCast.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'UseCastInsteadOfSelectToCast'

Check failure on line 4 in src/EcoCode.Core/Analyzers/EC90.UseCastInsteadOfSelectToCast.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'UseCastInsteadOfSelectToCast'
{
public static DiagnosticDescriptor Descriptor { get; } = Rule.CreateDescriptor(

Check failure on line 6 in src/EcoCode.Core/Analyzers/EC90.UseCastInsteadOfSelectToCast.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'UseCastInsteadOfSelectToCast.Descriptor'

Check failure on line 6 in src/EcoCode.Core/Analyzers/EC90.UseCastInsteadOfSelectToCast.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'UseCastInsteadOfSelectToCast.Descriptor'
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;

Check failure on line 14 in src/EcoCode.Core/Analyzers/EC90.UseCastInsteadOfSelectToCast.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'UseCastInsteadOfSelectToCast.SupportedDiagnostics'

Check failure on line 14 in src/EcoCode.Core/Analyzers/EC90.UseCastInsteadOfSelectToCast.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'UseCastInsteadOfSelectToCast.SupportedDiagnostics'
private static readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics = ImmutableArray.Create(Descriptor);

public override void Initialize(AnalysisContext context)

Check failure on line 17 in src/EcoCode.Core/Analyzers/EC90.UseCastInsteadOfSelectToCast.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'UseCastInsteadOfSelectToCast.Initialize(AnalysisContext)'

Check failure on line 17 in src/EcoCode.Core/Analyzers/EC90.UseCastInsteadOfSelectToCast.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Missing XML comment for publicly visible type or member 'UseCastInsteadOfSelectToCast.Initialize(AnalysisContext)'
{
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;
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 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);
}
Loading