Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -486,3 +486,6 @@ $RECYCLE.BIN/

**/Container
**/Publish

# Claude Code local configuration
.claude/
1 change: 1 addition & 0 deletions creedengo-csharp.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
</Folder>
<Project Path="src/Creedengo.Core/Creedengo.Core.csproj" />
<Project Path="src/Creedengo.Package/Creedengo.Package.csproj" />
<Project Path="src/Creedengo.Sandbox/Creedengo.Sandbox.csproj" />
<Project Path="src/Creedengo.Tests/Creedengo.Tests.csproj" />
<Project Path="src/Creedengo.Tool/Creedengo.Tool.csproj" />
</Solution>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace Creedengo.Core.Analyzers;

/// <summary>GCI98 fixer: Use 'ThenBy' instead of 'OrderBy'.</summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseThenByInsteadOfOrderByFixer)), Shared]
public sealed class UseThenByInsteadOfOrderByFixer : CodeFixProvider
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds => _fixableDiagnosticIds;
private static readonly ImmutableArray<string> _fixableDiagnosticIds =
ImmutableArray.Create(UseThenByInsteadOfOrderBy.Descriptor.Id);

/// <inheritdoc/>
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

/// <inheritdoc/>
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;

Comment thread
QzLP2P marked this conversation as resolved.
context.RegisterCodeFix(
CodeAction.Create(
title: "Use 'ThenBy' instead of 'OrderBy'",
createChangedDocument: _ => FixAsync(context.Document, memberAccess, nameSyntax),
equivalenceKey: "UseThenByInsteadOfOrderBy"),
diagnostic);
}
}

private static Task<Document> 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);
}
}
51 changes: 51 additions & 0 deletions src/Creedengo.Core/Analyzers/GCI98.UseThenByInsteadOfOrderBy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace Creedengo.Core.Analyzers;

/// <summary>GCI98: Use 'ThenBy' instead of 'OrderBy' in a LINQ sort chain.</summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UseThenByInsteadOfOrderBy : DiagnosticAnalyzer
{
private static readonly ImmutableArray<SyntaxKind> InvocationExpressions =
ImmutableArray.Create(SyntaxKind.InvocationExpression);

/// <summary>The diagnostic descriptor.</summary>
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.");

/// <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.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()));
}
}
3 changes: 2 additions & 1 deletion src/Creedengo.Core/Models/Rule.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Creedengo.Core.Models;
namespace Creedengo.Core.Models;

internal static class Rule
{
Expand Down Expand Up @@ -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";
}

/// <summary>Creates a diagnostic descriptor.</summary>
Expand Down
14 changes: 14 additions & 0 deletions src/Creedengo.Sandbox/Creedengo.Sandbox.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
Comment thread
vdebellabre marked this conversation as resolved.
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Creedengo.Core\Creedengo.Core.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions src/Creedengo.Sandbox/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Console.WriteLine("Write your own class sandbox");
19 changes: 19 additions & 0 deletions src/Creedengo.Sandbox/RCS1200Sandbox.cs
Original file line number Diff line number Diff line change
@@ -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);

}

}
Loading