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() .Select((kObj, i) => $"\"{(kObj is string s ? FixString(s) : kObj.ToString())}\": \"{(values[i] is string s1 ? FixString(s1) : values[i].ToString())}\"\n") - .OrderBy(static s => s)); + .Order()); } if (!Tools.Has()) diff --git a/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.cs b/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.cs index 57addaaa41..9dac2dddcf 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.cs @@ -759,7 +759,7 @@ namespace BizHawk.Client.EmuHawk private void RemoveWatchMenuItem_Click(object sender, EventArgs e) { if (!WatchListView.AnyRowsSelected) return; - foreach (var index in SelectedIndices.OrderByDescending(static i => i).ToList()) _watches.RemoveAt(index); + foreach (var index in SelectedIndices.OrderDescending().ToList()) _watches.RemoveAt(index); WatchListView.RowCount = _watches.Count; GeneralUpdate(); UpdateWatchCount(); diff --git a/src/BizHawk.Common/Extensions/CollectionExtensions.cs b/src/BizHawk.Common/Extensions/CollectionExtensions.cs index 3c0813bfd3..f2d6915239 100644 --- a/src/BizHawk.Common/Extensions/CollectionExtensions.cs +++ b/src/BizHawk.Common/Extensions/CollectionExtensions.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; namespace BizHawk.Common.CollectionExtensions { @@ -153,6 +154,16 @@ namespace BizHawk.Common.CollectionExtensions return null; } + /// shorthand for this.OrderBy(static e => e), backported from .NET 7 + public static IOrderedEnumerable Order(this IEnumerable source) + where T : IComparable + => source.OrderBy(ReturnSelf); + + /// shorthand for this.OrderByDescending(static e => e), backported from .NET 7 + public static IOrderedEnumerable OrderDescending(this IEnumerable source) + where T : IComparable + => source.OrderByDescending(ReturnSelf); + /// /// /// (This is an extension method which reimplements for other collections. @@ -180,6 +191,10 @@ namespace BizHawk.Common.CollectionExtensions return c - list.Count; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T ReturnSelf(this T self) + => self; + public static bool IsSortedAsc(this IReadOnlyList list) where T : IComparable { diff --git a/src/BizHawk.Emulation.Common/VSystemID.cs b/src/BizHawk.Emulation.Common/VSystemID.cs index 7614078862..7ae233eeac 100644 --- a/src/BizHawk.Emulation.Common/VSystemID.cs +++ b/src/BizHawk.Emulation.Common/VSystemID.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using BizHawk.Common.CollectionExtensions; + namespace BizHawk.Emulation.Common { /// @@ -74,8 +76,7 @@ namespace BizHawk.Emulation.Common private static List AllSysIDs => _allSysIDs ??= typeof(Raw).GetFields(BindingFlags.Public | BindingFlags.Static) .Select(x => (string) x.GetRawConstantValue()) - .OrderBy(s => s) - .ToList(); + .Order().ToList(); /// iff it's in the valid list, else public static string? Validate(string sysID)