add another roslyn analyzer for context menu commands that can use emoji

This commit is contained in:
13xforever
2025-03-20 12:13:54 +05:00
parent 57ef638a4c
commit 5a7d6896d3

View File

@@ -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<DiagnosticDescriptor> 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<char> VariationSelectors = [.. Enumerable.Range(0xFE00, 16).Select(i => (char)i)];
}