#nullable enable using System; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace NadekoBot.Generators.Command; [Generator] public class CommandAttributesGenerator : IIncrementalGenerator { public const string ATTRIBUTE = @"// namespace NadekoBot.Common; [System.AttributeUsage(System.AttributeTargets.Method)] public class CmdAttribute : System.Attribute { }"; public class MethodModel { public string? Namespace { get; } public IReadOnlyCollection Classes { get; } public string ReturnType { get; } public string MethodName { get; } public IEnumerable Params { get; } public MethodModel(string? ns, IReadOnlyCollection classes, string returnType, string methodName, IEnumerable @params) { Namespace = ns; Classes = classes; ReturnType = returnType; MethodName = methodName; Params = @params; } } public class FileModel { public string? Namespace { get; } public IReadOnlyCollection ClassHierarchy { get; } public IReadOnlyCollection Methods { get; } public FileModel(string? ns, IReadOnlyCollection classHierarchy, IReadOnlyCollection methods) { Namespace = ns; ClassHierarchy = classHierarchy; Methods = methods; } } public void Initialize(IncrementalGeneratorInitializationContext context) { // #if DEBUG // SpinWait.SpinUntil(() => Debugger.IsAttached); // #endif context.RegisterPostInitializationOutput(static ctx => ctx.AddSource( "CmdAttribute.g.cs", SourceText.From(ATTRIBUTE, Encoding.UTF8))); var methods = context.SyntaxProvider .CreateSyntaxProvider( static (node, _) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 }, static (ctx, cancel) => Transform(ctx, cancel)) .Where(static m => m is not null) .Where(static m => m?.ChildTokens().Any(static x => x.IsKind(SyntaxKind.PublicKeyword)) ?? false); var compilationMethods = context.CompilationProvider.Combine(methods.Collect()); context.RegisterSourceOutput(compilationMethods, static (ctx, tuple) => RegisterAction(in ctx, tuple.Left, in tuple.Right)); } private static void RegisterAction(in SourceProductionContext ctx, Compilation comp, in ImmutableArray methods) { if (methods is { IsDefaultOrEmpty: true }) return; var models = GetModels(comp, methods, ctx.CancellationToken); foreach (var model in models) { var name = $"{model.Namespace}.{string.Join(".", model.ClassHierarchy)}.g.cs"; try { var source = GetSourceText(model); ctx.AddSource(name, SourceText.From(source, Encoding.UTF8)); } catch (Exception ex) { Console.WriteLine($"Error writing source file {name}\n" + ex); } } } private static string GetSourceText(FileModel model) { using var sw = new StringWriter(); using var tw = new IndentedTextWriter(sw); tw.WriteLine("// "); tw.WriteLine("#pragma warning disable CS1066"); if (model.Namespace is not null) { tw.WriteLine($"namespace {model.Namespace};"); tw.WriteLine(); } foreach (var className in model.ClassHierarchy) { tw.WriteLine($"public partial class {className}"); tw.WriteLine("{"); tw.Indent ++; } foreach (var method in model.Methods) { tw.WriteLine("[NadekoCommand]"); tw.WriteLine("[NadekoDescription]"); tw.WriteLine("[Aliases]"); tw.WriteLine($"public partial {method.ReturnType} {method.MethodName}({string.Join(", ", method.Params)});"); } foreach (var _ in model.ClassHierarchy) { tw.Indent --; tw.WriteLine("}"); } tw.Flush(); return sw.ToString(); } private static IReadOnlyCollection GetModels(Compilation compilation, in ImmutableArray inputMethods, CancellationToken cancel) { var models = new List(); var methods = inputMethods .Where(static x => x is not null) .Distinct(); var methodModels = methods .Select(x => MethodDeclarationToMethodModel(compilation, x!)); var groups = methodModels .GroupBy(static x => $"{x.Namespace}.{string.Join(".", x.Classes)}"); foreach (var group in groups) { if (cancel.IsCancellationRequested) return new Collection(); if (group is null) continue; var elems = group.ToList(); if (elems.Count is 0) continue; var model = new FileModel( methods: elems, ns: elems[0].Namespace, classHierarchy: elems[0].Classes ); models.Add(model); } return models; } private static MethodModel MethodDeclarationToMethodModel(Compilation comp, MethodDeclarationSyntax decl) { // SpinWait.SpinUntil(static () => Debugger.IsAttached); var semanticModel = comp.GetSemanticModel(decl.SyntaxTree); var methodModel = new MethodModel( @params: decl.ParameterList.Parameters .Where(p => p.Type is not null) .Select(p => { var prefix = p.Modifiers.Any(static x => x.IsKind(SyntaxKind.ParamsKeyword)) ? "params " : string.Empty; var type = semanticModel .GetTypeInfo(p.Type!) .Type ?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var name = p.Identifier.Text; var suffix = string.Empty; if (p.Default is not null) { if (p.Default.Value is LiteralExpressionSyntax) { suffix = " = " + p.Default.Value; } else if (p.Default.Value is MemberAccessExpressionSyntax maes) { var maesSemModel = comp.GetSemanticModel(maes.SyntaxTree); var sym = maesSemModel.GetSymbolInfo(maes.Name); if (sym.Symbol is null) { suffix = " = " + p.Default.Value; } else { suffix = " = " + sym.Symbol.ToDisplayString(); } } } return $"{prefix}{type} {name}{suffix}"; }) .ToList(), methodName: decl.Identifier.Text, returnType: decl.ReturnType.ToString(), ns: GetNamespace(decl), classes: GetClasses(decl) ); return methodModel; } //https://github.com/andrewlock/NetEscapades.EnumGenerators/blob/main/src/NetEscapades.EnumGenerators/EnumGenerator.cs static string? GetNamespace(MethodDeclarationSyntax declarationSyntax) { // determine the namespace the class is declared in, if any string? nameSpace = null; var parentOfInterest = declarationSyntax.Parent; while (parentOfInterest is not null) { parentOfInterest = parentOfInterest.Parent; if (parentOfInterest is BaseNamespaceDeclarationSyntax ns) { nameSpace = ns.Name.ToString(); while (true) { if (ns.Parent is not NamespaceDeclarationSyntax parent) { break; } ns = parent; nameSpace = $"{ns.Name}.{nameSpace}"; } return nameSpace; } } return nameSpace; } static IReadOnlyCollection GetClasses(MethodDeclarationSyntax declarationSyntax) { // determine the namespace the class is declared in, if any var classes = new LinkedList(); var parentOfInterest = declarationSyntax.Parent; while (parentOfInterest is not null) { if (parentOfInterest is ClassDeclarationSyntax cds) { classes.AddFirst(cds.Identifier.ToString()); } parentOfInterest = parentOfInterest.Parent; } Debug.WriteLine($"Method {declarationSyntax.Identifier.Text} has {classes.Count} classes"); return classes; } private static MethodDeclarationSyntax? Transform(GeneratorSyntaxContext ctx, CancellationToken cancel) { var methodDecl = ctx.Node as MethodDeclarationSyntax; if (methodDecl is null) return default; foreach (var attListSyntax in methodDecl.AttributeLists) { foreach (var attSyntax in attListSyntax.Attributes) { if (cancel.IsCancellationRequested) return default; var symbol = ctx.SemanticModel.GetSymbolInfo(attSyntax).Symbol; if (symbol is not IMethodSymbol attSymbol) continue; if (attSymbol.ContainingType.ToDisplayString() == "NadekoBot.Common.CmdAttribute") return methodDecl; } } return default; } }