#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; namespace Robust.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class ValidateMemberAnalyzer : DiagnosticAnalyzer { private const string ValidateMemberType = "Robust.Shared.Analyzers.ValidateMemberAttribute"; private static readonly DiagnosticDescriptor ValidateMemberDescriptor = new( Diagnostics.IdValidateMember, "Invalid member name", "{0} is not a member of {1}", "Usage", DiagnosticSeverity.Error, true, "Be sure the type and member name are correct."); public override ImmutableArray SupportedDiagnostics => [ValidateMemberDescriptor]; public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.RegisterSyntaxNodeAction(AnalyzeExpression, SyntaxKind.InvocationExpression); } private void AnalyzeExpression(SyntaxNodeAnalysisContext context) { if (context.Node is not InvocationExpressionSyntax node) return; if (context.SemanticModel.GetSymbolInfo(node.Expression).Symbol is not IMethodSymbol methodSymbol) return; // We need at least one type argument for context if (methodSymbol.TypeArguments.Length < 1) return; // We'll be checking members of the first type argument 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) { // Get the symbol for this parameter if (context.SemanticModel.GetOperation(parameterContext) is not IArgumentOperation op || op.Parameter is null) continue; var parameterSymbol = op.Parameter.OriginalDefinition; // Make sure the parameter has the ValidateMember attribute if (!AttributeHelper.HasAttribute(parameterSymbol, ValidateMemberType, out _)) continue; // 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) 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) { if (member.Name == fieldName) { found = true; break; } } // If we didn't find it, report the violation if (!found) context.ReportDiagnostic(Diagnostic.Create( ValidateMemberDescriptor, parameterContext.GetLocation(), fieldName, targetType.Name )); } } }