diff --git a/SourceGenerators/AttributeUsageAnalyzer.cs b/SourceGenerators/AttributeUsageAnalyzer.cs index 3b7284aa..ffcecb43 100644 --- a/SourceGenerators/AttributeUsageAnalyzer.cs +++ b/SourceGenerators/AttributeUsageAnalyzer.cs @@ -1,4 +1,5 @@ -using System.Collections.Immutable; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -31,10 +32,20 @@ public class AttributeUsageAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, description: "Description must be less than or equal to 100 characters." ); + private static readonly DiagnosticDescriptor CommandWithEmojiVariationSelector = 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." + ); public override ImmutableArray SupportedDiagnostics { get; } = [ AccessCheckAttributeOnGroupCommandRule, DescriptionLengthRule, + CommandWithEmojiVariationSelector, ]; public override void Initialize(AnalysisContext context) @@ -46,6 +57,7 @@ public class AttributeUsageAnalyzer : DiagnosticAnalyzer // 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) @@ -135,6 +147,70 @@ public class AttributeUsageAnalyzer : DiagnosticAnalyzer // 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; + + var vs = actualName.ToCharArray().FirstOrDefault(VariationSelectors.Contains); + if (vs is default(char)) + return; + + var diagnostic = Diagnostic.Create(CommandWithEmojiVariationSelector, + // 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}" + ); + + // Reporting a diagnostic is the primary outcome of analyzers. + context.ReportDiagnostic(diagnostic); + } private static bool IsDescendantOfAttribute(AttributeData attributeData, string baseAttributeClassNameWithNamespace) { @@ -148,4 +224,6 @@ public class AttributeUsageAnalyzer : DiagnosticAnalyzer } return false; } + + private static readonly HashSet VariationSelectors = [.. Enumerable.Range(0xFE00, 16).Select(i => (char)i)]; } \ No newline at end of file