From 521e7981bcc8a871587b916a6aa88abdfd884c1e Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Thu, 29 Jan 2026 01:38:52 +0100 Subject: [PATCH] Fix ValidateMemberAnalyzer performance The analyzer was built to go off syntax nodes. This (AFAICT) meant that the SemanticModel had to be recalculated for every single invocation. If you don't know what the above means: it basically means the compiler has to re-analyze the entire file. Fix this by moving it to an operation analyzer so the compiler can properly cache the semantic model. --- RELEASE-NOTES.md | 2 +- Robust.Analyzers/ValidateMemberAnalyzer.cs | 34 +++++++++------------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index c70525218..0465762f6 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -47,7 +47,7 @@ END TEMPLATE--> ### Other -*None yet* +* Fixed `ValidateMemberAnalyzer` taking a ridiculous amount of compile time. ### Internal diff --git a/Robust.Analyzers/ValidateMemberAnalyzer.cs b/Robust.Analyzers/ValidateMemberAnalyzer.cs index 235b230b8..447048dcb 100644 --- a/Robust.Analyzers/ValidateMemberAnalyzer.cs +++ b/Robust.Analyzers/ValidateMemberAnalyzer.cs @@ -1,8 +1,6 @@ #nullable enable using System.Collections.Immutable; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; using Robust.Roslyn.Shared; @@ -29,16 +27,15 @@ public sealed class ValidateMemberAnalyzer : DiagnosticAnalyzer { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); - context.RegisterSyntaxNodeAction(AnalyzeExpression, SyntaxKind.InvocationExpression); + context.RegisterOperationAction(AnalyzeOperation, OperationKind.Invocation); } - private void AnalyzeExpression(SyntaxNodeAnalysisContext context) + private void AnalyzeOperation(OperationAnalysisContext context) { - if (context.Node is not InvocationExpressionSyntax node) + if (context.Operation is not IInvocationOperation node) return; - if (context.SemanticModel.GetSymbolInfo(node.Expression).Symbol is not IMethodSymbol methodSymbol) - return; + var methodSymbol = node.TargetMethod; // We need at least one type argument for context if (methodSymbol.TypeArguments.Length < 1) @@ -48,16 +45,12 @@ public sealed class ValidateMemberAnalyzer : DiagnosticAnalyzer if (methodSymbol.TypeArguments[0] is not INamedTypeSymbol targetType) return; - // We defer building this set until we need it later, so we don't have to build it for every single method invocation! - ImmutableHashSet? members = null; - // Check each parameter of the method - foreach (var parameterContext in node.ArgumentList.Arguments) + foreach (var op in node.Arguments) { - - // Get the symbol for this parameter - if (context.SemanticModel.GetOperation(parameterContext) is not IArgumentOperation op || op.Parameter is null) + if (op.Parameter is null) continue; + var parameterSymbol = op.Parameter.OriginalDefinition; // Make sure the parameter has the ValidateMember attribute @@ -66,15 +59,12 @@ public sealed class ValidateMemberAnalyzer : DiagnosticAnalyzer // Find the value passed for this parameter. // We use GetConstantValue to resolve compile-time values - i.e. the result of nameof() - if (context.SemanticModel.GetConstantValue(parameterContext.Expression).Value is not string fieldName) + if (op.Value.ConstantValue is not { HasValue: true, Value: string fieldName}) continue; - // Get a set containing all the members of the target type and its ancestors - members ??= targetType.GetBaseTypesAndThis().SelectMany(n => n.GetMembers()).ToImmutableHashSet(SymbolEqualityComparer.Default); - // Check each member of the target type to see if it matches our passed in value var found = false; - foreach (var member in members) + foreach (var member in targetType.GetMembers()) { if (member.Name == fieldName) { @@ -84,12 +74,14 @@ public sealed class ValidateMemberAnalyzer : DiagnosticAnalyzer } // If we didn't find it, report the violation if (!found) + { context.ReportDiagnostic(Diagnostic.Create( ValidateMemberDescriptor, - parameterContext.GetLocation(), + op.Syntax.GetLocation(), fieldName, targetType.Name - )); + )); + } } } }