From 2e882e4d3cb983edb093a28a47e882188ff726a2 Mon Sep 17 00:00:00 2001 From: Emotion Date: Wed, 12 Jul 2023 16:17:43 +1200 Subject: [PATCH] Added Ellie.Bot.Generators.Cloneable --- .../CloneableGenerator.cs | 258 ++++++++++++++++++ .../Ellie.Bot.Generators.Cloneable.csproj | 17 ++ .../SymbolExtensions.cs | 23 ++ .../SymbolReceiver.cs | 27 ++ 4 files changed, 325 insertions(+) create mode 100644 src/Ellie.Bot.Generators.Cloneable/CloneableGenerator.cs create mode 100644 src/Ellie.Bot.Generators.Cloneable/Ellie.Bot.Generators.Cloneable.csproj create mode 100644 src/Ellie.Bot.Generators.Cloneable/SymbolExtensions.cs create mode 100644 src/Ellie.Bot.Generators.Cloneable/SymbolReceiver.cs diff --git a/src/Ellie.Bot.Generators.Cloneable/CloneableGenerator.cs b/src/Ellie.Bot.Generators.Cloneable/CloneableGenerator.cs new file mode 100644 index 0000000..fd4a379 --- /dev/null +++ b/src/Ellie.Bot.Generators.Cloneable/CloneableGenerator.cs @@ -0,0 +1,258 @@ +// Code temporarily yeeted from +// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs +// because of NRT issue +#nullable disable +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +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)] + internal 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)] + internal 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)] + internal 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)); + } + } +} diff --git a/src/Ellie.Bot.Generators.Cloneable/Ellie.Bot.Generators.Cloneable.csproj b/src/Ellie.Bot.Generators.Cloneable/Ellie.Bot.Generators.Cloneable.csproj new file mode 100644 index 0000000..ac48aac --- /dev/null +++ b/src/Ellie.Bot.Generators.Cloneable/Ellie.Bot.Generators.Cloneable.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + latest + false + true + true + enable + enable + + + + + + + diff --git a/src/Ellie.Bot.Generators.Cloneable/SymbolExtensions.cs b/src/Ellie.Bot.Generators.Cloneable/SymbolExtensions.cs new file mode 100644 index 0000000..8d6de76 --- /dev/null +++ b/src/Ellie.Bot.Generators.Cloneable/SymbolExtensions.cs @@ -0,0 +1,23 @@ +// Code temporarily yeeted from +// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs +// because of NRT issue + +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/Ellie.Bot.Generators.Cloneable/SymbolReceiver.cs b/src/Ellie.Bot.Generators.Cloneable/SymbolReceiver.cs new file mode 100644 index 0000000..e264fca --- /dev/null +++ b/src/Ellie.Bot.Generators.Cloneable/SymbolReceiver.cs @@ -0,0 +1,27 @@ +// Code temporarily yeeted from +// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs +// because of NRT issue + +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); + } + } + } +}