diff --git a/.gitignore b/.gitignore index e6d35f75..7bade528 100644 --- a/.gitignore +++ b/.gitignore @@ -486,3 +486,6 @@ $RECYCLE.BIN/ **/Container **/Publish + +# Claude Code local configuration +.claude/ diff --git a/creedengo-csharp.slnx b/creedengo-csharp.slnx index bac26c87..5db9630e 100644 --- a/creedengo-csharp.slnx +++ b/creedengo-csharp.slnx @@ -32,6 +32,7 @@ + diff --git a/src/Creedengo.Core/Analyzers/GCI98.UseThenByInsteadOfOrderBy.Fixer.cs b/src/Creedengo.Core/Analyzers/GCI98.UseThenByInsteadOfOrderBy.Fixer.cs new file mode 100644 index 00000000..62d276e0 --- /dev/null +++ b/src/Creedengo.Core/Analyzers/GCI98.UseThenByInsteadOfOrderBy.Fixer.cs @@ -0,0 +1,49 @@ +namespace Creedengo.Core.Analyzers; + +/// GCI98 fixer: Use 'ThenBy' instead of 'OrderBy'. +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseThenByInsteadOfOrderByFixer)), Shared] +public sealed class UseThenByInsteadOfOrderByFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds => _fixableDiagnosticIds; + private static readonly ImmutableArray _fixableDiagnosticIds = + ImmutableArray.Create(UseThenByInsteadOfOrderBy.Descriptor.Id); + + /// + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + if (await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) is not { } root) + return; + + foreach (var diagnostic in context.Diagnostics) + { + if (root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) is not SimpleNameSyntax nameSyntax) + continue; + if (nameSyntax.Parent is not MemberAccessExpressionSyntax memberAccess) + continue; + + context.RegisterCodeFix( + CodeAction.Create( + title: "Use 'ThenBy' instead of 'OrderBy'", + createChangedDocument: _ => FixAsync(context.Document, memberAccess, nameSyntax), + equivalenceKey: "UseThenByInsteadOfOrderBy"), + diagnostic); + } + } + + private static Task FixAsync( + Document document, + MemberAccessExpressionSyntax memberAccess, + SimpleNameSyntax nameSyntax) + { + var newIdentifier = nameSyntax.Identifier.Text == "OrderBy" ? "ThenBy" : "ThenByDescending"; + SimpleNameSyntax newNameSyntax = nameSyntax is GenericNameSyntax generic + ? generic.WithIdentifier(SyntaxFactory.Identifier(newIdentifier)) + : SyntaxFactory.IdentifierName(newIdentifier); + var newMemberAccess = memberAccess.WithName(newNameSyntax.WithTriviaFrom(nameSyntax)); + return document.WithUpdatedRoot(memberAccess, newMemberAccess); + } +} diff --git a/src/Creedengo.Core/Analyzers/GCI98.UseThenByInsteadOfOrderBy.cs b/src/Creedengo.Core/Analyzers/GCI98.UseThenByInsteadOfOrderBy.cs new file mode 100644 index 00000000..1c0a67a5 --- /dev/null +++ b/src/Creedengo.Core/Analyzers/GCI98.UseThenByInsteadOfOrderBy.cs @@ -0,0 +1,51 @@ +namespace Creedengo.Core.Analyzers; + +/// GCI98: Use 'ThenBy' instead of 'OrderBy' in a LINQ sort chain. +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseThenByInsteadOfOrderBy : DiagnosticAnalyzer +{ + private static readonly ImmutableArray InvocationExpressions = + ImmutableArray.Create(SyntaxKind.InvocationExpression); + + /// The diagnostic descriptor. + public static DiagnosticDescriptor Descriptor { get; } = Rule.CreateDescriptor( + id: Rule.Ids.GCI98_UseThenByInsteadOfOrderBy, + title: "Use 'ThenBy' instead of 'OrderBy'", + message: "Call 'ThenBy' or 'ThenByDescending' instead of 'OrderBy' or 'OrderByDescending' to preserve the primary sort order", + category: Rule.Categories.Usage, + severity: DiagnosticSeverity.Warning, + description: "Chaining 'OrderBy' or 'OrderByDescending' after another sort operation discards all previous sort keys. Use 'ThenBy' or 'ThenByDescending' to add a secondary sort key while preserving the primary sort."); + + /// + 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.RegisterSyntaxNodeAction(static context => AnalyzeNode(context), InvocationExpressions); + } + + private static void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + var invocation = (InvocationExpressionSyntax)context.Node; + + if (invocation.Expression is not MemberAccessExpressionSyntax + { Name.Identifier.Text: "OrderBy" or "OrderByDescending" } memberAccess) + return; + + if (memberAccess.Expression is not InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name.Identifier.Text: "OrderBy" or "OrderByDescending" or "ThenBy" or "ThenByDescending" + } + }) + 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 943d3e6a..e55b3792 100644 --- a/src/Creedengo.Core/Models/Rule.cs +++ b/src/Creedengo.Core/Models/Rule.cs @@ -1,4 +1,4 @@ -namespace Creedengo.Core.Models; +namespace Creedengo.Core.Models; internal static class Rule { @@ -32,6 +32,7 @@ public static class Ids public const string GCIXX_UnnecessaryAssignment = "GCIXX"; public const string GCI96_UseEventArgsDotEmpty = "GCI96"; public const string GCI2333_RemoveRedundantToCharArrayCall = "GCI2333"; + public const string GCI98_UseThenByInsteadOfOrderBy = "GCI98"; } /// Creates a diagnostic descriptor. diff --git a/src/Creedengo.Sandbox/Creedengo.Sandbox.csproj b/src/Creedengo.Sandbox/Creedengo.Sandbox.csproj new file mode 100644 index 00000000..5588302d --- /dev/null +++ b/src/Creedengo.Sandbox/Creedengo.Sandbox.csproj @@ -0,0 +1,14 @@ + + + + Exe + net10.0 + enable + enable + false + + + + + + diff --git a/src/Creedengo.Sandbox/Program.cs b/src/Creedengo.Sandbox/Program.cs new file mode 100644 index 00000000..dfb8f564 --- /dev/null +++ b/src/Creedengo.Sandbox/Program.cs @@ -0,0 +1 @@ +Console.WriteLine("Write your own class sandbox"); diff --git a/src/Creedengo.Sandbox/RCS1200Sandbox.cs b/src/Creedengo.Sandbox/RCS1200Sandbox.cs new file mode 100644 index 00000000..b5ff3169 --- /dev/null +++ b/src/Creedengo.Sandbox/RCS1200Sandbox.cs @@ -0,0 +1,19 @@ +namespace Creedengo.Sandbox; + +internal class RCS1200Sandbox +{ + public static void Sort() + { + + var items = new (int a, int b)[] { + (1, 2), + (3, 4), + (5, 6) + }; + + var sorted = items.OrderBy(item => item.Item1) + .OrderBy(item => item.Item2); + + } + +} diff --git a/src/Creedengo.Tests/Tests/GCI98.UseThenByInsteadOfOrderBy.Tests.cs b/src/Creedengo.Tests/Tests/GCI98.UseThenByInsteadOfOrderBy.Tests.cs new file mode 100644 index 00000000..ece34a7a --- /dev/null +++ b/src/Creedengo.Tests/Tests/GCI98.UseThenByInsteadOfOrderBy.Tests.cs @@ -0,0 +1,262 @@ +namespace Creedengo.Tests.Tests; + +[TestClass] +public sealed class UseThenByInsteadOfOrderByTests +{ + private static readonly CodeFixerDlg VerifyAsync = + TestRunner.VerifyAsync; + + [TestMethod] + public Task EmptyCodeAsync() => VerifyAsync(""); + + #region No diagnostic + + [TestMethod] + public Task DontWarnOnSingleOrderByAsync() => VerifyAsync(""" + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B)>(); + var result = items.OrderBy(x => x.A); + } + } + """); + + [TestMethod] + public Task DontWarnOnSingleOrderByDescendingAsync() => VerifyAsync(""" + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B)>(); + var result = items.OrderByDescending(x => x.A); + } + } + """); + + [TestMethod] + public Task DontWarnOnOrderByThenByAsync() => VerifyAsync(""" + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B)>(); + var result = items.OrderBy(x => x.A).ThenBy(x => x.B); + } + } + """); + + [TestMethod] + public Task DontWarnOnOrderByDescendingThenByDescendingAsync() => VerifyAsync(""" + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B)>(); + var result = items.OrderByDescending(x => x.A).ThenByDescending(x => x.B); + } + } + """); + + [TestMethod] + public Task DontWarnOnOrderByFollowedBySelectAsync() => VerifyAsync(""" + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B)>(); + var result = items.OrderBy(x => x.A).Select(x => x.B); + } + } + """); + + #endregion + + #region Diagnostic + fix + + [TestMethod] + public Task WarnOnOrderByAfterOrderByAsync() => VerifyAsync(""" + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B)>(); + var query1 = items.OrderBy(x => x.A).[|OrderBy|](x => x.B); + var query2 = items + .OrderBy(x => x.A) + .[|OrderBy|](x => x.B); + } + } + """, """ + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B)>(); + var query1 = items.OrderBy(x => x.A).ThenBy(x => x.B); + var query2 = items + .OrderBy(x => x.A) + .ThenBy(x => x.B); + } + } + """); + + [TestMethod] + public Task WarnOnOrderByDescendingAfterOrderByAsync() => VerifyAsync(""" + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B)>(); + var result = items.OrderBy(x => x.A).[|OrderByDescending|](x => x.B); + } + } + """, """ + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B)>(); + var result = items.OrderBy(x => x.A).ThenByDescending(x => x.B); + } + } + """); + + [TestMethod] + public Task WarnOnOrderByAfterOrderByDescendingAsync() => VerifyAsync(""" + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B)>(); + var result = items.OrderByDescending(x => x.A).[|OrderBy|](x => x.B); + } + } + """, """ + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B)>(); + var result = items.OrderByDescending(x => x.A).ThenBy(x => x.B); + } + } + """); + + [TestMethod] + public Task WarnOnOrderByDescendingAfterOrderByDescendingAsync() => VerifyAsync(""" + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B)>(); + var result = items.OrderByDescending(x => x.A).[|OrderByDescending|](x => x.B); + } + } + """, """ + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B)>(); + var result = items.OrderByDescending(x => x.A).ThenByDescending(x => x.B); + } + } + """); + + [TestMethod] + public Task WarnOnOrderByAfterThenByAsync() => VerifyAsync(""" + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B, int C)>(); + var result = items.OrderBy(x => x.A).ThenBy(x => x.B).[|OrderBy|](x => x.C); + } + } + """, """ + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B, int C)>(); + var result = items.OrderBy(x => x.A).ThenBy(x => x.B).ThenBy(x => x.C); + } + } + """); + + [TestMethod] + public Task WarnOnChainedOrderByAsync() => VerifyAsync(""" + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B, int C)>(); + var result = items.OrderBy(x => x.A).[|OrderBy|](x => x.B).[|OrderBy|](x => x.C); + } + } + """, """ + using System.Linq; + using System.Collections.Generic; + + public static class Test + { + public static void Run() + { + var items = new List<(int A, int B, int C)>(); + var result = items.OrderBy(x => x.A).ThenBy(x => x.B).ThenBy(x => x.C); + } + } + """); + + #endregion +}