mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 01:38:27 -04:00
Restructured the project structure back to the way it was, there's no reasonable way to split the modules
This commit is contained in:
258
src/NadekoBot.Generators/Cloneable/CloneableGenerator.cs
Normal file
258
src/NadekoBot.Generators/Cloneable/CloneableGenerator.cs
Normal file
@@ -0,0 +1,258 @@
|
||||
// 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.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 = $$"""
|
||||
// <AutoGenerated/>
|
||||
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 = $$"""
|
||||
// <AutoGenerated/>
|
||||
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 = $$"""
|
||||
// <AutoGenerated/>
|
||||
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}
|
||||
{{
|
||||
/// <summary>
|
||||
/// Creates a copy of {classSymbol.Name} with NO circular reference checking. This method should be used if performance matters.
|
||||
///
|
||||
/// <exception cref=""StackOverflowException"">Will occur on any object that has circular references in the hierarchy.</exception>
|
||||
/// </summary>
|
||||
public {classSymbol.Name} Clone()
|
||||
{{
|
||||
return new {classSymbol.Name}
|
||||
{{
|
||||
{string.Join(",\n", fieldAssignmentsCodeFast)}
|
||||
}};
|
||||
}}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name=""referenceChain"">Should only be provided if specific objects should not be cloned but passed by reference instead.</param>
|
||||
public {classSymbol.Name} CloneSafe(Stack<object> referenceChain = null)
|
||||
{{
|
||||
if(referenceChain?.Contains(this) == true)
|
||||
return this;
|
||||
referenceChain ??= new Stack<object>();
|
||||
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<IPropertySymbol> GetCloneableProperties(ITypeSymbol classSymbol, bool isExplicit)
|
||||
{
|
||||
var targetSymbolMembers = classSymbol.GetMembers().OfType<IPropertySymbol>()
|
||||
.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<INamedTypeSymbol> 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));
|
||||
}
|
||||
}
|
||||
}
|
23
src/NadekoBot.Generators/Cloneable/SymbolExtensions.cs
Normal file
23
src/NadekoBot.Generators/Cloneable/SymbolExtensions.cs
Normal file
@@ -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<AttributeData> 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));
|
||||
}
|
||||
}
|
27
src/NadekoBot.Generators/Cloneable/SyntaxReceiver.cs
Normal file
27
src/NadekoBot.Generators/Cloneable/SyntaxReceiver.cs
Normal file
@@ -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<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();
|
||||
|
||||
/// <summary>
|
||||
/// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
140
src/NadekoBot.Generators/LocalizedStringsGenerator.cs
Normal file
140
src/NadekoBot.Generators/LocalizedStringsGenerator.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
#nullable enable
|
||||
using System.CodeDom.Compiler;
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.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 NadekoBot
|
||||
// {
|
||||
// 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("#pragma warning disable CS8981");
|
||||
sw.WriteLine("namespace NadekoBot;");
|
||||
sw.WriteLine();
|
||||
|
||||
sw.WriteLine("public static class strs");
|
||||
sw.WriteLine("{");
|
||||
sw.Indent++;
|
||||
|
||||
var typedParamStrings = new List<string>(10);
|
||||
foreach (var field in fields)
|
||||
{
|
||||
var matches = Regex.Matches(field.Value, @"{(?<num>\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<TranslationPair> GetFields(string? dataText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dataText))
|
||||
return new();
|
||||
|
||||
Dictionary<string, string> data;
|
||||
try
|
||||
{
|
||||
var output = JsonConvert.DeserializeObject<Dictionary<string, string>>(dataText!);
|
||||
if (output is null)
|
||||
return new();
|
||||
|
||||
data = output;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Debug.WriteLine("Failed parsing responses file.");
|
||||
return new();
|
||||
}
|
||||
|
||||
var list = new List<TranslationPair>();
|
||||
foreach (var entry in data)
|
||||
{
|
||||
list.Add(new(
|
||||
entry.Key,
|
||||
entry.Value
|
||||
));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
27
src/NadekoBot.Generators/NadekoBot.Generators.csproj
Normal file
27
src/NadekoBot.Generators/NadekoBot.Generators.csproj
Normal file
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
<ImplicitUsings>true</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" PrivateAssets="all" GeneratePathProperty="true" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="GetDependencyTargetPaths">
|
||||
<ItemGroup>
|
||||
<TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
|
24
src/NadekoBot.Generators/README.md
Normal file
24
src/NadekoBot.Generators/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
## Generators
|
||||
|
||||
Project which contains source generators required for NadekoBot 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 "NadekoBot" 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"
|
||||
|
Reference in New Issue
Block a user