mirror of
https://github.com/RPCS3/discord-bot.git
synced 2026-01-31 01:25:22 +01:00
251 lines
10 KiB
C#
251 lines
10 KiB
C#
using System.Collections.Generic;
|
|
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;
|
|
|
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
|
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 static readonly DiagnosticDescriptor AccessCheckAttributeOnGroupCommandRule = new(
|
|
"DSP0001",
|
|
"Access check attributes are ignored",
|
|
"Attribute {0} will be ignored for GroupCommand",
|
|
"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."
|
|
);
|
|
private static readonly DiagnosticDescriptor CommandWithEmojiVariationSelectorRule = new(
|
|
"DSP0003",
|
|
"Emoji with variation selector",
|
|
"Command name has an emoji character with VS{0} ({1}), which may not work as a command name",
|
|
"Usage",
|
|
DiagnosticSeverity.Warning,
|
|
isEnabledByDefault: true,
|
|
description: "Commands should avoid using variation selectors for emoji characters in command names."
|
|
);
|
|
private static readonly DiagnosticDescriptor CommandNameLengthRule = new(
|
|
"DSP0004",
|
|
"Command name length is too long",
|
|
"Command name is {0} characters long, which is {1} characters longer than allowed",
|
|
"Usage",
|
|
DiagnosticSeverity.Error,
|
|
isEnabledByDefault: true,
|
|
description: "Command name must be between 1 and 32 characters long."
|
|
);
|
|
|
|
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [
|
|
AccessCheckAttributeOnGroupCommandRule,
|
|
DescriptionLengthRule,
|
|
CommandWithEmojiVariationSelectorRule,
|
|
CommandNameLengthRule,
|
|
];
|
|
|
|
public override void Initialize(AnalysisContext context)
|
|
{
|
|
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
|
context.EnableConcurrentExecution();
|
|
|
|
// 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(AnalyzeMethod, SymbolKind.Method);
|
|
context.RegisterOperationAction(AnalyzeDescriptionAttribute, OperationKind.Attribute);
|
|
context.RegisterOperationAction(AnalyzeCommandAttribute, OperationKind.Attribute);
|
|
}
|
|
|
|
private static void AnalyzeMethod(SymbolAnalysisContext context)
|
|
{
|
|
var methodSymbol = (IMethodSymbol)context.Symbol;
|
|
var methodAttributes = methodSymbol.GetAttributes();
|
|
if (methodAttributes.IsEmpty)
|
|
return;
|
|
|
|
var hasGroupCommand = false;
|
|
foreach (var attr in methodAttributes)
|
|
if (IsDescendantOfAttribute(attr, "DSharpPlus.Commands.Attributes.GroupCommandAttribute"))
|
|
{
|
|
hasGroupCommand = true;
|
|
break;
|
|
}
|
|
|
|
if (!hasGroupCommand)
|
|
return;
|
|
|
|
foreach (var attr in methodAttributes)
|
|
if (IsDescendantOfAttribute(attr, "DSharpPlus.Commands.Attributes.CheckBaseAttribute"))
|
|
{
|
|
var attrLocation = attr.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attr.ApplicationSyntaxReference.Span);
|
|
var diagnostic = Diagnostic.Create(AccessCheckAttributeOnGroupCommandRule, attrLocation ?? methodSymbol.Locations[0], attr.AttributeClass?.Name ?? methodSymbol.Name);
|
|
context.ReportDiagnostic(diagnostic);
|
|
}
|
|
}
|
|
|
|
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 void AnalyzeCommandAttribute(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: "DSharpPlus",
|
|
Name: "Commands"
|
|
},
|
|
Name: "CommandAttribute"
|
|
}
|
|
} attrCtorOp)
|
|
return;
|
|
|
|
if (attrCtorOp.Arguments.Length is 0
|
|
|| attrCtorOp.Arguments.FirstOrDefault(arg => arg is
|
|
{
|
|
Parameter:
|
|
{
|
|
Name: "name",
|
|
Type: {ContainingNamespace.Name: "System", Name: "String"}
|
|
}
|
|
}) is not
|
|
{
|
|
Value: ILiteralOperation
|
|
{
|
|
ConstantValue:
|
|
{
|
|
HasValue: true,
|
|
Value: string actualName
|
|
}
|
|
}
|
|
})
|
|
return;
|
|
|
|
if (actualName is not {Length: >0})
|
|
return;
|
|
|
|
if (actualName.Length > 32)
|
|
{
|
|
context.ReportDiagnostic(
|
|
Diagnostic.Create(CommandNameLengthRule,
|
|
// 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.
|
|
actualName.Length, actualName.Length - 32
|
|
)
|
|
);
|
|
}
|
|
|
|
var vs = actualName.ToCharArray().FirstOrDefault(VariationSelectors.Contains);
|
|
if (vs is default(char))
|
|
return;
|
|
|
|
// Reporting a diagnostic is the primary outcome of analyzers.
|
|
context.ReportDiagnostic(
|
|
Diagnostic.Create(CommandWithEmojiVariationSelectorRule,
|
|
// 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.
|
|
vs - 0xFE00 + 1, $"0x{(int)vs:X4}"
|
|
)
|
|
);
|
|
}
|
|
|
|
private static bool IsDescendantOfAttribute(AttributeData attributeData, string baseAttributeClassNameWithNamespace)
|
|
{
|
|
var attrClass = attributeData.AttributeClass;
|
|
while (attrClass is not null)
|
|
{
|
|
if (attrClass.ToDisplayString() == baseAttributeClassNameWithNamespace)
|
|
return true;
|
|
|
|
attrClass = attrClass.BaseType;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static readonly HashSet<char> VariationSelectors = [.. Enumerable.Range(0xFE00, 16).Select(i => (char)i)];
|
|
} |