add roslyn analyzer for command description attributes

This commit is contained in:
13xforever
2025-03-12 14:30:27 +05:00
parent 54e4f3a721
commit 45869c4f7a
6 changed files with 95 additions and 11 deletions

1
.gitignore vendored
View File

@@ -259,7 +259,6 @@ paket-files/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
launchSettings.json
.vscode/
*.db
*.db-journal

View File

@@ -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))

View File

@@ -11,6 +11,7 @@
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<Nullable>enable</Nullable>
<EmitCompilerGeneratedFiles>false</EmitCompilerGeneratedFiles>
<RunCodeAnalysis>True</RunCodeAnalysis>
</PropertyGroup>
<ItemGroup>
<CompilerVisibleProperty Include="RootNamespace" />

View File

@@ -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<DiagnosticDescriptor> SupportedDiagnostics { get; } =
[AccessCheckAttributeOnGroupCommandRule];
public override ImmutableArray<DiagnosticDescriptor> 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;

View File

@@ -0,0 +1,8 @@
{
"profiles": {
"RoslynAnalyzers": {
"commandName": "DebugRoslynComponent",
"targetProject": "..\\CompatBot\\CompatBot.csproj"
}
}
}

View File

@@ -5,11 +5,12 @@
<LangVersion>latest</LangVersion>
<NoWarn>1701;1702;RS2008;RS1036</NoWarn>
<Nullable>enable</Nullable>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" />
</ItemGroup>
</Project>