diff --git a/EllieBot.sln b/EllieBot.sln index 4b578fa..c015691 100644 --- a/EllieBot.sln +++ b/EllieBot.sln @@ -23,6 +23,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Tests", "src\Ellie EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EllieBot.Coordinator", "src\EllieBot.Coordinator\EllieBot.Coordinator.csproj", "{A631DDF0-3AD1-4CB9-8458-314B1320868A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EllieBot.Generators", "src\EllieBot.Generators\EllieBot.Generators.csproj", "{CB1A5307-DD85-4795-8A8A-A25D36DADC51}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,10 @@ Global {A631DDF0-3AD1-4CB9-8458-314B1320868A}.Debug|Any CPU.Build.0 = Debug|Any CPU {A631DDF0-3AD1-4CB9-8458-314B1320868A}.Release|Any CPU.ActiveCfg = Release|Any CPU {A631DDF0-3AD1-4CB9-8458-314B1320868A}.Release|Any CPU.Build.0 = Release|Any CPU + {CB1A5307-DD85-4795-8A8A-A25D36DADC51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB1A5307-DD85-4795-8A8A-A25D36DADC51}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB1A5307-DD85-4795-8A8A-A25D36DADC51}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB1A5307-DD85-4795-8A8A-A25D36DADC51}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -55,6 +61,7 @@ Global {5AD2EFFB-7774-49B2-A791-3BAC4DAEE067} = {872A4C63-833C-4AE0-91AB-3CE348D3E6F8} {179DF3B3-AD32-4335-8231-9818338DF3A2} = {B28FB883-9688-41EB-BF5A-945F4A4EB628} {A631DDF0-3AD1-4CB9-8458-314B1320868A} = {B28FB883-9688-41EB-BF5A-945F4A4EB628} + {CB1A5307-DD85-4795-8A8A-A25D36DADC51} = {B28FB883-9688-41EB-BF5A-945F4A4EB628} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {79F61C2C-CDBB-4361-A234-91A0B334CFE4} diff --git a/src/EllieBot.Generators/Cloneable/CloneableGenerator.cs b/src/EllieBot.Generators/Cloneable/CloneableGenerator.cs new file mode 100644 index 0000000..6bd7da4 --- /dev/null +++ b/src/EllieBot.Generators/Cloneable/CloneableGenerator.cs @@ -0,0 +1,254 @@ +// Code temporarily yeeted from +// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs +// because of NRT issue +#nullable enable +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Cloneable +{ + [Generator] + public class CloneableGenerator : ISourceGenerator + { + private const string PREVENT_DEEP_COPY_KEY_STRING = "PreventDeepCopy"; + private const string EXPLICIT_DECLARATION_KEY_STRING = "ExplicitDeclaration"; + + private const string CLONEABLE_NAMESPACE = "Cloneable"; + private const string CLONEABLE_ATTRIBUTE_STRING = "CloneableAttribute"; + private const string CLONE_ATTRIBUTE_STRING = "CloneAttribute"; + private const string IGNORE_CLONE_ATTRIBUTE_STRING = "IgnoreCloneAttribute"; + + private const string CLONEABLE_ATTRIBUTE_TEXT = @"// +using System; + +namespace " + CLONEABLE_NAMESPACE + @" +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = true, AllowMultiple = false)] + public sealed class " + CLONEABLE_ATTRIBUTE_STRING + @" : Attribute + { + public " + CLONEABLE_ATTRIBUTE_STRING + @"() + { + } + + public bool " + EXPLICIT_DECLARATION_KEY_STRING + @" { get; set; } + } +} +"; + + private const string CLONE_PROPERTY_ATTRIBUTE_TEXT = @"// +using System; + +namespace " + CLONEABLE_NAMESPACE + @" +{ + [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class " + CLONE_ATTRIBUTE_STRING + @" : Attribute + { + public " + CLONE_ATTRIBUTE_STRING + @"() + { + } + + public bool " + PREVENT_DEEP_COPY_KEY_STRING + @" { get; set; } + } +} +"; + + private const string IGNORE_CLONE_PROPERTY_ATTRIBUTE_TEXT = @"// +using System; + +namespace " + CLONEABLE_NAMESPACE + @" +{ + [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class " + IGNORE_CLONE_ATTRIBUTE_STRING + @" : Attribute + { + public " + IGNORE_CLONE_ATTRIBUTE_STRING + @"() + { + } + } +} +"; + + private INamedTypeSymbol? _cloneableAttribute; + private INamedTypeSymbol? _ignoreCloneAttribute; + private INamedTypeSymbol? _cloneAttribute; + + public void Initialize(GeneratorInitializationContext context) + => context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + + public void Execute(GeneratorExecutionContext context) + { + InjectCloneableAttributes(context); + GenerateCloneMethods(context); + } + + private void GenerateCloneMethods(GeneratorExecutionContext context) + { + if (context.SyntaxReceiver is not SyntaxReceiver receiver) + return; + + Compilation compilation = GetCompilation(context); + + InitAttributes(compilation); + + var classSymbols = GetClassSymbols(compilation, receiver); + foreach (var classSymbol in classSymbols) + { + if (!classSymbol.TryGetAttribute(_cloneableAttribute!, out var attributes)) + continue; + + var attribute = attributes.Single(); + var isExplicit = (bool?)attribute.NamedArguments.FirstOrDefault(e => e.Key.Equals(EXPLICIT_DECLARATION_KEY_STRING)).Value.Value ?? false; + context.AddSource($"{classSymbol.Name}_cloneable.g.cs", SourceText.From(CreateCloneableCode(classSymbol, isExplicit), Encoding.UTF8)); + } + } + + private void InitAttributes(Compilation compilation) + { + _cloneableAttribute = compilation.GetTypeByMetadataName($"{CLONEABLE_NAMESPACE}.{CLONEABLE_ATTRIBUTE_STRING}")!; + _cloneAttribute = compilation.GetTypeByMetadataName($"{CLONEABLE_NAMESPACE}.{CLONE_ATTRIBUTE_STRING}")!; + _ignoreCloneAttribute = compilation.GetTypeByMetadataName($"{CLONEABLE_NAMESPACE}.{IGNORE_CLONE_ATTRIBUTE_STRING}")!; + } + + private static Compilation GetCompilation(GeneratorExecutionContext context) + { + var options = context.Compilation.SyntaxTrees.First().Options as CSharpParseOptions; + + var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(CLONEABLE_ATTRIBUTE_TEXT, Encoding.UTF8), options)). + AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8), options)). + AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(IGNORE_CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8), options)); + return compilation; + } + + private string CreateCloneableCode(INamedTypeSymbol classSymbol, bool isExplicit) + { + string namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + var fieldAssignmentsCode = GenerateFieldAssignmentsCode(classSymbol, isExplicit).ToList(); + var fieldAssignmentsCodeSafe = fieldAssignmentsCode.Select(x => + { + if (x.isCloneable) + return x.line + "Safe(referenceChain)"; + return x.line; + }); + var fieldAssignmentsCodeFast = fieldAssignmentsCode.Select(x => + { + if (x.isCloneable) + return x.line + "()"; + return x.line; + }); + + return $@"using System.Collections.Generic; + +namespace {namespaceName} +{{ + {GetAccessModifier(classSymbol)} partial class {classSymbol.Name} + {{ + /// + /// Creates a copy of {classSymbol.Name} with NO circular reference checking. This method should be used if performance matters. + /// + /// Will occur on any object that has circular references in the hierarchy. + /// + public {classSymbol.Name} Clone() + {{ + return new {classSymbol.Name} + {{ +{string.Join(",\n", fieldAssignmentsCodeFast)} + }}; + }} + + /// + /// Creates a copy of {classSymbol.Name} with circular reference checking. If a circular reference was detected, only a reference of the leaf object is passed instead of cloning it. + /// + /// Should only be provided if specific objects should not be cloned but passed by reference instead. + public {classSymbol.Name} CloneSafe(Stack referenceChain = null) + {{ + if(referenceChain?.Contains(this) == true) + return this; + referenceChain ??= new Stack(); + referenceChain.Push(this); + var result = new {classSymbol.Name} + {{ +{string.Join($",\n", fieldAssignmentsCodeSafe)} + }}; + referenceChain.Pop(); + return result; + }} + }} +}}"; + } + + private IEnumerable<(string line, bool isCloneable)> GenerateFieldAssignmentsCode(INamedTypeSymbol classSymbol, bool isExplicit) + { + var fieldNames = GetCloneableProperties(classSymbol, isExplicit); + + var fieldAssignments = fieldNames.Select(field => IsFieldCloneable(field, classSymbol)) + .OrderBy(x => x.isCloneable) + .Select(x => (GenerateAssignmentCode(x.item.Name, x.isCloneable), x.isCloneable)); + return fieldAssignments; + } + + private string GenerateAssignmentCode(string name, bool isCloneable) + { + if (isCloneable) + { + return $@" {name} = this.{name}?.Clone"; + } + + return $@" {name} = this.{name}"; + } + + private (IPropertySymbol item, bool isCloneable) IsFieldCloneable(IPropertySymbol x, INamedTypeSymbol classSymbol) + { + if (SymbolEqualityComparer.Default.Equals(x.Type, classSymbol)) + { + return (x, false); + } + + if (!x.Type.TryGetAttribute(_cloneableAttribute!, out var attributes)) + { + return (x, false); + } + + var preventDeepCopy = (bool?)attributes.Single().NamedArguments.FirstOrDefault(e => e.Key.Equals(PREVENT_DEEP_COPY_KEY_STRING)).Value.Value ?? false; + return (item: x, !preventDeepCopy); + } + + private string GetAccessModifier(INamedTypeSymbol classSymbol) + => classSymbol.DeclaredAccessibility.ToString().ToLowerInvariant(); + + private IEnumerable GetCloneableProperties(ITypeSymbol classSymbol, bool isExplicit) + { + var targetSymbolMembers = classSymbol.GetMembers().OfType() + .Where(x => x.SetMethod is not null && + x.CanBeReferencedByName); + if (isExplicit) + { + return targetSymbolMembers.Where(x => x.HasAttribute(_cloneAttribute!)); + } + else + { + return targetSymbolMembers.Where(x => !x.HasAttribute(_ignoreCloneAttribute!)); + } + } + + private static IEnumerable GetClassSymbols(Compilation compilation, SyntaxReceiver receiver) + => receiver.CandidateClasses.Select(clazz => GetClassSymbol(compilation, clazz)); + + private static INamedTypeSymbol GetClassSymbol(Compilation compilation, ClassDeclarationSyntax clazz) + { + var model = compilation.GetSemanticModel(clazz.SyntaxTree); + var classSymbol = model.GetDeclaredSymbol(clazz)!; + return classSymbol; + } + + private static void InjectCloneableAttributes(GeneratorExecutionContext context) + { + context.AddSource(CLONEABLE_ATTRIBUTE_STRING, SourceText.From(CLONEABLE_ATTRIBUTE_TEXT, Encoding.UTF8)); + context.AddSource(CLONE_ATTRIBUTE_STRING, SourceText.From(CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8)); + context.AddSource(IGNORE_CLONE_ATTRIBUTE_STRING, SourceText.From(IGNORE_CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8)); + } + } +} \ No newline at end of file diff --git a/src/EllieBot.Generators/Cloneable/SymbolExtensions.cs b/src/EllieBot.Generators/Cloneable/SymbolExtensions.cs new file mode 100644 index 0000000..6c0c605 --- /dev/null +++ b/src/EllieBot.Generators/Cloneable/SymbolExtensions.cs @@ -0,0 +1,24 @@ +// Code temporarily yeeted from +// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs +// because of NRT issue +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Cloneable +{ + internal static class SymbolExtensions + { + public static bool TryGetAttribute(this ISymbol symbol, INamedTypeSymbol attributeType, + out IEnumerable attributes) + { + attributes = symbol.GetAttributes() + .Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); + return attributes.Any(); + } + + public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeType) + => symbol.GetAttributes() + .Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); + } +} \ No newline at end of file diff --git a/src/EllieBot.Generators/Cloneable/SyntaxReceiver.cs b/src/EllieBot.Generators/Cloneable/SyntaxReceiver.cs new file mode 100644 index 0000000..962c395 --- /dev/null +++ b/src/EllieBot.Generators/Cloneable/SyntaxReceiver.cs @@ -0,0 +1,27 @@ +// Code temporarily yeeted from +// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs +// because of NRT issue +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Cloneable +{ + internal class SyntaxReceiver : ISyntaxReceiver + { + public IList CandidateClasses { get; } = new List(); + + /// + /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation + /// + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + // any field with at least one attribute is a candidate for being cloneable + if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax && + classDeclarationSyntax.AttributeLists.Count > 0) + { + CandidateClasses.Add(classDeclarationSyntax); + } + } + } +} \ No newline at end of file diff --git a/src/EllieBot.Generators/Command/CommandAttributesGenerator.cs b/src/EllieBot.Generators/Command/CommandAttributesGenerator.cs new file mode 100644 index 0000000..81a758c --- /dev/null +++ b/src/EllieBot.Generators/Command/CommandAttributesGenerator.cs @@ -0,0 +1,336 @@ +// #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 EllieBot.Generators.Command; +// +// [Generator] +// public class CommandAttributesGenerator : IIncrementalGenerator +// { +// public const string ATTRIBUTE = @"// +// +// namespace EllieBot.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 +// // if (!Debugger.IsAttached) +// // Debugger.Launch(); +// // // 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("[EllieCommand]"); +// tw.WriteLine("[EllieDescription]"); +// 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!)) +// .Where(static x => x is not null) +// .Cast(); +// +// 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); +// +// SemanticModel semanticModel; +// try +// { +// semanticModel = comp.GetSemanticModel(decl.SyntaxTree); +// } +// catch +// { +// // for some reason this method can throw "Not part of this compilation" argument exception +// return null; +// } +// +// 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() == "EllieBot.Common.CmdAttribute") +// return methodDecl; +// } +// } +// +// return default; +// } +// } \ No newline at end of file diff --git a/src/EllieBot.Generators/EllieBot.Generators.csproj b/src/EllieBot.Generators/EllieBot.Generators.csproj new file mode 100644 index 0000000..742b2ea --- /dev/null +++ b/src/EllieBot.Generators/EllieBot.Generators.csproj @@ -0,0 +1,25 @@ + + + + netstandard2.0 + latest + false + true + + + + + + + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + diff --git a/src/EllieBot.Generators/LocalizedStringsGenerator.cs b/src/EllieBot.Generators/LocalizedStringsGenerator.cs new file mode 100644 index 0000000..95abda9 --- /dev/null +++ b/src/EllieBot.Generators/LocalizedStringsGenerator.cs @@ -0,0 +1,144 @@ +#nullable enable +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Newtonsoft.Json; + +namespace EllieBot.Generators +{ + internal readonly struct TranslationPair + { + public string Name { get; } + public string Value { get; } + + public TranslationPair(string name, string value) + { + Name = name; + Value = value; + } + } + + [Generator] + public class LocalizedStringsGenerator : ISourceGenerator + { + private const string LOC_STR_SOURCE = @"namespace EllieBot +{ + public readonly struct LocStr + { + public readonly string Key; + public readonly object[] Params; + + public LocStr(string key, params object[] data) + { + Key = key; + Params = data; + } + } +}"; + + public void Initialize(GeneratorInitializationContext context) + { + + } + + public void Execute(GeneratorExecutionContext context) + { + var file = context.AdditionalFiles.First(x => x.Path.EndsWith("responses.en-US.json")); + + var fields = GetFields(file.GetText()?.ToString()); + + using (var stringWriter = new StringWriter()) + using (var sw = new IndentedTextWriter(stringWriter)) + { + sw.WriteLine("namespace EllieBot;"); + sw.WriteLine(); + + sw.WriteLine("public static class strs"); + sw.WriteLine("{"); + sw.Indent++; + + var typedParamStrings = new List(10); + foreach (var field in fields) + { + var matches = Regex.Matches(field.Value, @"{(?\d)[}:]"); + var max = 0; + foreach (Match match in matches) + { + max = Math.Max(max, int.Parse(match.Groups["num"].Value) + 1); + } + + typedParamStrings.Clear(); + var typeParams = new string[max]; + var passedParamString = string.Empty; + for (var i = 0; i < max; i++) + { + typedParamStrings.Add($"in T{i} p{i}"); + passedParamString += $", p{i}"; + typeParams[i] = $"T{i}"; + } + + var sig = string.Empty; + var typeParamStr = string.Empty; + if (max > 0) + { + sig = $"({string.Join(", ", typedParamStrings)})"; + typeParamStr = $"<{string.Join(", ", typeParams)}>"; + } + + sw.WriteLine("public static LocStr {0}{1}{2} => new LocStr(\"{3}\"{4});", + field.Name, + typeParamStr, + sig, + field.Name, + passedParamString); + } + + sw.Indent--; + sw.WriteLine("}"); + + + sw.Flush(); + context.AddSource("strs.g.cs", stringWriter.ToString()); + } + + context.AddSource("LocStr.g.cs", LOC_STR_SOURCE); + } + + private List GetFields(string? dataText) + { + if (string.IsNullOrWhiteSpace(dataText)) + return new(); + + Dictionary data; + try + { + var output = JsonConvert.DeserializeObject>(dataText!); + if (output is null) + return new(); + + data = output; + } + catch + { + Debug.WriteLine("Failed parsing responses file."); + return new(); + } + + var list = new List(); + foreach (var entry in data) + { + list.Add(new( + entry.Key, + entry.Value + )); + } + + return list; + } + } +} \ No newline at end of file diff --git a/src/EllieBot.Generators/README.md b/src/EllieBot.Generators/README.md new file mode 100644 index 0000000..cfe1ea4 --- /dev/null +++ b/src/EllieBot.Generators/README.md @@ -0,0 +1,24 @@ +## Generators + +Project which contains source generators required for EllieBot project + +--- +### 1) Localized Strings Generator + + -- Why -- + Type safe response strings access, and enforces correct usage of response strings. + + -- How it works -- + Creates a file "strs.cs" containing a class called "strs" in "EllieBot" namespace. + + Loads "data/strings/responses.en-US.json" and creates a property or a function for each key in the responses json file based on whether the value has string format placeholders or not. + + - If a value has no placeholders, it creates a property in the strs class which returns an instance of a LocStr struct containing only the key and no replacement parameters + + - If a value has placeholders, it creates a function with the same number of arguments as the number of placeholders, and passes those arguments to the LocStr instance + + -- How to use -- + 1. Add a new key to responses.en-US.json "greet_me": "Hello, {0}" + 2. You now have access to a function strs.greet_me(obj p1) + 3. Using "GetText(strs.greet_me("Me"))" will return "Hello, Me" +