diff --git a/src/Creedengo.Core/Analyzers/GCI2334.TrueForAllInsteadOfAll.Fixer.cs b/src/Creedengo.Core/Analyzers/GCI2334.TrueForAllInsteadOfAll.Fixer.cs new file mode 100644 index 00000000..fc697562 --- /dev/null +++ b/src/Creedengo.Core/Analyzers/GCI2334.TrueForAllInsteadOfAll.Fixer.cs @@ -0,0 +1,49 @@ +namespace Creedengo.Core.Analyzers; + +/// GCI2334: Use 'TrueForAll' instead of 'All' on a List. +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(TrueForAllInsteadOfAllFixer)), Shared] +public sealed class TrueForAllInsteadOfAllFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds => _fixableDiagnosticIds; + private static readonly ImmutableArray _fixableDiagnosticIds = ImmutableArray.Create(TrueForAllInsteadOfAll.Descriptor.Id); + + /// + [ExcludeFromCodeCoverage] + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + 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 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(); + } +} diff --git a/src/Creedengo.Core/Analyzers/GCI2334.TrueForAllInsteadOfAll.cs b/src/Creedengo.Core/Analyzers/GCI2334.TrueForAllInsteadOfAll.cs new file mode 100644 index 00000000..7e00abe5 --- /dev/null +++ b/src/Creedengo.Core/Analyzers/GCI2334.TrueForAllInsteadOfAll.cs @@ -0,0 +1,61 @@ +namespace Creedengo.Core.Analyzers; + +/// GCI2334: Use 'TrueForAll' instead of 'All' on a List. +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class TrueForAllInsteadOfAll : DiagnosticAnalyzer +{ + private static readonly ImmutableArray SyntaxKinds = ImmutableArray.Create(SyntaxKind.InvocationExpression); + + /// The diagnostic descriptor. + public static DiagnosticDescriptor Descriptor { get; } = Rule.CreateDescriptor( + id: Rule.Ids.GCI2334_TrueForAllInsteadOfAll, + title: "Use 'TrueForAll' instead of 'All'", + message: "Use 'List.TrueForAll' instead of 'Enumerable.All' for better performance", + category: Rule.Categories.Performance, + severity: DiagnosticSeverity.Warning, + description: "Prefer 'List.TrueForAll' over the LINQ 'Enumerable.All' extension method when the source is a 'List', as it avoids LINQ overhead."); + + /// + public override ImmutableArray SupportedDiagnostics => _supportedDiagnostics; + private static readonly ImmutableArray _supportedDiagnostics = ImmutableArray.Create(Descriptor); + + /// + 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())); + } +} diff --git a/src/Creedengo.Core/Models/Rule.cs b/src/Creedengo.Core/Models/Rule.cs index 6cc5d153..9fd497a3 100644 --- a/src/Creedengo.Core/Models/Rule.cs +++ b/src/Creedengo.Core/Models/Rule.cs @@ -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"; } diff --git a/src/Creedengo.Tests/Tests/GCI2334.TrueForAllInsteadOfAll.Tests.cs b/src/Creedengo.Tests/Tests/GCI2334.TrueForAllInsteadOfAll.Tests.cs new file mode 100644 index 00000000..3270ad33 --- /dev/null +++ b/src/Creedengo.Tests/Tests/GCI2334.TrueForAllInsteadOfAll.Tests.cs @@ -0,0 +1,205 @@ +namespace Creedengo.Tests.Tests; + +[TestClass] +public sealed class TrueForAllInsteadOfAllTests +{ + private static readonly CodeFixerDlg VerifyAsync = TestRunner.VerifyAsync; + + [TestMethod] + public Task EmptyCodeAsync() => VerifyAsync(""); + + // --- No-diagnostic cases --- + + [TestMethod] // .All() on an array — source type is not List + 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 — source type is not List + public Task AllOnIEnumerableNoDiagnosticAsync() => VerifyAsync(""" + using System.Collections.Generic; + using System.Linq; + + public class Test + { + public void Run(IEnumerable values) + { + bool result = values.All(x => x > 0); + } + } + """); + + [TestMethod] // .All() on IList — source type is not List + public Task AllOnIListNoDiagnosticAsync() => VerifyAsync(""" + using System.Collections.Generic; + using System.Linq; + + public class Test + { + public void Run(IList 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 { 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 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 { 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 { 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 { 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 { 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 items) + { + return items.[|All|](s => s.Length > 0); + } + } + """, + """ + using System.Collections.Generic; + using System.Linq; + + public class Test + { + public bool Run(List 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 { 1, 2, 3 }; + var list2 = new List { "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 { 1, 2, 3 }; + var list2 = new List { "a", "b" }; + bool a = list1.TrueForAll(x => x > 0); + bool b = list2.TrueForAll(s => s.Length > 0); + } + } + """); +}