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

/// <summary>GCI2334: Use 'TrueForAll' instead of 'All' on a List.</summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(TrueForAllInsteadOfAllFixer)), Shared]
public sealed class TrueForAllInsteadOfAllFixer : CodeFixProvider
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds => _fixableDiagnosticIds;
private static readonly ImmutableArray<string> _fixableDiagnosticIds = ImmutableArray.Create(TrueForAllInsteadOfAll.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: "Use 'TrueForAll' instead of 'All'",
createChangedDocument: token => RefactorAsync(context.Document, nodeToFix, token),
equivalenceKey: "Use 'TrueForAll' instead of 'All'"),
context.Diagnostics);
}

private static async Task<Document> RefactorAsync(Document document, SyntaxNode nodeToFix, CancellationToken token)
{
// nodeToFix is the IdentifierNameSyntax "All"; climb up to the InvocationExpressionSyntax
if (nodeToFix.Parent is not MemberAccessExpressionSyntax memberAccess ||
memberAccess.Parent is not InvocationExpressionSyntax invocation)
{
return document;
}

var editor = await DocumentEditor.CreateAsync(document, token).ConfigureAwait(false);

var newName = SyntaxFactory.IdentifierName("TrueForAll").WithTriviaFrom(memberAccess.Name);
editor.ReplaceNode(invocation, invocation.WithExpression(memberAccess.WithName(newName)));

return editor.GetChangedDocument();
}
}
61 changes: 61 additions & 0 deletions src/Creedengo.Core/Analyzers/GCI2334.TrueForAllInsteadOfAll.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
namespace Creedengo.Core.Analyzers;

/// <summary>GCI2334: Use 'TrueForAll' instead of 'All' on a List.</summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class TrueForAllInsteadOfAll : DiagnosticAnalyzer
{
private static readonly ImmutableArray<SyntaxKind> SyntaxKinds = ImmutableArray.Create(SyntaxKind.InvocationExpression);

/// <summary>The diagnostic descriptor.</summary>
public static DiagnosticDescriptor Descriptor { get; } = Rule.CreateDescriptor(
id: Rule.Ids.GCI2334_TrueForAllInsteadOfAll,
title: "Use 'TrueForAll' instead of 'All'",
message: "Use 'List<T>.TrueForAll' instead of 'Enumerable.All' for better performance",
category: Rule.Categories.Performance,
severity: DiagnosticSeverity.Warning,
description: "Prefer 'List<T>.TrueForAll' over the LINQ 'Enumerable.All' extension method when the source is a 'List<T>', as it avoids LINQ overhead.");

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

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(static context =>
{
var enumerableType = context.Compilation.GetTypeByMetadataName("System.Linq.Enumerable");
var listType = context.Compilation.GetTypeByMetadataName("System.Collections.Generic.List`1");
if (enumerableType is null || listType is null) return;

context.RegisterSyntaxNodeAction(
nodeContext => AnalyzeInvocation(nodeContext, enumerableType, listType),
SyntaxKinds);
});
}

private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedTypeSymbol enumerableType, INamedTypeSymbol listType)
{
var invocationExpr = (InvocationExpressionSyntax)context.Node;

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

if (memberAccess.Name.Identifier.Text != nameof(Enumerable.All))
return;

if (context.SemanticModel.GetSymbolInfo(invocationExpr).Symbol is not IMethodSymbol method)
return;

if (!method.IsExtensionMethod || !SymbolEqualityComparer.Default.Equals(method.ContainingType, enumerableType))
return;

var receiverType = context.SemanticModel.GetTypeInfo(memberAccess.Expression).Type;
if (receiverType is null || !SymbolEqualityComparer.Default.Equals(receiverType.OriginalDefinition, listType))
return;

context.ReportDiagnostic(Diagnostic.Create(Descriptor, memberAccess.Name.GetLocation()));
}
Comment on lines +56 to +60
}
1 change: 1 addition & 0 deletions src/Creedengo.Core/Models/Rule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public static class Ids
public const string GCI96_UseEventArgsDotEmpty = "GCI96";
public const string GCI2508_RemoveUselessToStringCall = "GCI2508";
public const string GCI2333_RemoveRedundantToCharArrayCall = "GCI2333";
public const string GCI2334_TrueForAllInsteadOfAll = "GCI2334";
public const string GCI98_UseThenByInsteadOfOrderBy = "GCI98";
}

