using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Robust.Roslyn.Shared; namespace Robust.Shared.EntitySystemSubscriptionsGenerator; /// /// This analyzer ensures that all methods annotated with the relevant subscription attributes are: /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public class EntitySystemSubscriptionGeneratorErrorAnalyzer : DiagnosticAnalyzer { private static readonly DiagnosticDescriptor BadMethodSignature = new( Diagnostics.IdInvalidAMethodSignatureForGeneratedSubscription, "Invalid method signature", "Method signature is incompatible with required delegate type(s) for \"{0}\". Compatible types are: {1}.", "Usage", DiagnosticSeverity.Error, true ); private static readonly DiagnosticDescriptor NotEntitySystem = new( Diagnostics.IdInvalidContainingTypeForGeneratedSubscription, $"Method not in {KnownTypes.EntitySystemTypeName}", $"Method is declared in type \"{{0}}\" which does not extend {KnownTypes.EntitySystemTypeName}", "Usage", DiagnosticSeverity.Error, true ); private static readonly DiagnosticDescriptor NotPartial = new( Diagnostics.IdNonPartialContainingTypeForGeneratedSubscription, "Containing class must be declared as Partial", "Method is declared in type \"{0}\" which is not Partial", "Usage", DiagnosticSeverity.Error, true ); public override ImmutableArray SupportedDiagnostics { get; } = [BadMethodSignature, NotEntitySystem, NotPartial]; public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis( GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics ); EnsureAnnotatedSubscriptionMethodsAreInAPartialEntitySystem(context); EnsureAnnotatedSubscriptionMethodsHaveCorrectSignatures(context); } private static void EnsureAnnotatedSubscriptionMethodsAreInAPartialEntitySystem(AnalysisContext context) { List attributeNames = [ KnownTypes.AllSubscriptionMemberAttributeName, KnownTypes.NetworkSubscriptionMemberAttributeName, KnownTypes.LocalSubscriptionMemberAttributeName, ]; context.RegisterCompilationStartAction(c => { if (c.Compilation.GetTypeByMetadataName(KnownTypes.EntitySystemTypeName) is not { } entitySystemType) return; var attributeSymbols = attributeNames .Select(attributeName => c.Compilation.GetTypeByMetadataName(attributeName)) .OfType() .ToList(); c.RegisterSymbolStartAction( c => { if (!c.Symbol.GetAttributes() .Select(it => it.AttributeClass) .Intersect(attributeSymbols, SymbolEqualityComparer.IncludeNullability) .Any()) return; var references = c.Symbol.ContainingType.DeclaringSyntaxReferences; if (references.Length != 0) { var containingTypeDeclaration = (TypeDeclarationSyntax)references[0].GetSyntax(); if (!containingTypeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) { c.RegisterSymbolEndAction(c => c.ReportDiagnostic(Diagnostic.Create( NotPartial, c.Symbol.Locations[0], c.Symbol.ContainingType?.Name ?? "" ))); } } if (IsSubtypeOf(c.Symbol.ContainingType, entitySystemType)) return; c.RegisterSymbolEndAction(c => c.ReportDiagnostic(Diagnostic.Create( NotEntitySystem, c.Symbol.Locations[0], c.Symbol.ContainingType?.Name ?? "" ))); }, SymbolKind.Method ); }); } private static bool IsSubtypeOf(ITypeSymbol subtype, INamedTypeSymbol supertype) { return SymbolEqualityComparer.Default.Equals(subtype.BaseType, supertype) || (subtype.BaseType is not null && IsSubtypeOf(subtype.BaseType, supertype)); } private static void EnsureAnnotatedSubscriptionMethodsHaveCorrectSignatures(AnalysisContext context) { EnsureAnnotatedSubscriptionMethodHasCorrectSignature( context, KnownTypes.AllSubscriptionMemberAttributeName, m => (EntitySystemSubscriptionGenerator.TryParseEntityEventHandler(m) ?? EntitySystemSubscriptionGenerator.TryParseEntitySessionEventHandler(m)) is not null, KnownTypes.NonComponentSubscriptionHandlerTypes ); EnsureAnnotatedSubscriptionMethodHasCorrectSignature( context, KnownTypes.NetworkSubscriptionMemberAttributeName, m => (EntitySystemSubscriptionGenerator.TryParseEntityEventHandler(m) ?? EntitySystemSubscriptionGenerator.TryParseEntitySessionEventHandler(m)) is not null, KnownTypes.NonComponentSubscriptionHandlerTypes ); EnsureAnnotatedSubscriptionMethodHasCorrectSignature( context, KnownTypes.LocalSubscriptionMemberAttributeName, m => ( EntitySystemSubscriptionGenerator.TryParseEntityEventHandler(m) ?? EntitySystemSubscriptionGenerator.TryParseEntitySessionEventHandler(m) ?? EntitySystemSubscriptionGenerator.TryParseComponentEventHandler(m) ?? EntitySystemSubscriptionGenerator.TryParseEntityEventRefHandler(m) ) is not null, string.Join(", ", KnownTypes.NonComponentSubscriptionHandlerTypes, KnownTypes.ComponentSubscriptionHandlerTypes) ); } /// Checks that any methods annotated with have the correct signature as /// determined by . If not, a /// diagnostic is emitted, describing how the signature should instead conform to /// . private static void EnsureAnnotatedSubscriptionMethodHasCorrectSignature( AnalysisContext context, string annotationName, Func hasCorrectParameters, string acceptableHandlerTypes ) { context.RegisterCompilationStartAction(c => { if (c.Compilation.GetTypeByMetadataName(annotationName) is not { } annotationSymbol) return; c.RegisterSymbolStartAction( c => { // The `symbolKind` arg to `RegisterSymbolStartAction` should make this never fail. if (c.Symbol is not IMethodSymbol symbol) throw new Exception($"Expected {nameof(IMethodSymbol)} but got {c.Symbol.GetType().FullName}"); if (!symbol.GetAttributes() .Select(it => it.AttributeClass) .Contains(annotationSymbol, SymbolEqualityComparer.IncludeNullability) || hasCorrectParameters(symbol)) return; c.RegisterSymbolEndAction(c => c.ReportDiagnostic(Diagnostic.Create( BadMethodSignature, symbol.Locations[0], annotationName, acceptableHandlerTypes ))); }, SymbolKind.Method ); }); } }