#nullable enable using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Robust.Roslyn.Shared; using Robust.Shared.Prototypes; namespace Robust.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class PrototypeAnalyzer : DiagnosticAnalyzer { public static readonly DiagnosticDescriptor PrototypeRedundantTypeRule = new( Diagnostics.IdPrototypeRedundantType, "Redundant Prototype Type specification", "Prototype {0} has explicitly set type \"{1}\" that matches autogenerated value", "Usage", DiagnosticSeverity.Warning, true, "Remove the redundant type specification." ); public static readonly DiagnosticDescriptor PrototypeEndsWithPrototypeRule = new( Diagnostics.IdPrototypeEndsWithPrototype, "Prototype name must end with the word Prototype", "Prototype {0} does not end with the word Prototype", "Usage", DiagnosticSeverity.Error, true, "Add the word Prototype to the end of the class name or manually specify a name in the Prototype attribute." ); public override ImmutableArray SupportedDiagnostics => [PrototypeRedundantTypeRule, PrototypeEndsWithPrototypeRule]; public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static ctx => { var prototypeAttribute = ctx.Compilation.GetTypeByMetadataName("Robust.Shared.Prototypes.PrototypeAttribute"); // No attribute, no analyzer. if (prototypeAttribute is null) return; ctx.RegisterSyntaxNodeAction( symCtx => AnalyzePrototype(symCtx, prototypeAttribute), SyntaxKind.ClassDeclaration); }); } private static void AnalyzePrototype(SyntaxNodeAnalysisContext context, INamedTypeSymbol prototypeAttributeSymbol) { if (context.Node is not ClassDeclarationSyntax classDeclarationSyntax) return; var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax); if (classSymbol is null) return; var className = classSymbol.Name; if (!AttributeHelper.HasAttribute(classSymbol, prototypeAttributeSymbol, out var attributeData)) return; var prototypeAttribute = GetAttributeSyntax(attributeData, classDeclarationSyntax); if (prototypeAttribute == null) return; // Check for autogenerated type if (prototypeAttribute.ArgumentList?.Arguments[0] is not { } argumentSyntax) { if (!className.EndsWith(PrototypeUtility.PrototypeNameEnding)) { context.ReportDiagnostic(Diagnostic.Create(PrototypeEndsWithPrototypeRule, classDeclarationSyntax.Identifier.GetLocation(), className)); } return; } // We only care about redundancy if the argument is a string literal. // Passing in a value that resolves to a redundant string is fine. if (argumentSyntax.Expression is not LiteralExpressionSyntax literalSyntax) return; var literalValue = context.SemanticModel.GetConstantValue(literalSyntax); if (literalValue.Value is not string specifiedName) return; var autoName = PrototypeUtility.CalculatePrototypeName(className); // Check for name redundancy if (autoName == specifiedName) { // If the class name does not end with "Prototype", allow the redundancy if (!className.EndsWith(PrototypeUtility.PrototypeNameEnding)) return; var location = argumentSyntax.GetLocation(); context.ReportDiagnostic(Diagnostic.Create(PrototypeRedundantTypeRule, location, className, specifiedName)); } } private static AttributeSyntax? GetAttributeSyntax( AttributeData attributeData, ClassDeclarationSyntax classSyntax) { if (attributeData.ApplicationSyntaxReference is not { } syntaxReference) return null; foreach (var attributeList in classSyntax.AttributeLists) { foreach (var attribute in attributeList.Attributes) { if (syntaxReference.SyntaxTree != attribute.SyntaxTree) continue; if (!syntaxReference.Span.OverlapsWith(attribute.Span)) continue; return attribute; } } return null; } }