mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 01:38:27 -04:00
- Removed NadekoCommand and Aliases attribute from all commands
- All commands must be marked as partial - Added [Cmd] Attribute to all commands - Cmd Attribute comes from the source generator which adds [NadekoCommand] and [Aliases] Attribute to each command - Should be updated in the future probably to be more performant and maybe add extra data to the commands - Started reorganizing modules and submodules
This commit is contained in:
343
src/NadekoBot.Generators/Command/CommandAttributesGenerator.cs
Normal file
343
src/NadekoBot.Generators/Command/CommandAttributesGenerator.cs
Normal file
@@ -0,0 +1,343 @@
|
||||
using System.CodeDom.Compiler;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
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 = @"// <AutoGenerated />
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
[System.AttributeUsage(System.AttributeTargets.Method)]
|
||||
public class CmdAttribute : System.Attribute
|
||||
{
|
||||
|
||||
}";
|
||||
|
||||
public class MethodModel
|
||||
{
|
||||
public string Namespace { get; set; }
|
||||
public IReadOnlyCollection<string> Classes { get; set; }
|
||||
public string ReturnType { get; set; }
|
||||
public string MethodName { get; set; }
|
||||
public IEnumerable<string> Params { get; set; }
|
||||
}
|
||||
|
||||
public class FileModel
|
||||
{
|
||||
public string Namespace { get; set; }
|
||||
public IReadOnlyCollection<string> ClassHierarchy { get; set; }
|
||||
public IReadOnlyCollection<MethodModel> Methods { get; set; }
|
||||
}
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
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 != default)
|
||||
.Where(static m => m.ChildTokens().Any(static x => x.IsKind(SyntaxKind.PublicKeyword)));
|
||||
|
||||
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<MethodDeclarationSyntax> methods)
|
||||
{
|
||||
if (methods.IsDefaultOrEmpty)
|
||||
return;
|
||||
|
||||
var models = GetModels(comp, methods, ctx.CancellationToken);
|
||||
|
||||
foreach (var model in models)
|
||||
{
|
||||
var source = GetSourceText(model);
|
||||
ctx.AddSource($"{model.Namespace}.{string.Join(".", model.ClassHierarchy)}.g.cs",
|
||||
SourceText.From(source, Encoding.UTF8));
|
||||
}
|
||||
}
|
||||
|
||||
// private static void GenerateFileFromModel(in SourceProductionContext ctx, IGrouping<string, DataModel> @group)
|
||||
// {
|
||||
// using var sw = new StringWriter();
|
||||
// using var tw = new IndentedTextWriter(sw);
|
||||
//
|
||||
// foreach (var model in group)
|
||||
// {
|
||||
// tw.WriteLine($"public partial {model.ReturnType} {model.MethodName}({string.Join(", ", model.Params)});");
|
||||
// }
|
||||
//
|
||||
// GenerateFileFromModel(ctx, sw.ToString(), group.Key, group.AsEnumerable());
|
||||
//
|
||||
//
|
||||
// }
|
||||
|
||||
// private static void GenerateFileFromModel(SourceProductionContext ctx, string innerContent, string groupName, IEnumerable<DataModel> modelsEnum)
|
||||
// {
|
||||
// using var sw = new StringWriter();
|
||||
// using var tw = new IndentedTextWriter(sw);
|
||||
//
|
||||
// var models = modelsEnum.ToList();
|
||||
// var referenceModel = models.First();
|
||||
// tw.WriteLine($"namespace {referenceModel.Namespace};");
|
||||
//
|
||||
// foreach (var className in referenceModel.ClassNames.Reverse().ToList())
|
||||
// {
|
||||
// tw.WriteLine($"public partial class {className}");
|
||||
// tw.WriteLine("{");
|
||||
// tw.WriteLine();
|
||||
// tw.Indent ++;
|
||||
// }
|
||||
//
|
||||
// foreach (var model in models)
|
||||
// {
|
||||
// tw.WriteLine($"public partial {model.ReturnType} {model.MethodName}({string.Join(", ", model.Params)});");
|
||||
// }
|
||||
//
|
||||
// foreach (var _ in referenceModel.ClassNames)
|
||||
// {
|
||||
// tw.Indent --;
|
||||
// tw.WriteLine("}");
|
||||
// }
|
||||
//
|
||||
// // tw.Indent--;
|
||||
// // tw.WriteLine("}");
|
||||
// tw.Flush();
|
||||
//
|
||||
// ctx.AddSource($"{groupName}.qwertyus.g.cs",
|
||||
// SourceText.From(sw.ToString(), Encoding.UTF8));
|
||||
// }
|
||||
|
||||
private static string GetSourceText(FileModel model)
|
||||
{
|
||||
using var sw = new StringWriter();
|
||||
using var tw = new IndentedTextWriter(sw);
|
||||
|
||||
tw.WriteLine("// <AutoGenerated />");
|
||||
tw.WriteLine("#pragma warning disable CS1066");
|
||||
tw.WriteLine($"namespace {model.Namespace};");
|
||||
|
||||
foreach (var className in model.ClassHierarchy)
|
||||
{
|
||||
tw.WriteLine($"public partial class {className}");
|
||||
tw.Write("{");
|
||||
tw.Indent ++;
|
||||
}
|
||||
|
||||
foreach (var method in model.Methods)
|
||||
{
|
||||
tw.WriteLine();
|
||||
tw.WriteLine("[NadekoCommand]");
|
||||
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<FileModel> GetModels(Compilation compilation,
|
||||
in ImmutableArray<MethodDeclarationSyntax> methods,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
var models = new List<FileModel>();
|
||||
|
||||
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)
|
||||
{
|
||||
var elems = group.ToList();
|
||||
var model = new FileModel()
|
||||
{
|
||||
Methods = elems,
|
||||
Namespace = 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 methodModel = new MethodModel();
|
||||
var semanticModel = comp.GetSemanticModel(decl.SyntaxTree);
|
||||
methodModel.Params = decl.ParameterList.Parameters
|
||||
.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();
|
||||
|
||||
methodModel.MethodName = decl.Identifier.Text;
|
||||
methodModel.ReturnType = decl.ReturnType.ToString();
|
||||
methodModel.Namespace = GetNamespace(decl);
|
||||
methodModel.Classes = GetClasses(decl);
|
||||
|
||||
return methodModel;
|
||||
}
|
||||
|
||||
// private static IEnumerable<string> GetParams(Compilation compilation, MethodDeclarationSyntax method)
|
||||
// {
|
||||
// var semModel = compilation.GetSemanticModel(method.SyntaxTree);
|
||||
// return method.ParameterList.Parameters.Select(p =>
|
||||
// {
|
||||
// var prefix = string.Empty;
|
||||
//
|
||||
// var typeSymbol = semModel.GetTypeInfo(p).Type;
|
||||
//
|
||||
// if (p.Modifiers.Any(static x => x.IsKind(SyntaxKind.ParamsKeyword)))
|
||||
// prefix += "params ";
|
||||
// return prefix + $"{typeSymbol?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {p.Identifier.ToString()}";
|
||||
// });
|
||||
// // foreach (var param in method.ParameterList.Parameters)
|
||||
// // {
|
||||
// // yield return param.ToString();
|
||||
// // }
|
||||
// }
|
||||
|
||||
//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
|
||||
var nameSpace = string.Empty;
|
||||
var parentOfInterest = declarationSyntax.Parent;
|
||||
while (parentOfInterest != 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 default;
|
||||
}
|
||||
|
||||
static IReadOnlyCollection<string> GetClasses(MethodDeclarationSyntax declarationSyntax)
|
||||
{
|
||||
// determine the namespace the class is declared in, if any
|
||||
var classes = new LinkedList<string>();
|
||||
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 = (MethodDeclarationSyntax)ctx.Node;
|
||||
|
||||
foreach (var attListSyntax in methodDecl.AttributeLists)
|
||||
{
|
||||
foreach (var attSyntax in attListSyntax.Attributes)
|
||||
{
|
||||
if (cancel.IsCancellationRequested)
|
||||
return default;
|
||||
|
||||
var symbol = ModelExtensions.GetSymbolInfo(ctx.SemanticModel, attSyntax).Symbol;
|
||||
if (symbol is not IMethodSymbol attSymbol)
|
||||
continue;
|
||||
|
||||
if (attSymbol.ContainingType.ToDisplayString() == "NadekoBot.Common.CmdAttribute")
|
||||
return methodDecl;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user