diff --git a/Common.ruleset b/Common.ruleset
index 5b8d7aa745..96a02ae30f 100644
--- a/Common.ruleset
+++ b/Common.ruleset
@@ -31,6 +31,9 @@
+
+
+
diff --git a/ExternalProjects/BizHawk.Analyzer/OrderBySelfAnalyzer.cs b/ExternalProjects/BizHawk.Analyzer/OrderBySelfAnalyzer.cs
new file mode 100644
index 0000000000..8fc6539fa4
--- /dev/null
+++ b/ExternalProjects/BizHawk.Analyzer/OrderBySelfAnalyzer.cs
@@ -0,0 +1,70 @@
+namespace BizHawk.Analyzers;
+
+using System.Collections.Immutable;
+using System.Linq;
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class OrderBySelfAnalyzer : DiagnosticAnalyzer
+{
+ private static readonly DiagnosticDescriptor DiagUseOrderBySelfExt = new(
+ id: "BHI3101",
+ title: "Use .Order()/.OrderDescending() shorthand",
+ messageFormat: "Replace .OrderBy{0}(e => e) with .Order{0}()",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(DiagUseOrderBySelfExt);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterCompilationStartAction(initContext =>
+ {
+ if (initContext.Compilation.GetTypeByMetadataName("BizHawk.Common.CollectionExtensions.CollectionExtensions") is null) return; // project does not have BizHawk.Common dependency
+ var linqExtClassSym = initContext.Compilation.GetTypeByMetadataName("System.Linq.Enumerable")!;
+ var orderByAscSym = linqExtClassSym.GetMembers("OrderBy").Cast().First(sym => sym.Parameters.Length is 2);
+ var orderByDescSym = linqExtClassSym.GetMembers("OrderByDescending").Cast().First(sym => sym.Parameters.Length is 2);
+ initContext.RegisterOperationAction(
+ oac =>
+ {
+ static bool IsSelfReturnLambda(AnonymousFunctionExpressionSyntax afes)
+ {
+ ParameterSyntax paramSyn;
+ switch (afes)
+ {
+ case AnonymousMethodExpressionSyntax ames: // banned in BizHawk but included for completeness
+ if ((ames.ParameterList?.Parameters)?.Count is not 1) return false;
+ paramSyn = ames.ParameterList.Parameters[0];
+ break;
+ case ParenthesizedLambdaExpressionSyntax ples:
+ if (ples.ParameterList.Parameters.Count is not 1) return false;
+ paramSyn = ples.ParameterList.Parameters[0];
+ break;
+ case SimpleLambdaExpressionSyntax sles:
+ paramSyn = sles.Parameter;
+ break;
+ default:
+ return false;
+ }
+ bool Matches(IdentifierNameSyntax ins)
+ => ins.Identifier.ValueText == paramSyn.Identifier.ValueText;
+ if (afes.ExpressionBody is not null) return afes.ExpressionBody is IdentifierNameSyntax ins && Matches(ins);
+ return afes.Block!.Statements.Count is 1 && afes.Block.Statements[0] is ReturnStatementSyntax { Expression: IdentifierNameSyntax ins1 } && Matches(ins1);
+ }
+ var operation = (IInvocationOperation) oac.Operation;
+ var calledSym = operation.TargetMethod.ConstructedFrom;
+ if (!(orderByAscSym!.Matches(calledSym) || orderByDescSym!.Matches(calledSym))) return;
+ if (((ArgumentSyntax) operation.Arguments[1].Syntax).Expression is not AnonymousFunctionExpressionSyntax afes) return;
+ if (IsSelfReturnLambda(afes)) oac.ReportDiagnostic(Diagnostic.Create(DiagUseOrderBySelfExt, afes.GetLocation(), orderByDescSym.Matches(calledSym) ? "Descending" : string.Empty));
+ },
+ OperationKind.Invocation);
+ });
+ }
+}
diff --git a/References/BizHawk.Analyzer.dll b/References/BizHawk.Analyzer.dll
index aac0933a98..b3a7549b29 100644
Binary files a/References/BizHawk.Analyzer.dll and b/References/BizHawk.Analyzer.dll differ
diff --git a/src/BizHawk.Client.Common/ArgParser.cs b/src/BizHawk.Client.Common/ArgParser.cs
index bd5a56b6b7..5bff6e25df 100644
--- a/src/BizHawk.Client.Common/ArgParser.cs
+++ b/src/BizHawk.Client.Common/ArgParser.cs
@@ -5,6 +5,8 @@ using System.Collections.Generic;
using System.Linq;
using System.IO;
+using BizHawk.Common.CollectionExtensions;
+
namespace BizHawk.Client.Common
{
///
@@ -86,7 +88,7 @@ namespace BizHawk.Client.Common
}
// automatically set dump length to maximum frame
- autoDumpLength = currAviWriterFrameList.OrderBy(x => x).Last();
+ autoDumpLength = currAviWriterFrameList.Order().Last();
}
else if (argDowncased.StartsWith("--version"))
{
diff --git a/src/BizHawk.Client.Common/FilesystemFilterSet.cs b/src/BizHawk.Client.Common/FilesystemFilterSet.cs
index 9437d40f13..dfa1cb730c 100644
--- a/src/BizHawk.Client.Common/FilesystemFilterSet.cs
+++ b/src/BizHawk.Client.Common/FilesystemFilterSet.cs
@@ -3,6 +3,8 @@
using System.Collections.Generic;
using System.Linq;
+using BizHawk.Common.CollectionExtensions;
+
namespace BizHawk.Client.Common
{
public sealed class FilesystemFilterSet
@@ -27,7 +29,7 @@ namespace BizHawk.Client.Common
/// call other overload (omit ) to not prepend combined entry, return value is a valid Filter for Save-/OpenFileDialog
public string ToString(string combinedEntryDesc, bool addAllFilesEntry = true)
{
- _allSer ??= FilesystemFilter.SerializeEntry(combinedEntryDesc, Filters.SelectMany(static filter => filter.Extensions).Distinct().OrderBy(static s => s).ToList());
+ _allSer ??= FilesystemFilter.SerializeEntry(combinedEntryDesc, Filters.SelectMany(static filter => filter.Extensions).Distinct().Order().ToList());
return $"{_allSer}|{ToString(addAllFilesEntry)}";
}
diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.Editing.cs b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.Editing.cs
index 2e0de988ec..a4e6d8ef13 100644
--- a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.Editing.cs
+++ b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.Editing.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
+using BizHawk.Common.CollectionExtensions;
using BizHawk.Emulation.Common;
namespace BizHawk.Client.Common
@@ -117,8 +118,7 @@ namespace BizHawk.Client.Common
// and process each block independently
List framesToDelete = frames
.Where(fr => fr >= 0 && fr < InputLogLength)
- .OrderBy(fr => fr)
- .ToList();
+ .Order().ToList();
// f is the current index for framesToDelete
int f = 0;
int numDeleted = 0;
diff --git a/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs b/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs
index 844af118d0..264e96678c 100644
--- a/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs
+++ b/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using BizHawk.Common;
+using BizHawk.Common.CollectionExtensions;
using BizHawk.Emulation.Common;
namespace BizHawk.Client.Common
@@ -244,7 +245,7 @@ namespace BizHawk.Client.Common
// Enumerate all reserved states in reverse order
private IEnumerable ReservedStates()
{
- foreach (var key in _reserved.Keys.OrderByDescending(k => k))
+ foreach (var key in _reserved.Keys.OrderDescending())
{
yield return new StateInfo(key, _reserved[key]);
}
diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/ConsoleLuaLibrary.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/ConsoleLuaLibrary.cs
index 2d6a04fc56..853acce6b4 100644
--- a/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/ConsoleLuaLibrary.cs
+++ b/src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/ConsoleLuaLibrary.cs
@@ -3,6 +3,8 @@ using System.Linq;
using System.Text;
using BizHawk.Client.Common;
+using BizHawk.Common.CollectionExtensions;
+
using NLua;
namespace BizHawk.Client.EmuHawk
@@ -82,7 +84,7 @@ namespace BizHawk.Client.EmuHawk
return string.Concat(keyObjs.Cast