- Added NadekoBot.Generators projects which will contain source generators

- Implemented initial version of the response strings source generator
  - Creates a class with property names equivalent to key names in responses.en-US.json
  - Each Property has struct type (with generic type parameters matching the number of string format placeholders) for type safe GetText implementation
  - Struct types are readonly refs as they should be ephermal, and only used to pass string keys to GetText
This commit is contained in:
Kwoth
2021-07-23 19:01:26 +02:00
parent e67f659a8a
commit 34d0f66466
10 changed files with 211 additions and 13 deletions

View File

@@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Tests", "src\Nade
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Coordinator", "src\NadekoBot.Coordinator\NadekoBot.Coordinator.csproj", "{AE9B7F8C-81D7-4401-83A3-643B38258374}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Coordinator", "src\NadekoBot.Coordinator\NadekoBot.Coordinator.csproj", "{AE9B7F8C-81D7-4401-83A3-643B38258374}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Generators", "src\NadekoBot.Generators\NadekoBot.Generators.csproj", "{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -54,6 +56,12 @@ Global
{AE9B7F8C-81D7-4401-83A3-643B38258374}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU {AE9B7F8C-81D7-4401-83A3-643B38258374}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
{AE9B7F8C-81D7-4401-83A3-643B38258374}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE9B7F8C-81D7-4401-83A3-643B38258374}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE9B7F8C-81D7-4401-83A3-643B38258374}.Release|Any CPU.Build.0 = Release|Any CPU {AE9B7F8C-81D7-4401-83A3-643B38258374}.Release|Any CPU.Build.0 = Release|Any CPU
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.GlobalNadeko|Any CPU.ActiveCfg = Debug|Any CPU
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -64,6 +72,7 @@ Global
{2F4CF6D6-0C2F-4944-B204-9508CDA53195} = {6058FEDF-A318-4CD4-8F04-A7E8E7EC8874} {2F4CF6D6-0C2F-4944-B204-9508CDA53195} = {6058FEDF-A318-4CD4-8F04-A7E8E7EC8874}
{DB448DD4-C97F-40E9-8BD3-F605FF1FF833} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2} {DB448DD4-C97F-40E9-8BD3-F605FF1FF833} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
{AE9B7F8C-81D7-4401-83A3-643B38258374} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2} {AE9B7F8C-81D7-4401-83A3-643B38258374} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F3F555C-855F-4BE8-B526-D062D3E8ACA4} SolutionGuid = {5F3F555C-855F-4BE8-B526-D062D3E8ACA4}

View File

