diff --git a/Flow.Launcher.Localization.Analyzers/Flow.Launcher.Localization.Analyzers.csproj b/Flow.Launcher.Localization.Analyzers/Flow.Launcher.Localization.Analyzers.csproj index e9ff31a..ce219d0 100644 --- a/Flow.Launcher.Localization.Analyzers/Flow.Launcher.Localization.Analyzers.csproj +++ b/Flow.Launcher.Localization.Analyzers/Flow.Launcher.Localization.Analyzers.csproj @@ -8,12 +8,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj b/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj index 6c83304..40d585d 100644 --- a/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj +++ b/Flow.Launcher.Localization.SourceGenerators/Flow.Launcher.Localization.SourceGenerators.csproj @@ -8,11 +8,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs index 4c97db5..1d48f6e 100644 --- a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs +++ b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -11,10 +13,13 @@ namespace Flow.Launcher.Localization.SourceGenerators.Localize { + /// + /// Generates properties for strings based on resource files. + /// [Generator] - public partial class LocalizeSourceGenerator : ISourceGenerator + public partial class LocalizeSourceGenerator : IIncrementalGenerator { - private OptimizationLevel _optimizationLevel; + #region Fields private const string CoreNamespace1 = "Flow.Launcher"; private const string CoreNamespace2 = "Flow.Launcher.Core"; @@ -22,477 +27,580 @@ public partial class LocalizeSourceGenerator : ISourceGenerator private const string ClassName = "Localize"; private const string PluginInterfaceName = "IPluginI18n"; private const string PluginContextTypeName = "PluginInitContext"; - private const string KeywordStatic = "static"; - private const string KeywordPrivate = "private"; - private const string KeywordProtected = "protected"; private const string XamlPrefix = "system"; private const string XamlTag = "String"; - private const string DefaultLanguageFilePathEndsWith = @"\Languages\en.xaml"; - private const string XamlCustomPathPropertyKey = "build_property.localizegeneratorlangfiles"; - private readonly char[] _xamlCustomPathPropertyDelimiters = { '\n', ';' }; private readonly Regex _languagesXamlRegex = new Regex(@"\\Languages\\[^\\]+\.xaml$", RegexOptions.IgnoreCase); - public void Initialize(GeneratorInitializationContext context) + private static readonly Version PackageVersion = typeof(LocalizeSourceGenerator).Assembly.GetName().Version; + + #endregion + + #region Incremental Generator + + /// + /// Initializes the generator and registers source output based on resource files. + /// + /// The initialization context. + public void Initialize(IncrementalGeneratorInitializationContext context) { + var xamlFiles = context.AdditionalTextsProvider + .Where(file => _languagesXamlRegex.IsMatch(file.Path)); + + var localizedStrings = xamlFiles + .Select((file, ct) => ParseXamlFile(file, ct)) + .Collect() + .SelectMany((files, _) => files); + + // TODO: Add support for usedKeys + var invocationKeys = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (n, _) => n is InvocationExpressionSyntax, + transform: GetLocalizationKeyFromInvocation) + .Where(key => !string.IsNullOrEmpty(key)) + .Collect() + .Select((keys, _) => keys.Distinct().ToImmutableHashSet()); + + var pluginClasses = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (n, _) => n is ClassDeclarationSyntax, + transform: GetPluginClassInfo) + .Where(info => info != null) + .Collect(); + + var compilation = context.CompilationProvider; + + var combined = localizedStrings.Combine(invocationKeys).Combine(pluginClasses).Combine(compilation).Combine(xamlFiles.Collect()); + + context.RegisterSourceOutput(combined, Execute); } - public void Execute(GeneratorExecutionContext context) + /// + /// Executes the generation of string properties based on the provided data. + /// + /// The source production context. + /// The provided data. + private void Execute(SourceProductionContext spc, + ((((ImmutableArray LocalizableStrings, + ImmutableHashSet InvocationKeys), + ImmutableArray PluginClassInfos), + Compilation Compilation), + ImmutableArray AdditionalTexts) data) { - _optimizationLevel = context.Compilation.Options.OptimizationLevel; - - context.AnalyzerConfigOptions.GlobalOptions.TryGetValue( - XamlCustomPathPropertyKey, - out var langFilePathEndsWithStr - ); - - var allLanguageKeys = new List(); - context.Compilation.SyntaxTrees - .SelectMany(v => v.GetRoot().DescendantNodes().OfType()) - .ToList() - .ForEach( - v => - { - var split = v.Expression.ToString().Split('.'); - if (split.Length < 2) return; - if (split[0] != ClassName) return; - allLanguageKeys.Add(split[1]); - }); - - var allXamlFiles = context.AdditionalFiles - .Where(v => _languagesXamlRegex.IsMatch(v.Path)) - .ToArray(); - AdditionalText[] resourceDictionaries; - if (allXamlFiles.Length is 0) + var xamlFiles = data.AdditionalTexts; + if (xamlFiles.Length == 0) { - context.ReportDiagnostic(Diagnostic.Create( + spc.ReportDiagnostic(Diagnostic.Create( SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries, Location.None )); return; } - if (string.IsNullOrEmpty(langFilePathEndsWithStr)) + var compilation = data.Item1.Compilation; + var pluginClasses = data.Item1.Item1.PluginClassInfos; + var usedKeys = data.Item1.Item1.Item1.InvocationKeys; + var localizedStrings = data.Item1.Item1.Item1.LocalizableStrings; + + var assemblyName = compilation.AssemblyName ?? DefaultNamespace; + var optimizationLevel = compilation.Options.OptimizationLevel; + + var pluginInfo = GetValidPluginInfo(pluginClasses, spc); + var isCoreAssembly = assemblyName == CoreNamespace1 || assemblyName == CoreNamespace2; + + GenerateSource( + spc, + xamlFiles[0], + localizedStrings, + optimizationLevel, + assemblyName, + isCoreAssembly, + pluginInfo, + usedKeys); + } + + #endregion + + #region Parse Xaml File + + private static ImmutableArray ParseXamlFile(AdditionalText file, CancellationToken ct) + { + var content = file.GetText(ct)?.ToString(); + if (content is null) { - if (allXamlFiles.Length is 1) - { - resourceDictionaries = allXamlFiles; - } - else - { - resourceDictionaries = allXamlFiles.Where(v => v.Path.EndsWith(DefaultLanguageFilePathEndsWith)).ToArray(); - if (resourceDictionaries.Length is 0) - { - context.ReportDiagnostic(Diagnostic.Create( - SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries, - Location.None - )); - return; - } - } + return ImmutableArray.Empty; } - else + + var doc = XDocument.Parse(content); + var systemNs = doc.Root?.GetNamespaceOfPrefix(XamlPrefix); // Should be "system" + var xNs = doc.Root?.GetNamespaceOfPrefix("x"); + if (systemNs is null || xNs is null) { - var langFilePathEndings = langFilePathEndsWithStr - .Trim() - .Split(_xamlCustomPathPropertyDelimiters) - .Select(v => v.Trim()) - .ToArray(); - resourceDictionaries = allXamlFiles.Where(v => langFilePathEndings.Any(v.Path.EndsWith)).ToArray(); - if (resourceDictionaries.Length is 0) - { - context.ReportDiagnostic(Diagnostic.Create( - SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries, - Location.None - )); - return; - } + return ImmutableArray.Empty; } - var ns = context.Compilation.AssemblyName ?? DefaultNamespace; - - var localizedStrings = LoadLocalizedStrings(resourceDictionaries); - - var unusedLocalizationKeys = localizedStrings.Keys.Except(allLanguageKeys).ToArray(); + var localizableStrings = new List(); + foreach (var element in doc.Descendants(systemNs + XamlTag)) // "String" elements in system namespace + { + if (ct.IsCancellationRequested) + { + return ImmutableArray.Empty; + } - foreach (var key in unusedLocalizationKeys) - context.ReportDiagnostic(Diagnostic.Create( - SourceGeneratorDiagnostics.LocalizationKeyUnused, - Location.None, - key - )); + var key = element.Attribute(xNs + "Key")?.Value; // Correctly get x:Key + var value = element.Value; + var comment = element.PreviousNode as XComment; - var sourceCode = GenerateSourceCode(localizedStrings, context, unusedLocalizationKeys); + if (key != null) + { + localizableStrings.Add(ParseLocalizableString(key, value, comment)); + } + } - context.AddSource($"{ClassName}.{ns}.g.cs", SourceText.From(sourceCode, Encoding.UTF8)); + return localizableStrings.ToImmutableArray(); } - private static Dictionary LoadLocalizedStrings(AdditionalText[] files) + private static LocalizableString ParseLocalizableString(string key, string value, XComment comment) { - var result = new Dictionary(); + var (summary, parameters) = ParseComment(comment); + return new LocalizableString(key, value, summary, parameters); + } - foreach (var file in files) + private static (string Summary, ImmutableArray Parameters) ParseComment(XComment comment) + { + if (comment == null || comment.Value == null) { - ProcessXamlFile(file, result); + return (null, ImmutableArray.Empty); } - return result; + try + { + var doc = XDocument.Parse($"{comment.Value}"); + var summary = doc.Descendants("summary").FirstOrDefault()?.Value.Trim(); + var parameters = doc.Descendants("param") + .Select(p => new LocalizableStringParam( + int.Parse(p.Attribute("index").Value), + p.Attribute("name").Value, + p.Attribute("type").Value)) + .ToImmutableArray(); + return (summary, parameters); + } + catch + { + return (null, ImmutableArray.Empty); + } } - private static void ProcessXamlFile(AdditionalText file, Dictionary result) { - var content = file.GetText()?.ToString(); - if (content is null) return; - var doc = XDocument.Parse(content); - var ns = doc.Root?.GetNamespaceOfPrefix(XamlPrefix); - if (ns is null) return; - foreach (var element in doc.Descendants(ns + XamlTag)) - { - var name = element.FirstAttribute?.Value; - var value = element.Value; + #endregion - if (name is null) continue; + #region Get Used Localization Keys - string summary = null; - var paramsList = new List(); - var commentNode = element.PreviousNode; + // TODO: Add support for usedKeys + private static string GetLocalizationKeyFromInvocation(GeneratorSyntaxContext context, CancellationToken ct) + { + if (ct.IsCancellationRequested) + { + return null; + } - if (commentNode is XComment comment) - summary = ProcessXamlFileComment(comment, paramsList); + var invocation = (InvocationExpressionSyntax)context.Node; + var expression = invocation.Expression; + var parts = new List(); - result[name] = new LocalizableString(name, value, summary, paramsList); + // Traverse the member access hierarchy + while (expression is MemberAccessExpressionSyntax memberAccess) + { + parts.Add(memberAccess.Name.Identifier.Text); + expression = memberAccess.Expression; } - } - private static string ProcessXamlFileComment(XComment comment, List paramsList) { - string summary = null; - try + // Add the leftmost identifier + if (expression is IdentifierNameSyntax identifier) { - if (CommentIncludesDocumentationMarkup(comment)) - { - var commentDoc = XDocument.Parse($"{comment.Value}"); - summary = ExtractDocumentationCommentSummary(commentDoc); - foreach (var param in commentDoc.Descendants("param")) - { - if (!int.TryParse(param.Attribute("index")?.Value, out var index)) - { - index = -1; - } - var paramName = param.Attribute("name")?.Value; - var paramType = param.Attribute("type")?.Value; - if (index < 0 || paramName is null || paramType is null) continue; - paramsList.Add(new LocalizableStringParam(index, paramName, paramType)); - } - } + parts.Add(identifier.Identifier.Text); + } + else + { + return null; } - catch (Exception ex) + + // Reverse to get [ClassName, SubClass, Method] from [Method, SubClass, ClassName] + parts.Reverse(); + + // Check if the first part is ClassName and there's at least one more part + if (parts.Count < 2 || parts[0] != ClassName) { - // ignore - Console.WriteLine($"Exception in ProcessXamlFileComment: {ex.Message}"); + return null; } - return summary; + return parts[1]; } - private static string ExtractDocumentationCommentSummary(XDocument commentDoc) { - return commentDoc.Descendants("summary").FirstOrDefault()?.Value.Trim(); - } + #endregion - private static bool CommentIncludesDocumentationMarkup(XComment comment) { - return comment.Value.Contains("") || comment.Value.Contains(" localizedStrings, - GeneratorExecutionContext context, - string[] unusedLocalizationKeys - ) + private static PluginClassInfo GetPluginClassInfo(GeneratorSyntaxContext context, CancellationToken ct) { - var ns = context.Compilation.AssemblyName; - - var sb = new StringBuilder(); - if (ns is CoreNamespace1 || ns is CoreNamespace2) + var classDecl = (ClassDeclarationSyntax)context.Node; + var location = GetLocation(context.SemanticModel.SyntaxTree, classDecl); + if (!classDecl.BaseList?.Types.Any(t => t.Type.ToString() == PluginInterfaceName) ?? true) { - GenerateFileHeader(sb, context); - GenerateClass(sb, localizedStrings, unusedLocalizationKeys); - return sb.ToString(); + // Cannot find class that implements IPluginI18n + return null; } - string contextPropertyName = null; - var mainClassFound = false; - foreach (var (syntaxTree, classDeclaration) in GetClasses(context)) + var property = classDecl.Members + .OfType() + .FirstOrDefault(p => p.Type.ToString() == PluginContextTypeName); + if (property is null) { - if (!DoesClassImplementInterface(classDeclaration, PluginInterfaceName)) - continue; + // Cannot find context + return new PluginClassInfo(location, classDecl.Identifier.Text, null, false, false, false); + } - mainClassFound = true; + var modifiers = property.Modifiers; + return new PluginClassInfo( + location, + classDecl.Identifier.Text, + property.Identifier.Text, + modifiers.Any(SyntaxKind.StaticKeyword), + modifiers.Any(SyntaxKind.PrivateKeyword), + modifiers.Any(SyntaxKind.ProtectedKeyword)); + } + + private static PluginClassInfo GetValidPluginInfo( + ImmutableArray pluginClasses, + SourceProductionContext context) + { + var nonNullExistClasses = pluginClasses.Where(p => p != null && p.PropertyName != null).ToArray(); + if (nonNullExistClasses.Length == 0) + { + context.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.CouldNotFindPluginEntryClass, + Location.None + )); + return null; + } - var property = GetPluginContextProperty(classDeclaration); - if (property is null) + foreach (var pluginClass in nonNullExistClasses) + { + if (pluginClass.IsValid == true) { - context.ReportDiagnostic(Diagnostic.Create( - SourceGeneratorDiagnostics.CouldNotFindContextProperty, - GetLocation(syntaxTree, classDeclaration), - classDeclaration.Identifier - )); - return string.Empty; + return pluginClass; } - var propertyModifiers = GetPropertyModifiers(property); - - if (!propertyModifiers.Static) + if (!pluginClass.IsStatic) { context.ReportDiagnostic(Diagnostic.Create( SourceGeneratorDiagnostics.ContextPropertyNotStatic, - GetLocation(syntaxTree, property), - property.Identifier + pluginClass.Location, + pluginClass.PropertyName )); - return string.Empty; } - if (propertyModifiers.Private) + if (pluginClass.IsPrivate) { context.ReportDiagnostic(Diagnostic.Create( SourceGeneratorDiagnostics.ContextPropertyIsPrivate, - GetLocation(syntaxTree, property), - property.Identifier + pluginClass.Location, + pluginClass.PropertyName )); - return string.Empty; } - if (propertyModifiers.Protected) + if (pluginClass.IsProtected) { context.ReportDiagnostic(Diagnostic.Create( SourceGeneratorDiagnostics.ContextPropertyIsProtected, - GetLocation(syntaxTree, property), - property.Identifier + pluginClass.Location, + pluginClass.PropertyName )); - return string.Empty; } + } + + return null; + } + + private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode classDeclaration) + { + return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan); + } + +#endregion + + #region Generate Source + + private static void GenerateSource( + SourceProductionContext spc, + AdditionalText xamlFile, + ImmutableArray localizedStrings, + OptimizationLevel optimizationLevel, + string assemblyName, + bool isCoreAssembly, + PluginClassInfo pluginInfo, + IEnumerable usedKeys) + { + // Get unusedKeys if we need to optimize + IEnumerable unusedKeys = new List(); + if (optimizationLevel == OptimizationLevel.Release) + { + unusedKeys = localizedStrings + .Select(ls => ls.Key) + .ToImmutableHashSet() + .Except(usedKeys); - contextPropertyName = $"{classDeclaration.Identifier}.{property.Identifier}"; - break; + foreach (var key in unusedKeys) + { + spc.ReportDiagnostic(Diagnostic.Create( + SourceGeneratorDiagnostics.LocalizationKeyUnused, + Location.None, + key)); + } } - if (mainClassFound is false) + var sourceBuilder = new StringBuilder(); + + // Generate header + GeneratedHeaderFromPath(sourceBuilder, xamlFile.Path); + sourceBuilder.AppendLine(); + + // Generate usings + if (isCoreAssembly) { - context.ReportDiagnostic(Diagnostic.Create( - SourceGeneratorDiagnostics.CouldNotFindPluginEntryClass, - Location.None - )); - return string.Empty; + sourceBuilder.AppendLine("using Flow.Launcher.Core.Resource;"); + sourceBuilder.AppendLine(); } - GenerateFileHeader(sb, context, true); - GenerateClass(sb, localizedStrings, unusedLocalizationKeys, contextPropertyName); - return sb.ToString(); - } + // Generate nullable enable + sourceBuilder.AppendLine("#nullable enable"); + sourceBuilder.AppendLine(); - private static void GenerateFileHeader(StringBuilder sb, GeneratorExecutionContext context, bool isPlugin = false) - { - var rootNamespace = context.Compilation.AssemblyName; - sb.AppendLine("// "); - sb.AppendLine("#nullable enable"); + // Generate namespace + sourceBuilder.AppendLine($"namespace {assemblyName};"); + sourceBuilder.AppendLine(); - if (!isPlugin) - sb.AppendLine("using Flow.Launcher.Core.Resource;"); + // Uncomment them for debugging + //sourceBuilder.AppendLine("/*"); + /*// Generate all localization strings + sourceBuilder.AppendLine("localizedStrings"); + foreach (var ls in localizedStrings) + { + sourceBuilder.AppendLine($"{ls.Key} - {ls.Value}"); + } + sourceBuilder.AppendLine(); - sb.AppendLine($"namespace {rootNamespace};"); - } + // Generate all unused keys + sourceBuilder.AppendLine("unusedKeys"); + foreach (var key in unusedKeys) + { + sourceBuilder.AppendLine($"{key}"); + } + sourceBuilder.AppendLine(); - private void GenerateClass( - StringBuilder sb, - Dictionary localizedStrings, - string[] unusedLocalizationKeys, - string propertyName = null - ) { - const string name = nameof(LocalizeSourceGenerator); - var version = typeof(LocalizeSourceGenerator).Assembly.GetName().Version; - sb.AppendLine(); - sb.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{name}\", \"{version}\")]"); - sb.AppendLine($"public static class {ClassName}"); - sb.AppendLine("{"); - foreach (var localizedString in localizedStrings) + // Generate all used keys + sourceBuilder.AppendLine("usedKeys"); + foreach (var key in usedKeys) { - if (_optimizationLevel == OptimizationLevel.Release && unusedLocalizationKeys.Contains(localizedString.Key)) + sourceBuilder.AppendLine($"{key}"); + }*/ + //sourceBuilder.AppendLine("*/"); + + // Generate class + sourceBuilder.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{nameof(LocalizeSourceGenerator)}\", \"{PackageVersion}\")]"); + sourceBuilder.AppendLine($"public static class {ClassName}"); + sourceBuilder.AppendLine("{"); + + // Generate localization methods + var tabString = Spacing(1); + foreach (var ls in localizedStrings) + { + // TODO: Add support for usedKeys + /*if (unusedKeys.Contains(ls.Key)) + { continue; + }*/ - GenerateDocCommentForMethod(sb, localizedString.Value); - GenerateMethod(sb, localizedString.Value, propertyName); + GenerateDocComments(sourceBuilder, ls, tabString); + GenerateLocalizationMethod(sourceBuilder, ls, isCoreAssembly, pluginInfo, tabString); } - sb.AppendLine("}"); + sourceBuilder.AppendLine("}"); + + // Add source to context + spc.AddSource($"{ClassName}.{assemblyName}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); } - private static void GenerateDocCommentForMethod(StringBuilder sb, LocalizableString localizableString) + private static void GeneratedHeaderFromPath(StringBuilder sb, string xamlFilePath) { - sb.AppendLine("/// "); - if (!(localizableString.Summary is null)) + if (string.IsNullOrEmpty(xamlFilePath)) { - sb.AppendLine(string.Join("\n", localizableString.Summary.Trim().Split('\n').Select(v => $"/// {v}"))); + sb.AppendLine("/// "); } - - sb.AppendLine("/// "); - var value = localizableString.Value; - foreach (var p in localizableString.Params) + else { - value = value.Replace($"{{{p.Index}}}", $"{{{p.Name}}}"); + sb.AppendLine("/// ") + .AppendLine($"/// From: {xamlFilePath}") + .AppendLine("/// "); } - sb.AppendLine(string.Join("\n", value.Split('\n').Select(v => $"/// {v}"))); - sb.AppendLine("/// "); - sb.AppendLine("/// "); } - private static void GenerateMethod(StringBuilder sb, LocalizableString localizableString, string contextPropertyName) + private static void GenerateDocComments(StringBuilder sb, LocalizableString ls, string tabString) { - sb.Append($"public static string {localizableString.Key}("); - var declarationArgs = new List(); - var callArgs = new List(); - for (var i = 0; i < 10; i++) + if (ls.Summary != null) { - if (localizableString.Value.Contains($"{{{i}}}")) - { - var param = localizableString.Params.FirstOrDefault(v => v.Index == i); - if (!(param is null)) - { - declarationArgs.Add($"{param.Type} {param.Name}"); - callArgs.Add(param.Name); - } - else - { - declarationArgs.Add($"object? arg{i}"); - callArgs.Add($"arg{i}"); - } - } - else + sb.AppendLine($"{tabString}/// "); + foreach (var line in ls.Summary.Split('\n')) { - break; + sb.AppendLine($"{tabString}/// {line.Trim()}"); } + sb.AppendLine($"{tabString}/// "); } - string callArray; - switch (callArgs.Count) + sb.AppendLine($"{tabString}/// "); + foreach (var line in ls.Value.Split('\n')) { - case 0: - callArray = ""; - break; - case 1: - callArray = callArgs[0]; - break; - default: - callArray = $"new object?[] {{ {string.Join(", ", callArgs)} }}"; - break; + sb.AppendLine($"{tabString}/// {line.Trim()}"); } + sb.AppendLine($"{tabString}/// "); + } - sb.Append(string.Join(", ", declarationArgs)); + private static void GenerateLocalizationMethod( + StringBuilder sb, + LocalizableString ls, + bool isCoreAssembly, + PluginClassInfo pluginInfo, + string tabString) + { + sb.Append($"{tabString}public static string {ls.Key}("); + var parameters = BuildParameters(ls); + sb.Append(string.Join(", ", parameters.Select(p => $"{p.Type} {p.Name}"))); sb.Append(") => "); - if (contextPropertyName is null) + + var formatArgs = parameters.Count > 0 + ? $", {string.Join(", ", parameters.Select(p => p.Name))}" + : string.Empty; + + if (isCoreAssembly) { - if (string.IsNullOrEmpty(callArray)) - { - sb.AppendLine($"InternationalizationManager.Instance.GetTranslation(\"{localizableString.Key}\");"); - } - else - { - sb.AppendLine( - $"string.Format(InternationalizationManager.Instance.GetTranslation(\"{localizableString.Key}\"), {callArray});" - ); - } + sb.AppendLine(parameters.Count > 0 + ? $"string.Format(InternationalizationManager.Instance.GetTranslation(\"{ls.Key}\"){formatArgs});" + : $"InternationalizationManager.Instance.GetTranslation(\"{ls.Key}\");"); + } + else if (pluginInfo?.IsValid == true) + { + sb.AppendLine(parameters.Count > 0 + ? $"string.Format({pluginInfo.ContextAccessor}.API.GetTranslation(\"{ls.Key}\"){formatArgs});" + : $"{pluginInfo.ContextAccessor}.API.GetTranslation(\"{ls.Key}\");"); } else { - if (string.IsNullOrEmpty(callArray)) - { - sb.AppendLine($"{contextPropertyName}.API.GetTranslation(\"{localizableString.Key}\");"); - } - else - { - sb.AppendLine($"string.Format({contextPropertyName}.API.GetTranslation(\"{localizableString.Key}\"), {callArray});"); - } + sb.AppendLine("\"LOCALIZATION_ERROR\";"); } sb.AppendLine(); } - private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode classDeclaration) - { - return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan); - } - - private static IEnumerable<(SyntaxTree, ClassDeclarationSyntax)> GetClasses(GeneratorExecutionContext context) + private static List BuildParameters(LocalizableString ls) { - foreach (var syntaxTree in context.Compilation.SyntaxTrees) + var parameters = new List(); + for (var i = 0; i < 10; i++) { - var classDeclarations = syntaxTree.GetRoot().DescendantNodes().OfType(); - foreach (var classDeclaration in classDeclarations) + if (!ls.Value.Contains($"{{{i}}}")) { - yield return (syntaxTree, classDeclaration); + continue; } + + var param = ls.Params.FirstOrDefault(p => p.Index == i); + parameters.Add(param is null + ? new MethodParameter($"arg{i}", "object?") + : new MethodParameter(param.Name, param.Type)); } + return parameters; } - private static bool DoesClassImplementInterface(ClassDeclarationSyntax classDeclaration, string interfaceName) + private static string Spacing(int n) { - return classDeclaration.BaseList?.Types.Any(v => interfaceName == v.ToString()) is true; + Span spaces = stackalloc char[n * 4]; + spaces.Fill(' '); + + var sb = new StringBuilder(n * 4); + foreach (var c in spaces) + { + _ = sb.Append(c); + } + + return sb.ToString(); } - private static PropertyDeclarationSyntax GetPluginContextProperty(ClassDeclarationSyntax classDeclaration) + #endregion + + #region Classes + + public class MethodParameter { - return classDeclaration.Members - .OfType() - .FirstOrDefault(v => v.Type.ToString() is PluginContextTypeName); + public string Name { get; } + public string Type { get; } + + public MethodParameter(string name, string type) + { + Name = name; + Type = type; + } } - private static Modifiers GetPropertyModifiers(PropertyDeclarationSyntax property) + public class LocalizableStringParam { - var isStatic = property.Modifiers.Any(v => v.Text is KeywordStatic); - var isPrivate = property.Modifiers.Any(v => v.Text is KeywordPrivate); - var isProtected = property.Modifiers.Any(v => v.Text is KeywordProtected); + public int Index { get; } + public string Name { get; } + public string Type { get; } - return new Modifiers(isStatic, isPrivate, isProtected); + public LocalizableStringParam(int index, string name, string type) + { + Index = index; + Name = name; + Type = type; + } } - private class Modifiers + public class LocalizableString { - public bool Static { get; } - public bool Private { get; } - public bool Protected { get; } + public string Key { get; } + public string Value { get; } + public string Summary { get; } + public IEnumerable Params { get; } - public Modifiers(bool isStatic = false, bool isPrivate = false, bool isProtected = false) + public LocalizableString(string key, string value, string summary, IEnumerable @params) { - Static = isStatic; - Private = isPrivate; - Protected = isProtected; + Key = key; + Value = value; + Summary = summary; + Params = @params; } } - } - public class LocalizableStringParam - { - public int Index { get; } - public string Name { get; } - public string Type { get; } - - public LocalizableStringParam(int index, string name, string type) + public class PluginClassInfo { - Index = index; - Name = name; - Type = type; - } - } + public Location Location { get; } + public string ClassName { get; } + public string PropertyName { get; } + public bool IsStatic { get; } + public bool IsPrivate { get; } + public bool IsProtected { get; } - public class LocalizableString - { - public string Key { get; } - public string Value { get; } - public string Summary { get; } - public IEnumerable Params { get; } + public string ContextAccessor => $"{ClassName}.{PropertyName}"; + public bool IsValid => PropertyName != null && IsStatic && (!IsPrivate) && (!IsProtected); - public LocalizableString(string key, string value, string summary, IEnumerable @params) - { - Key = key; - Value = value; - Summary = summary; - Params = @params; + public PluginClassInfo(Location location, string className, string propertyName, bool isStatic, bool isPrivate, bool isProtected) + { + Location = location; + ClassName = className; + PropertyName = propertyName; + IsStatic = isStatic; + IsPrivate = isPrivate; + IsProtected = isProtected; + } } + + #endregion } }