From 45869c4f7a0fea391573fca2648b2c910a31b0b4 Mon Sep 17 00:00:00 2001 From: 13xforever Date: Wed, 12 Mar 2025 14:30:27 +0500 Subject: [PATCH] add roslyn analyzer for command description attributes --- .gitignore | 1 - CompatBot/Commands/BotMath.cs | 1 + CompatBot/CompatBot.csproj | 1 + SourceGenerators/AttributeUsageAnalyzer.cs | 92 +++++++++++++++++-- .../Properties/launchSettings.json | 8 ++ SourceGenerators/SourceGenerators.csproj | 3 +- 6 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 SourceGenerators/Properties/launchSettings.json diff --git a/.gitignore b/.gitignore index 50943b39..909f431f 100644 --- a/.gitignore +++ b/.gitignore @@ -259,7 +259,6 @@ paket-files/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc -launchSettings.json .vscode/ *.db *.db-journal diff --git a/CompatBot/Commands/BotMath.cs b/CompatBot/Commands/BotMath.cs index 87dda5cd..fb25dcd4 100644 --- a/CompatBot/Commands/BotMath.cs +++ b/CompatBot/Commands/BotMath.cs @@ -14,6 +14,7 @@ internal sealed class BotMath } [Command("calculate"), TextAlias("calc"), DefaultGroupCommand] + [Description("12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901")] public async ValueTask Calc(CommandContext ctx, [RemainingText, Description("Math expression")] string expression) { if (string.IsNullOrEmpty(expression)) diff --git a/CompatBot/CompatBot.csproj b/CompatBot/CompatBot.csproj index de7a96b1..a1196e54 100644 --- a/CompatBot/CompatBot.csproj +++ b/CompatBot/CompatBot.csproj @@ -11,6 +11,7 @@ true enable false + True diff --git a/SourceGenerators/AttributeUsageAnalyzer.cs b/SourceGenerators/AttributeUsageAnalyzer.cs index 4f634f04..3b7284aa 100644 --- a/SourceGenerators/AttributeUsageAnalyzer.cs +++ b/SourceGenerators/AttributeUsageAnalyzer.cs @@ -1,6 +1,9 @@ using System.Collections.Immutable; +using System.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; namespace SourceGenerators; @@ -9,21 +12,30 @@ public class AttributeUsageAnalyzer : DiagnosticAnalyzer { // You can change these strings in the Resources.resx file. If you do not want your analyzer to be localize-able, you can use regular strings for Title and MessageFormat. // See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/Localizing%20Analyzers.md for more on localization - private const string Category = "Usage"; - private const string DiagnosticId = "DSharpPlusAttributeUsage"; - private static readonly DiagnosticDescriptor AccessCheckAttributeOnGroupCommandRule = new DiagnosticDescriptor( - DiagnosticId, + private static readonly DiagnosticDescriptor AccessCheckAttributeOnGroupCommandRule = new( + "DSP0001", "Access check attributes are ignored", "Attribute {0} will be ignored for GroupCommand", - Category, + "Usage", DiagnosticSeverity.Error, isEnabledByDefault: true, description: "GroupCommand methods will silently ignore any access check attributes, so instead create an instance of the required check attribute and call it explicitly inside the method." ); + private static readonly DiagnosticDescriptor DescriptionLengthRule = new( + "DSP0002", + "Description is too long", + "Description is {0} characters long, which is {1} characters longer than allowed", + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Description must be less than or equal to 100 characters." + ); - public override ImmutableArray SupportedDiagnostics { get; } = - [AccessCheckAttributeOnGroupCommandRule]; + public override ImmutableArray SupportedDiagnostics { get; } = [ + AccessCheckAttributeOnGroupCommandRule, + DescriptionLengthRule, + ]; public override void Initialize(AnalysisContext context) { @@ -32,10 +44,11 @@ public class AttributeUsageAnalyzer : DiagnosticAnalyzer // TODO: Consider registering other actions that act on syntax instead of or in addition to symbols // See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/Analyzer%20Actions%20Semantics.md for more information - context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method); + context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); + context.RegisterOperationAction(AnalyzeDescriptionAttribute, OperationKind.Attribute); } - private static void AnalyzeSymbol(SymbolAnalysisContext context) + private static void AnalyzeMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; var methodAttributes = methodSymbol.GetAttributes(); @@ -62,6 +75,67 @@ public class AttributeUsageAnalyzer : DiagnosticAnalyzer } } + private void AnalyzeDescriptionAttribute(OperationAnalysisContext context) + { + // The Roslyn architecture is based on inheritance. + // To get the required metadata, we should match the 'Operation' and 'Syntax' objects to the particular types, + // which are based on the 'OperationKind' parameter specified in the 'Register...' method. + if (context.Operation is not IAttributeOperation attributeOperation + || context.Operation.Syntax is not AttributeSyntax attributeSyntax) + return; + + if (attributeOperation.Kind != OperationKind.Attribute + || attributeOperation.Operation is not IObjectCreationOperation + { + Kind: OperationKind.ObjectCreation, + Type: + { + ContainingNamespace: + { + ContainingNamespace.Name: "System", + Name: "ComponentModel" + }, + Name: "DescriptionAttribute" + } + } attrCtorOp) + return; + + if (attrCtorOp.Arguments.Length is 0 + || attrCtorOp.Arguments.FirstOrDefault(arg => arg is + { + Parameter: + { + Name: "description", + Type: {ContainingNamespace.Name: "System", Name: "String"} + } + }) is not + { + Value: ILiteralOperation + { + ConstantValue: + { + HasValue: true, + Value: string actualDescription + } + } + }) + return; + + const int maxDescriptionLength = 100; + if (actualDescription.Length <= maxDescriptionLength) + return; + + var diagnostic = Diagnostic.Create(DescriptionLengthRule, + // The highlighted area in the analyzed source code. Keep it as specific as possible. + attributeSyntax.GetLocation(), + // The value is passed to the 'MessageFormat' argument of your rule. + actualDescription.Length, actualDescription.Length - maxDescriptionLength + ); + + // Reporting a diagnostic is the primary outcome of analyzers. + context.ReportDiagnostic(diagnostic); + } + private static bool IsDescendantOfAttribute(AttributeData attributeData, string baseAttributeClassNameWithNamespace) { var attrClass = attributeData.AttributeClass; diff --git a/SourceGenerators/Properties/launchSettings.json b/SourceGenerators/Properties/launchSettings.json new file mode 100644 index 00000000..1fa18079 --- /dev/null +++ b/SourceGenerators/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "RoslynAnalyzers": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\CompatBot\\CompatBot.csproj" + } + } +} \ No newline at end of file diff --git a/SourceGenerators/SourceGenerators.csproj b/SourceGenerators/SourceGenerators.csproj index 555bf5c2..39178e43 100644 --- a/SourceGenerators/SourceGenerators.csproj +++ b/SourceGenerators/SourceGenerators.csproj @@ -5,11 +5,12 @@ latest 1701;1702;RS2008;RS1036 enable + true - +