@@ -0,0 +1,159 @@
using System;
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
{
internal class FieldData
{
public string Type { get; set; }
public string Name { get; set; }
}
[Generator]
public class LocalizedStringsGenerator : ISourceGenerator
{
private const string LocStrSource = @"namespace NadekoBot
{
public readonly ref struct LocStr
{
public readonly string Key;
public LocStr(string key)
{
Key = key;
}
public static implicit operator LocStr(string data)
=> new LocStr(data);
}
public readonly ref struct LocStr<T1>
{
public readonly string Key;
public LocStr(string key)
{
Key = key;
}
public static implicit operator LocStr<T1>(string data)
=> new LocStr<T1>(data);
}
public readonly ref struct LocStr<T1, T2>
{
public readonly string Key;
public LocStr(string key)
{
Key = key;
}
public static implicit operator LocStr<T1, T2>(string data)
=> new LocStr<T1, T2>(data);
}
public readonly ref struct LocStr<T1, T2, T3>
{
public readonly string Key;
public LocStr(string key)
{
Key = key;
}
public static implicit operator LocStr<T1, T2, T3>(string data)
=> new LocStr<T1, T2, T3>(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 NadekoBot");
sw.WriteLine("{");
sw.Indent++;
sw.WriteLine("public static class Strs");
sw.WriteLine("{");
sw.Indent++;
foreach (var field in fields)
{
sw.WriteLine($"public static {field.Type} {field.Name} => \"{field.Name}\";");
}
sw.Indent--;
sw.WriteLine("}");
sw.Indent--;
sw.WriteLine("}");
sw.Flush();
context.AddSource("Strs.cs", stringWriter.ToString());
}
context.AddSource("LocStr.cs", LocStrSource);
}
private List<FieldData> GetFields(string dataText)
{
if (string.IsNullOrWhiteSpace(dataText))
throw new ArgumentNullException(nameof(dataText));
var data = JsonConvert.DeserializeObject<Dictionary<string, string>>(dataText);
var list = new List<FieldData>();
foreach (var entry in data)
{
list.Add(new FieldData()
{
Type = GetFieldType(entry.Value),
Name = entry.Key,
});
}
return list;
}
private string GetFieldType(string value)
{
var matches = Regex.Matches(value, @"{(?<num>\d)}");
int max = 0;
foreach (Match match in matches)
{
max = Math.Max(max, int.Parse(match.Groups["num"].Value));
}
if (max == 0)
return "LocStr";
if (max == 1)
return "LocStr<string>";
if (max == 2)
return "LocStr<string, string>";
if (max == 3)
return "LocStr<string, string, string>";
return "!Error";
}
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.10.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" 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>

View File

@@ -32,6 +32,9 @@ namespace NadekoBot.Modules
protected string GetText(string key) => protected string GetText(string key) =>
Strings.GetText(key, _cultureInfo); Strings.GetText(key, _cultureInfo);
protected string GetText(in LocStr key) =>
Strings.GetText(key.Key, _cultureInfo);
protected string GetText(string key, params object[] args) => protected string GetText(string key, params object[] args) =>
Strings.GetText(key, _cultureInfo, args); Strings.GetText(key, _cultureInfo, args);

View File

@@ -51,7 +51,7 @@ namespace NadekoBot.Modules.Games
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithDescription(ctx.User.ToString()) .WithDescription(ctx.User.ToString())
.AddField("❓ " + GetText("question"), question, false) .AddField("❓ " + GetText("question"), question, false)
.AddField("🎱 " + GetText("8ball"), res, false)); .AddField("🎱 " + GetText("_8ball"), res, false));
} }
[NadekoCommand, Aliases] [NadekoCommand, Aliases]

View File

@@ -532,7 +532,7 @@ namespace NadekoBot.Modules.Searches
var embed = _eb.Create() var embed = _eb.Create()
.WithDescription(ctx.User.Mention) .WithDescription(ctx.User.Mention)
.AddField(GetText("word"), data.Word, true) .AddField(GetText("word"), data.Word, true)
.AddField(GetText("class"), data.WordType, true) .AddField(GetText("_class"), data.WordType, true)
.AddField(GetText("definition"), data.Definition) .AddField(GetText("definition"), data.Definition)
.WithOkColor(); .WithOkColor();

View File

@@ -190,13 +190,13 @@ namespace NadekoBot.Modules.Utility
string description = ""; string description = "";
if (_service.IsNoRedundant(runner.Repeater.Id)) if (_service.IsNoRedundant(runner.Repeater.Id))
{ {
description = Format.Underline(Format.Bold(GetText("no_redundant:"))) + "\n\n"; description = Format.Underline(Format.Bold(GetText("no_redundant"))) + "\n\n";
} }
description += $"<#{runner.Repeater.ChannelId}>\n" + description += $"<#{runner.Repeater.ChannelId}>\n" +
$"`{GetText("interval:")}` {intervalString}\n" + $"`{GetText("Comment")}` {intervalString}\n" +
$"`{GetText("executes_in:")}` {executesInString}\n" + $"`{GetText("executes_in_colon")}` {executesInString}\n" +
$"`{GetText("message:")}` {message}"; $"`{GetText("message_colon")}` {message}";
return description; return description;
} }

View File

