From 0634470a8a5bbed1a2a6c8dc2ef10c7812f717b0 Mon Sep 17 00:00:00 2001 From: Kwoth Date: Tue, 28 Dec 2021 10:40:59 +0100 Subject: [PATCH] Enabled Nullable reference types. Added a temporary fix for clonable NRT warnings. --- .../Cloneable/CloneableGenerator.cs | 261 ++++++++++++++++++ .../Cloneable/SymbolExtensions.cs | 26 ++ .../Cloneable/SyntaxReceiver.cs | 27 ++ .../LocalizedStringsGenerator.cs | 8 +- .../NadekoBot.Generators.csproj | 1 + src/NadekoBot/NadekoBot.csproj | 3 +- 6 files changed, 320 insertions(+), 6 deletions(-) create mode 100644 src/NadekoBot.Generators/Cloneable/CloneableGenerator.cs create mode 100644 src/NadekoBot.Generators/Cloneable/SymbolExtensions.cs create mode 100644 src/NadekoBot.Generators/Cloneable/SyntaxReceiver.cs diff --git a/src/NadekoBot.Generators/Cloneable/CloneableGenerator.cs b/src/NadekoBot.Generators/Cloneable/CloneableGenerator.cs new file mode 100644 index 000000000..b466830f3 --- /dev/null +++ b/src/NadekoBot.Generators/Cloneable/CloneableGenerator.cs @@ -0,0 +1,261 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Cloneable +{ + [Generator] + public class CloneableGenerator : ISourceGenerator + { + private const string PreventDeepCopyKeyString = "PreventDeepCopy"; + private const string ExplicitDeclarationKeyString = "ExplicitDeclaration"; + + private const string CloneableNamespace = "Cloneable"; + private const string CloneableAttributeString = "CloneableAttribute"; + private const string CloneAttributeString = "CloneAttribute"; + private const string IgnoreCloneAttributeString = "IgnoreCloneAttribute"; + + private const string cloneableAttributeText = @"// +using System; + +namespace " + CloneableNamespace + @" +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = true, AllowMultiple = false)] + public sealed class " + CloneableAttributeString + @" : Attribute + { + public " + CloneableAttributeString + @"() + { + } + + public bool " + ExplicitDeclarationKeyString + @" { get; set; } + } +} +"; + + private const string clonePropertyAttributeText = @"// +using System; + +namespace " + CloneableNamespace + @" +{ + [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class " + CloneAttributeString + @" : Attribute + { + public " + CloneAttributeString + @"() + { + } + + public bool " + PreventDeepCopyKeyString + @" { get; set; } + } +} +"; + + private const string ignoreClonePropertyAttributeText = @"// +using System; + +namespace " + CloneableNamespace + @" +{ + [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class " + IgnoreCloneAttributeString + @" : Attribute + { + public " + IgnoreCloneAttributeString + @"() + { + } + } +} +"; + + 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(ExplicitDeclarationKeyString)).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($"{CloneableNamespace}.{CloneableAttributeString}")!; + cloneAttribute = compilation.GetTypeByMetadataName($"{CloneableNamespace}.{CloneAttributeString}")!; + ignoreCloneAttribute = compilation.GetTypeByMetadataName($"{CloneableNamespace}.{IgnoreCloneAttributeString}")!; + } + + private static Compilation GetCompilation(GeneratorExecutionContext context) + { + var options = context.Compilation.SyntaxTrees.First().Options as CSharpParseOptions; + + var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(cloneableAttributeText, Encoding.UTF8), options)). + AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(clonePropertyAttributeText, Encoding.UTF8), options)). + AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(ignoreClonePropertyAttributeText, Encoding.UTF8), options)); + return compilation; + } + + private string CreateCloneableCode(INamedTypeSymbol classSymbol, bool isExplicit) + { + string namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + var fieldAssignmentsCode = GenerateFieldAssignmentsCode(classSymbol, isExplicit); + 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($",{Environment.NewLine}", 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($",{Environment.NewLine}", 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(PreventDeepCopyKeyString)).Value.Value ?? false; + return (item: x, !preventDeepCopy); + } + + private string GetAccessModifier(INamedTypeSymbol classSymbol) + { + return 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) + { + return 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(CloneableAttributeString, SourceText.From(cloneableAttributeText, Encoding.UTF8)); + context.AddSource(CloneAttributeString, SourceText.From(clonePropertyAttributeText, Encoding.UTF8)); + context.AddSource(IgnoreCloneAttributeString, SourceText.From(ignoreClonePropertyAttributeText, Encoding.UTF8)); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot.Generators/Cloneable/SymbolExtensions.cs b/src/NadekoBot.Generators/Cloneable/SymbolExtensions.cs new file mode 100644 index 000000000..4ae97bcf5 --- /dev/null +++ b/src/NadekoBot.Generators/Cloneable/SymbolExtensions.cs @@ -0,0 +1,26 @@ +// 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) + { + return symbol.GetAttributes() + .Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot.Generators/Cloneable/SyntaxReceiver.cs b/src/NadekoBot.Generators/Cloneable/SyntaxReceiver.cs new file mode 100644 index 000000000..962c3957c --- /dev/null +++ b/src/NadekoBot.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/NadekoBot.Generators/LocalizedStringsGenerator.cs b/src/NadekoBot.Generators/LocalizedStringsGenerator.cs index 742620d19..3a65fbff8 100644 --- a/src/NadekoBot.Generators/LocalizedStringsGenerator.cs +++ b/src/NadekoBot.Generators/LocalizedStringsGenerator.cs @@ -3,10 +3,8 @@ using System.CodeDom.Compiler; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; using Newtonsoft.Json; namespace NadekoBot.Generators @@ -20,7 +18,7 @@ namespace NadekoBot.Generators [Generator] public class LocalizedStringsGenerator : ISourceGenerator { - private const string LocStrSource = @"namespace NadekoBot + private const string LOC_STR_SOURCE = @"namespace NadekoBot { public readonly struct LocStr { @@ -89,10 +87,10 @@ namespace NadekoBot.Generators sw.Flush(); - context.AddSource("strs.cs", stringWriter.ToString()); + context.AddSource("strs.g.cs", stringWriter.ToString()); } - context.AddSource("LocStr.cs", LocStrSource); + context.AddSource("LocStr.g.cs", LOC_STR_SOURCE); } private List GetFields(string dataText) diff --git a/src/NadekoBot.Generators/NadekoBot.Generators.csproj b/src/NadekoBot.Generators/NadekoBot.Generators.csproj index b8c56dc49..3363b47bb 100644 --- a/src/NadekoBot.Generators/NadekoBot.Generators.csproj +++ b/src/NadekoBot.Generators/NadekoBot.Generators.csproj @@ -2,6 +2,7 @@ netstandard2.0 + 9 false diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 4c4eb3346..e30d914bd 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -3,6 +3,7 @@ net6.0 10.0 + enable True true $(MSBuildProjectDirectory) @@ -13,7 +14,7 @@ - +