using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Robust.Roslyn.Shared; namespace Robust.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class ExplicitVirtualAnalyzer : DiagnosticAnalyzer { internal const string Attribute = "Robust.Shared.Analyzers.VirtualAttribute"; [SuppressMessage("ReSharper", "RS2008")] public static readonly DiagnosticDescriptor ExplicitVirtualRule = new( Diagnostics.IdExplicitVirtual, "Class must be explicitly marked as [Virtual], abstract, static, or sealed", "Class must be explicitly marked as [Virtual], abstract, static, or sealed", "Usage", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Class must be explicitly marked as [Virtual], abstract, static, or sealed."); public static readonly DiagnosticDescriptor ExclusiveRule = new( Diagnostics.IdExclusiveVirtual, "A class marked as [Virtual] cannot be abstract, static, or sealed", "A class marked as [Virtual] cannot be abstract, static, or sealed", "Usage", DiagnosticSeverity.Error, isEnabledByDefault: true, description: "A class marked as [Virtual] cannot be abstract, static, or sealed."); public override ImmutableArray SupportedDiagnostics => [ ExplicitVirtualRule, ExclusiveRule, ]; public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(ctx => { if (ctx.Compilation.GetTypeByMetadataName(Attribute) is not INamedTypeSymbol attrSymbol) return; ctx.RegisterSyntaxNodeAction(nodeContext => AnalyzeNode(nodeContext, attrSymbol), SyntaxKind.ClassDeclaration); }); } private static void AnalyzeNode(SyntaxNodeAnalysisContext context, INamedTypeSymbol attrSymbol) { var classDecl = (ClassDeclarationSyntax)context.Node; var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDecl); if (classSymbol == null) return; var hasKeyword = classSymbol.IsSealed || classSymbol.IsAbstract || classSymbol.IsStatic; var hasAttribute = AttributeHelper.HasAttribute(classSymbol, attrSymbol, out _); if (hasAttribute && hasKeyword) { // Having both [Virtual] and sealed/abstract/static doesn't make sense. context.ReportDiagnostic(Diagnostic.Create( ExclusiveRule, classDecl.Keyword.GetLocation() )); } // Having just [Virtual] or sealed/abstract/static is fine. if (hasKeyword || hasAttribute) return; // Having neither is bad. var diag = Diagnostic.Create(ExplicitVirtualRule, classDecl.Keyword.GetLocation()); context.ReportDiagnostic(diag); } } // Doesn't work as I'd hoped: Roslyn doesn't provide an API for global usings and I can't get batch changes to work. /* [ExportCodeFixProvider(LanguageNames.CSharp)] public sealed class ExplicitVirtualCodeFixProvider : CodeFixProvider { private const string TitleSealed = "Annotate class as sealed."; private const string TitleVirtual = "Annotate class as [Virtual]."; private const string TitleAbstract = "Annotate class as abstract."; private const string TitleStatic = "Annotate class as static."; public override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken); foreach (var diagnostic in context.Diagnostics) { var span = diagnostic.Location.SourceSpan; var classDecl = root.FindToken(span.Start).Parent.AncestorsAndSelf().OfType() .First(); context.RegisterCodeFix( CodeAction.Create( TitleVirtual, c => FixVirtualAsync(context.Document, classDecl, c), TitleVirtual), diagnostic); context.RegisterCodeFix( CodeAction.Create( TitleStatic, c => FixStaticAsync(context.Document, classDecl, c), TitleStatic), diagnostic); context.RegisterCodeFix( CodeAction.Create( TitleSealed, c => FixSealedAsync(context.Document, classDecl, c), TitleSealed), diagnostic); context.RegisterCodeFix( CodeAction.Create( TitleAbstract, c => FixAbstractAsync(context.Document, classDecl, c), TitleAbstract), diagnostic); } } private async Task FixVirtualAsync( Document document, ClassDeclarationSyntax classDecl, CancellationToken cancellationToken) { var ns = "Robust.Shared.Analyzers"; var attrib = SyntaxFactory.Attribute(SyntaxFactory.ParseName("Virtual")); var newClassDecl = classDecl.AddAttributeLists( SyntaxFactory.AttributeList(SyntaxFactory.SeparatedList(new[] { attrib }))); var root = (CompilationUnitSyntax)await document.GetSyntaxRootAsync(cancellationToken); root = root.ReplaceNode(classDecl, newClassDecl); var options = await document.GetOptionsAsync(cancellationToken); if (root.Usings.All(u => u.Name.ToString() != ns)) { root = root.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(ns))); } return document.WithSyntaxRoot(root); } private async Task FixStaticAsync( Document document, ClassDeclarationSyntax classDecl, CancellationToken cancellationToken) { var newClassDecl = classDecl.AddModifiers(SyntaxFactory.Token(SyntaxKind.StaticKeyword)); var root = (CompilationUnitSyntax)await document.GetSyntaxRootAsync(cancellationToken); root = root.ReplaceNode(classDecl, newClassDecl); return document.WithSyntaxRoot(root); } private async Task FixAbstractAsync( Document document, ClassDeclarationSyntax classDecl, CancellationToken cancellationToken) { var newClassDecl = classDecl.AddModifiers(SyntaxFactory.Token(SyntaxKind.AbstractKeyword)); var root = (CompilationUnitSyntax)await document.GetSyntaxRootAsync(cancellationToken); root = root.ReplaceNode(classDecl, newClassDecl); return document.WithSyntaxRoot(root); } private async Task FixSealedAsync( Document document, ClassDeclarationSyntax classDecl, CancellationToken cancellationToken) { var newClassDecl = classDecl.AddModifiers(SyntaxFactory.Token(SyntaxKind.SealedKeyword)); var root = (CompilationUnitSyntax)await document.GetSyntaxRootAsync(cancellationToken); root = root.ReplaceNode(classDecl, newClassDecl); return document.WithSyntaxRoot(root); } public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(Diagnostics.IdExplicitVirtual); } */