Expand Down
205 changes: 205 additions & 0 deletions src/Creedengo.Tests/Tests/GCI2334.TrueForAllInsteadOfAll.Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
namespace Creedengo.Tests.Tests;

[TestClass]
public sealed class TrueForAllInsteadOfAllTests
{
private static readonly CodeFixerDlg VerifyAsync = TestRunner.VerifyAsync<TrueForAllInsteadOfAll, TrueForAllInsteadOfAllFixer>;

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

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

[TestMethod] // .All() on an array — source type is not List<T>
public Task AllOnArrayNoDiagnosticAsync() => VerifyAsync("""
using System.Linq;

public class Test
{
public void Run()
{
int[] arr = { 1, 2, 3 };
bool result = arr.All(x => x > 0);
}
}
""");

[TestMethod] // .All() on IEnumerable<T> — source type is not List<T>
public Task AllOnIEnumerableNoDiagnosticAsync() => VerifyAsync("""
using System.Collections.Generic;
using System.Linq;

public class Test
{
public void Run(IEnumerable<int> values)
{
bool result = values.All(x => x > 0);
}
}
""");

[TestMethod] // .All() on IList<T> — source type is not List<T>
public Task AllOnIListNoDiagnosticAsync() => VerifyAsync("""
using System.Collections.Generic;
using System.Linq;

public class Test
{
public void Run(IList<int> values)
{
bool result = values.All(x => x > 0);
}
}
""");

[TestMethod] // .TrueForAll() already used — no LINQ call to flag
public Task TrueForAllAlreadyUsedNoDiagnosticAsync() => VerifyAsync("""
using System.Collections.Generic;

public class Test
{
public void Run()
{
var list = new List<int> { 1, 2, 3 };
bool result = list.TrueForAll(x => x > 0);
}
}
""");

[TestMethod] // custom type with an All method — not from Enumerable
public Task AllOnCustomTypeWithAllMethodNoDiagnosticAsync() => VerifyAsync("""
public class MyCollection
{
public bool All(System.Func<int, bool> predicate) => true;
}

public class Test
{
public void Run()
{
var col = new MyCollection();
bool result = col.All(x => x > 0);
}
}
""");

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

[TestMethod]
public Task AllOnListWithLambdaShouldUseTrueForAllAsync() => VerifyAsync("""
using System.Collections.Generic;
using System.Linq;

public class Test
{
public void Run()
{
var list = new List<int> { 1, 2, 3 };
bool result = list.[|All|](x => x > 0);
}
}
""",
"""
using System.Collections.Generic;
using System.Linq;

public class Test
{
public void Run()
{
var list = new List<int> { 1, 2, 3 };
bool result = list.TrueForAll(x => x > 0);
}
}
""");

[TestMethod]
public Task AllOnListWithMethodGroupShouldUseTrueForAllAsync() => VerifyAsync("""
using System.Collections.Generic;
using System.Linq;

public class Test
{
private static bool IsPositive(int x) => x > 0;

public void Run()
{
var list = new List<int> { 1, 2, 3 };
bool result = list.[|All|](IsPositive);
}
}
""",
"""
using System.Collections.Generic;
using System.Linq;

public class Test
{
private static bool IsPositive(int x) => x > 0;

public void Run()
{
var list = new List<int> { 1, 2, 3 };
bool result = list.TrueForAll(IsPositive);
}
}
""");

[TestMethod]
public Task AllOnListParameterShouldUseTrueForAllAsync() => VerifyAsync("""
using System.Collections.Generic;
using System.Linq;

public class Test
{
public bool Run(List<string> items)
{
return items.[|All|](s => s.Length > 0);
}
}
""",
"""
using System.Collections.Generic;
using System.Linq;

public class Test
{
public bool Run(List<string> items)
{
return items.TrueForAll(s => s.Length > 0);
}
}
""");

[TestMethod]
public Task AllOnMultipleListsShouldUseTrueForAllAsync() => VerifyAsync("""
using System.Collections.Generic;
using System.Linq;

public class Test
{
public void Run()
{
var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<string> { "a", "b" };
bool a = list1.[|All|](x => x > 0);
bool b = list2.[|All|](s => s.Length > 0);
}
}
""",
"""
using System.Collections.Generic;
using System.Linq;

public class Test
{
public void Run()
{
var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<string> { "a", "b" };
bool a = list1.TrueForAll(x => x > 0);
bool b = list2.TrueForAll(s => s.Length > 0);
}
}
""");
}