@@ -249,7 +249,7 @@ namespace NadekoBot.Modules.Utility
.WithAuthor($"NadekoBot v{StatsService.BotVersion}", .WithAuthor($"NadekoBot v{StatsService.BotVersion}",
"https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/avatar.png", "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/avatar.png",
"https://nadekobot.readthedocs.io/en/latest/") "https://nadekobot.readthedocs.io/en/latest/")
.AddField(GetText("author"), _stats.Author, true) .AddField(GetText(Strs.author), _stats.Author, true)
.AddField(GetText("botid"), _client.CurrentUser.Id.ToString(), true) .AddField(GetText("botid"), _client.CurrentUser.Id.ToString(), true)
.AddField(GetText("shard"), $"#{_client.ShardId} / {_creds.TotalShards}", true) .AddField(GetText("shard"), $"#{_client.ShardId} / {_creds.TotalShards}", true)
.AddField(GetText("commands_ran"), _stats.CommandsRan.ToString(), true) .AddField(GetText("commands_ran"), _stats.CommandsRan.ToString(), true)

View File

@@ -58,8 +58,12 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ayu\Ayu.Discord.Voice\Ayu.Discord.Voice.csproj" /> <ProjectReference Include="..\ayu\Ayu.Discord.Voice\Ayu.Discord.Voice.csproj" />
<ProjectReference Include="..\NadekoBot.Generators\NadekoBot.Generators.csproj" OutputItemType="Analyzer" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<AdditionalFiles Include="data\strings\responses\responses.en-US.json" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Protobuf Include="..\NadekoBot.Coordinator\Protos\coordinator.proto" GrpcServices="Client"> <Protobuf Include="..\NadekoBot.Coordinator\Protos\coordinator.proto" GrpcServices="Client">
<Link>Protos\coordinator.proto</Link> <Link>Protos\coordinator.proto</Link>

View File

@@ -323,7 +323,7 @@
"waifu_reset_fail": "Failed resetting waifu stats. Make sure you have enough currency.", "waifu_reset_fail": "Failed resetting waifu stats. Make sure you have enough currency.",
"waifu_reset_confirm": "This will reset your waifu stats", "waifu_reset_confirm": "This will reset your waifu stats",
"waifu_reset_price": "Price: {0}", "waifu_reset_price": "Price: {0}",
"8ball": "8ball", "_8ball": "8ball",
"acrophobia": "Acrophobia", "acrophobia": "Acrophobia",
"acro_ended_no_sub": "Game ended with no submissions.", "acro_ended_no_sub": "Game ended with no submissions.",
"acro_no_votes_cast": "No votes cast. Game ended with no winner.", "acro_no_votes_cast": "No votes cast. Game ended with no winner.",
@@ -508,7 +508,7 @@
"cost": "Cost", "cost": "Cost",
"date": "Date", "date": "Date",
"word": "Word", "word": "Word",
"class": "Class", "_class": "Class",
"definition": "Definition", "definition": "Definition",
"example": "Example", "example": "Example",
"dropped": "Dropped", "dropped": "Dropped",
@@ -629,10 +629,10 @@
"repeater_removed": "Repeater #{0} Removed", "repeater_removed": "Repeater #{0} Removed",
"repeater_exceed_limit": "You cannot have more than {0} repeaters per server.", "repeater_exceed_limit": "You cannot have more than {0} repeaters per server.",
"repeater_remove_fail": "Failed removing repeater on that index. Either you've specified invalid index, or repeater was in executing state at that time, in which case, try again in a few seconds.", "repeater_remove_fail": "Failed removing repeater on that index. Either you've specified invalid index, or repeater was in executing state at that time, in which case, try again in a few seconds.",
"interval:": "Interval:", "interval_colon": "Interval:",
"executes_in:": "Executes in:", "executes_in_colon": "Executes in:",
"message:": "Message:", "message_colon": "Message:",
"no_redundant:": "Won't post duplicate message.", "no_redundant": "Won't post duplicate message.",
"name": "Name", "name": "Name",
"nickname": "Nickname", "nickname": "Nickname",
"nobody_playing_game": "Nobody is playing that game.", "nobody_playing_game": "Nobody is playing that game.",