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); }); } }