From 7e5e177d27c9ebcb65c4a0679ea6c09e71959bc9 Mon Sep 17 00:00:00 2001
From: Jack251970 <1160210343@qq.com>
Date: Wed, 5 Mar 2025 17:26:35 +0800
Subject: [PATCH 1/8] Rewrite to IIncrementalGenerator and update NuGet package
---
...low.Launcher.Localization.Analyzers.csproj | 6 +-
...ncher.Localization.SourceGenerators.csproj | 4 +-
.../Localize/LocalizeSourceGenerator.cs | 642 +++++++-----------
.../Flow.Launcher.Localization.csproj | 2 +-
4 files changed, 262 insertions(+), 392 deletions(-)
diff --git a/Flow.Launcher.Localization.Analyzers/Flow.Launcher.Localization.Analyzers.csproj b/Flow.Launcher.Localization.Analyzers/Flow.Launcher.Localization.Analyzers.csproj
index b22e7a5..bda4564 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 784d8fc..5b0738a 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..dc597c6 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;
@@ -12,487 +14,355 @@
namespace Flow.Launcher.Localization.SourceGenerators.Localize
{
[Generator]
- public partial class LocalizeSourceGenerator : ISourceGenerator
+ public partial class LocalizeSourceGenerator : IIncrementalGenerator
{
- private OptimizationLevel _optimizationLevel;
-
private const string CoreNamespace1 = "Flow.Launcher";
private const string CoreNamespace2 = "Flow.Launcher.Core";
private const string DefaultNamespace = "Flow.Launcher";
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 XamlPrefix = "system";
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);
+ private static readonly Regex s_languagesXamlRegex = new Regex(@"\\Languages\\[^\\]+\.xaml$", RegexOptions.IgnoreCase);
- public void Initialize(GeneratorInitializationContext context)
+ public void Initialize(IncrementalGeneratorInitializationContext context)
{
- }
-
- public void Execute(GeneratorExecutionContext context)
- {
- _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 = context.AdditionalTextsProvider
+ .Where(file => s_languagesXamlRegex.IsMatch(file.Path));
+
+ var localizedStrings = xamlFiles
+ .Select((file, ct) => ParseXamlFile(file, ct))
+ .Collect()
+ .SelectMany((files, _) => files);
+
+ var invocationKeys = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ predicate: (n, _) => n is InvocationExpressionSyntax,
+ transform: GetLocalizationKeyFromInvocation)
+ .Where(key => !string.IsNullOrEmpty(key))
+ .Collect()
+ .Select((keys, _) => keys.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);
+
+ context.RegisterSourceOutput(combined, (spc, data) =>
{
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries,
- Location.None
- ));
- return;
- }
+ var (Left, Right) = data;
+ var localizedStringsList = Left.Left.Left;
+ var usedKeys = Left.Left.Right;
+ var pluginClassesList = Left.Right;
+ var compilationData = Right;
- if (string.IsNullOrEmpty(langFilePathEndsWithStr))
- {
- 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;
- }
- }
- }
- else
- {
- 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;
- }
- }
-
- var ns = context.Compilation.AssemblyName ?? DefaultNamespace;
-
- var localizedStrings = LoadLocalizedStrings(resourceDictionaries);
+ var assemblyName = compilationData.AssemblyName ?? DefaultNamespace;
+ var optimizationLevel = compilationData.Options.OptimizationLevel;
- var unusedLocalizationKeys = localizedStrings.Keys.Except(allLanguageKeys).ToArray();
+ var unusedKeys = localizedStringsList
+ .Select(ls => ls.Key)
+ .ToImmutableHashSet()
+ .Except(usedKeys);
- foreach (var key in unusedLocalizationKeys)
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.LocalizationKeyUnused,
- Location.None,
- key
- ));
-
- var sourceCode = GenerateSourceCode(localizedStrings, context, unusedLocalizationKeys);
+ foreach (var key in unusedKeys)
+ {
+ spc.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.LocalizationKeyUnused,
+ Location.None,
+ key));
+ }
- context.AddSource($"{ClassName}.{ns}.g.cs", SourceText.From(sourceCode, Encoding.UTF8));
+ var pluginInfo = GetValidPluginInfo(pluginClassesList, spc);
+ var isCoreAssembly = assemblyName == CoreNamespace1 || assemblyName == CoreNamespace2;
+
+ GenerateSource(
+ spc,
+ localizedStringsList,
+ unusedKeys,
+ optimizationLevel,
+ assemblyName,
+ isCoreAssembly,
+ pluginInfo);
+ });
}
- private static Dictionary LoadLocalizedStrings(AdditionalText[] files)
+ private static void GenerateSource(
+ SourceProductionContext context,
+ ImmutableArray localizedStrings,
+ IEnumerable unusedKeys,
+ OptimizationLevel optimizationLevel,
+ string assemblyName,
+ bool isCoreAssembly,
+ PluginClassInfo pluginInfo)
{
- var result = new Dictionary();
+ var sourceBuilder = new StringBuilder();
+ sourceBuilder.AppendLine("// ");
+ sourceBuilder.AppendLine("#nullable enable");
- foreach (var file in files)
+ if (isCoreAssembly)
{
- ProcessXamlFile(file, result);
+ sourceBuilder.AppendLine("using Flow.Launcher.Core.Resource;");
}
- return result;
- }
+ sourceBuilder.AppendLine($"namespace {assemblyName};");
+ sourceBuilder.AppendLine();
+ sourceBuilder.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{nameof(LocalizeSourceGenerator)}\", \"1.0.0\")]");
+ sourceBuilder.AppendLine($"public static class {ClassName}");
+ sourceBuilder.AppendLine("{");
- 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))
+ foreach (var ls in localizedStrings)
{
- var name = element.FirstAttribute?.Value;
- var value = element.Value;
-
- if (name is null) continue;
+ if (optimizationLevel == OptimizationLevel.Release && unusedKeys.Contains(ls.Key))
+ continue;
- string summary = null;
- var paramsList = new List();
- var commentNode = element.PreviousNode;
+ GenerateDocComments(sourceBuilder, ls);
+ GenerateLocalizationMethod(sourceBuilder, ls, isCoreAssembly, pluginInfo);
+ }
- if (commentNode is XComment comment)
- summary = ProcessXamlFileComment(comment, paramsList);
+ sourceBuilder.AppendLine("}");
- result[name] = new LocalizableString(name, value, summary, paramsList);
- }
+ context.AddSource($"{ClassName}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
- private static string ProcessXamlFileComment(XComment comment, List paramsList) {
- string summary = null;
- try
+ private static void GenerateLocalizationMethod(
+ StringBuilder sb,
+ LocalizableString ls,
+ bool isCoreAssembly,
+ PluginClassInfo pluginInfo)
+ {
+ sb.Append($"public static string {ls.Key}(");
+ var parameters = BuildParameters(ls);
+ sb.Append(string.Join(", ", parameters.Select(p => $"{p.Type} {p.Name}")));
+ sb.Append(") => ");
+
+ var formatArgs = parameters.Count > 0
+ ? $", {string.Join(", ", parameters.Select(p => p.Name))}"
+ : string.Empty;
+
+ if (isCoreAssembly)
{
- 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));
- }
- }
+ 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}\");");
}
- catch (Exception ex)
+ else
{
- // ignore
- Console.WriteLine($"Exception in ProcessXamlFileComment: {ex.Message}");
+ sb.AppendLine("\"LOCALIZATION_ERROR\";");
}
- return summary;
+ sb.AppendLine();
}
- private static string ExtractDocumentationCommentSummary(XDocument commentDoc) {
- return commentDoc.Descendants("summary").FirstOrDefault()?.Value.Trim();
+ private static ImmutableArray ParseXamlFile(AdditionalText file, CancellationToken ct)
+ {
+ var content = file.GetText(ct)?.ToString();
+ if (content is null) return ImmutableArray.Empty;
+
+ var doc = XDocument.Parse(content);
+ var ns = doc.Root?.GetNamespaceOfPrefix(XamlPrefix);
+ if (ns is null) return ImmutableArray.Empty;
+
+ return doc.Descendants(ns + XamlTag)
+ .Select(element =>
+ {
+ var key = element.Attribute("Key")?.Value;
+ var value = element.Value;
+ var comment = element.PreviousNode as XComment;
+
+ return key is null ? null : ParseLocalizableString(key, value, comment);
+ })
+ .Where(ls => ls != null)
+ .ToImmutableArray();
}
- private static bool CommentIncludesDocumentationMarkup(XComment comment) {
- return comment.Value.Contains("") || comment.Value.Contains(" localizedStrings,
- GeneratorExecutionContext context,
- string[] unusedLocalizationKeys
- )
+ private static (string Summary, ImmutableArray Parameters) ParseComment(XComment comment)
{
- var ns = context.Compilation.AssemblyName;
+ if (comment == null || comment.Value == null)
+ return (null, ImmutableArray.Empty);
- var sb = new StringBuilder();
- if (ns is CoreNamespace1 || ns is CoreNamespace2)
+ try
{
- GenerateFileHeader(sb, context);
- GenerateClass(sb, localizedStrings, unusedLocalizationKeys);
- return sb.ToString();
+ 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);
}
-
- string contextPropertyName = null;
- var mainClassFound = false;
- foreach (var (syntaxTree, classDeclaration) in GetClasses(context))
+ catch
{
- if (!DoesClassImplementInterface(classDeclaration, PluginInterfaceName))
- continue;
-
- mainClassFound = true;
-
- var property = GetPluginContextProperty(classDeclaration);
- if (property is null)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.CouldNotFindContextProperty,
- GetLocation(syntaxTree, classDeclaration),
- classDeclaration.Identifier
- ));
- return string.Empty;
- }
-
- var propertyModifiers = GetPropertyModifiers(property);
-
- if (!propertyModifiers.Static)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.ContextPropertyNotStatic,
- GetLocation(syntaxTree, property),
- property.Identifier
- ));
- return string.Empty;
- }
-
- if (propertyModifiers.Private)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.ContextPropertyIsPrivate,
- GetLocation(syntaxTree, property),
- property.Identifier
- ));
- return string.Empty;
- }
-
- if (propertyModifiers.Protected)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.ContextPropertyIsProtected,
- GetLocation(syntaxTree, property),
- property.Identifier
- ));
- return string.Empty;
- }
-
- contextPropertyName = $"{classDeclaration.Identifier}.{property.Identifier}";
- break;
+ return (null, ImmutableArray.Empty);
}
+ }
- if (mainClassFound is false)
+ private static string GetLocalizationKeyFromInvocation(GeneratorSyntaxContext context, CancellationToken ct)
+ {
+ var invocation = (InvocationExpressionSyntax)context.Node;
+ if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
{
- context.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.CouldNotFindPluginEntryClass,
- Location.None
- ));
- return string.Empty;
+ if (memberAccess.Expression is IdentifierNameSyntax identifierName && identifierName.Identifier.Text == ClassName)
+ {
+ return memberAccess.Name.Identifier.Text;
+ }
}
-
- GenerateFileHeader(sb, context, true);
- GenerateClass(sb, localizedStrings, unusedLocalizationKeys, contextPropertyName);
- return sb.ToString();
+ return null;
}
- private static void GenerateFileHeader(StringBuilder sb, GeneratorExecutionContext context, bool isPlugin = false)
+ private static PluginClassInfo GetPluginClassInfo(GeneratorSyntaxContext context, CancellationToken ct)
{
- var rootNamespace = context.Compilation.AssemblyName;
- sb.AppendLine("// ");
- sb.AppendLine("#nullable enable");
+ var classDecl = (ClassDeclarationSyntax)context.Node;
+ if (!classDecl.BaseList?.Types.Any(t => t.Type.ToString() == PluginInterfaceName) ?? true)
+ return null;
- if (!isPlugin)
- sb.AppendLine("using Flow.Launcher.Core.Resource;");
-
- sb.AppendLine($"namespace {rootNamespace};");
- }
+ var property = classDecl.Members
+ .OfType()
+ .FirstOrDefault(p => p.Type.ToString() == PluginContextTypeName);
- 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)
- {
- if (_optimizationLevel == OptimizationLevel.Release && unusedLocalizationKeys.Contains(localizedString.Key))
- continue;
+ if (property is null)
+ return new PluginClassInfo(classDecl.Identifier.Text, null, false);
- GenerateDocCommentForMethod(sb, localizedString.Value);
- GenerateMethod(sb, localizedString.Value, propertyName);
- }
+ var modifiers = property.Modifiers;
+ var isValid = modifiers.Any(SyntaxKind.StaticKeyword) &&
+ !modifiers.Any(SyntaxKind.PrivateKeyword) &&
+ !modifiers.Any(SyntaxKind.ProtectedKeyword);
- sb.AppendLine("}");
+ return new PluginClassInfo(
+ classDecl.Identifier.Text,
+ property.Identifier.Text,
+ isValid);
}
- private static void GenerateDocCommentForMethod(StringBuilder sb, LocalizableString localizableString)
+ private static PluginClassInfo GetValidPluginInfo(
+ ImmutableArray pluginClasses,
+ SourceProductionContext context)
{
- sb.AppendLine("/// ");
- if (!(localizableString.Summary is null))
+ foreach (var pluginClass in pluginClasses)
{
- sb.AppendLine(string.Join("\n", localizableString.Summary.Trim().Split('\n').Select(v => $"/// {v}")));
- }
+ if (pluginClass?.IsValid == true)
+ return pluginClass;
- sb.AppendLine("/// ");
- var value = localizableString.Value;
- foreach (var p in localizableString.Params)
- {
- value = value.Replace($"{{{p.Index}}}", $"{{{p.Name}}}");
+ if (pluginClass.IsValid == false)
+ {
+ // TODO
+ //context.ReportDiagnostic(Diagnostic.Create(
+ // SourceGeneratorDiagnostics.InvalidPluginConfiguration,
+ // Location.None));
+ }
}
- sb.AppendLine(string.Join("\n", value.Split('\n').Select(v => $"/// {v}")));
- sb.AppendLine("///
");
- sb.AppendLine("/// ");
+ return null;
}
- private static void GenerateMethod(StringBuilder sb, LocalizableString localizableString, string contextPropertyName)
+ private static List BuildParameters(LocalizableString ls)
{
- sb.Append($"public static string {localizableString.Key}(");
- var declarationArgs = new List();
- var callArgs = new List();
+ var parameters = new List();
for (var i = 0; i < 10; i++)
{
- 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
- {
- break;
- }
- }
+ if (!ls.Value.Contains($"{{{i}}}")) continue;
- string callArray;
- switch (callArgs.Count)
- {
- case 0:
- callArray = "";
- break;
- case 1:
- callArray = callArgs[0];
- break;
- default:
- callArray = $"new object?[] {{ {string.Join(", ", callArgs)} }}";
- break;
+ 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;
+ }
- sb.Append(string.Join(", ", declarationArgs));
- sb.Append(") => ");
- if (contextPropertyName is null)
- {
- if (string.IsNullOrEmpty(callArray))
- {
- sb.AppendLine($"InternationalizationManager.Instance.GetTranslation(\"{localizableString.Key}\");");
- }
- else
- {
- sb.AppendLine(
- $"string.Format(InternationalizationManager.Instance.GetTranslation(\"{localizableString.Key}\"), {callArray});"
- );
- }
- }
- else
+ private static void GenerateDocComments(StringBuilder sb, LocalizableString ls)
+ {
+ if (ls.Summary != null)
{
- if (string.IsNullOrEmpty(callArray))
- {
- sb.AppendLine($"{contextPropertyName}.API.GetTranslation(\"{localizableString.Key}\");");
- }
- else
- {
- sb.AppendLine($"string.Format({contextPropertyName}.API.GetTranslation(\"{localizableString.Key}\"), {callArray});");
- }
+ sb.AppendLine("/// ");
+ foreach (var line in ls.Summary.Split('\n'))
+ sb.AppendLine($"/// {line.Trim()}");
+ sb.AppendLine("/// ");
}
- sb.AppendLine();
+ sb.AppendLine("/// ");
+ foreach (var line in ls.Value.Split('\n'))
+ sb.AppendLine($"/// {line.Trim()}");
+ sb.AppendLine("///
");
}
- private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode classDeclaration)
+ public class MethodParameter
{
- return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan);
- }
+ public string Name { get; }
+ public string Type { get; }
- private static IEnumerable<(SyntaxTree, ClassDeclarationSyntax)> GetClasses(GeneratorExecutionContext context)
- {
- foreach (var syntaxTree in context.Compilation.SyntaxTrees)
+ public MethodParameter(string name, string type)
{
- var classDeclarations = syntaxTree.GetRoot().DescendantNodes().OfType();
- foreach (var classDeclaration in classDeclarations)
- {
- yield return (syntaxTree, classDeclaration);
- }
+ Name = name;
+ Type = type;
}
}
- private static bool DoesClassImplementInterface(ClassDeclarationSyntax classDeclaration, string interfaceName)
+ public class LocalizableStringParam
{
- return classDeclaration.BaseList?.Types.Any(v => interfaceName == v.ToString()) is true;
- }
+ public int Index { get; }
+ public string Name { get; }
+ public string Type { get; }
- private static PropertyDeclarationSyntax GetPluginContextProperty(ClassDeclarationSyntax classDeclaration)
- {
- return classDeclaration.Members
- .OfType()
- .FirstOrDefault(v => v.Type.ToString() is PluginContextTypeName);
- }
-
- private static Modifiers GetPropertyModifiers(PropertyDeclarationSyntax property)
- {
- 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);
-
- 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 string ClassName { get; }
+ public string PropertyName { get; }
+ public bool IsValid { 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 LocalizableString(string key, string value, string summary, IEnumerable @params)
- {
- Key = key;
- Value = value;
- Summary = summary;
- Params = @params;
+ public PluginClassInfo(string className, string propertyName, bool isValid)
+ {
+ ClassName = className;
+ PropertyName = propertyName;
+ IsValid = isValid;
+ }
}
}
}
diff --git a/Flow.Launcher.Localization/Flow.Launcher.Localization.csproj b/Flow.Launcher.Localization/Flow.Launcher.Localization.csproj
index c4f34bd..2b93ab4 100644
--- a/Flow.Launcher.Localization/Flow.Launcher.Localization.csproj
+++ b/Flow.Launcher.Localization/Flow.Launcher.Localization.csproj
@@ -1,7 +1,7 @@
- 1.0.0
+ 1.0.1
netstandard2.0
true
Flow.Launcher.Localization
From 7f31bed8c7af018149099c3d7e2920e950f94ef8 Mon Sep 17 00:00:00 2001
From: Jack251970 <1160210343@qq.com>
Date: Wed, 5 Mar 2025 19:26:30 +0800
Subject: [PATCH 2/8] Improve code quality
---
.../Localize/LocalizeSourceGenerator.cs | 203 ++++++++++--------
1 file changed, 119 insertions(+), 84 deletions(-)
diff --git a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
index dc597c6..bed1dae 100644
--- a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
+++ b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
@@ -13,24 +13,37 @@
namespace Flow.Launcher.Localization.SourceGenerators.Localize
{
+ ///
+ /// Generates properties for strings based on resource files.
+ ///
[Generator]
public partial class LocalizeSourceGenerator : IIncrementalGenerator
{
+ #region Fields
+
private const string CoreNamespace1 = "Flow.Launcher";
private const string CoreNamespace2 = "Flow.Launcher.Core";
private const string DefaultNamespace = "Flow.Launcher";
private const string ClassName = "Localize";
private const string PluginInterfaceName = "IPluginI18n";
private const string PluginContextTypeName = "PluginInitContext";
- private const string XamlTag = "String";
private const string XamlPrefix = "system";
- private const string DefaultLanguageFilePathEndsWith = @"\Languages\en.xaml";
- private static readonly Regex s_languagesXamlRegex = new Regex(@"\\Languages\\[^\\]+\.xaml$", RegexOptions.IgnoreCase);
+ private const string XamlTag = "String";
+
+ private readonly Regex _languagesXamlRegex = new Regex(@"\\Languages\\[^\\]+\.xaml$", RegexOptions.IgnoreCase);
+
+ #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 => s_languagesXamlRegex.IsMatch(file.Path));
+ .Where(file => _languagesXamlRegex.IsMatch(file.Path));
var localizedStrings = xamlFiles
.Select((file, ct) => ParseXamlFile(file, ct))
@@ -94,78 +107,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
});
}
- private static void GenerateSource(
- SourceProductionContext context,
- ImmutableArray localizedStrings,
- IEnumerable unusedKeys,
- OptimizationLevel optimizationLevel,
- string assemblyName,
- bool isCoreAssembly,
- PluginClassInfo pluginInfo)
- {
- var sourceBuilder = new StringBuilder();
- sourceBuilder.AppendLine("// ");
- sourceBuilder.AppendLine("#nullable enable");
-
- if (isCoreAssembly)
- {
- sourceBuilder.AppendLine("using Flow.Launcher.Core.Resource;");
- }
-
- sourceBuilder.AppendLine($"namespace {assemblyName};");
- sourceBuilder.AppendLine();
- sourceBuilder.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{nameof(LocalizeSourceGenerator)}\", \"1.0.0\")]");
- sourceBuilder.AppendLine($"public static class {ClassName}");
- sourceBuilder.AppendLine("{");
-
- foreach (var ls in localizedStrings)
- {
- if (optimizationLevel == OptimizationLevel.Release && unusedKeys.Contains(ls.Key))
- continue;
-
- GenerateDocComments(sourceBuilder, ls);
- GenerateLocalizationMethod(sourceBuilder, ls, isCoreAssembly, pluginInfo);
- }
-
- sourceBuilder.AppendLine("}");
-
- context.AddSource($"{ClassName}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
- }
-
- private static void GenerateLocalizationMethod(
- StringBuilder sb,
- LocalizableString ls,
- bool isCoreAssembly,
- PluginClassInfo pluginInfo)
- {
- sb.Append($"public static string {ls.Key}(");
- var parameters = BuildParameters(ls);
- sb.Append(string.Join(", ", parameters.Select(p => $"{p.Type} {p.Name}")));
- sb.Append(") => ");
-
- var formatArgs = parameters.Count > 0
- ? $", {string.Join(", ", parameters.Select(p => p.Name))}"
- : string.Empty;
-
- if (isCoreAssembly)
- {
- 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
- {
- sb.AppendLine("\"LOCALIZATION_ERROR\";");
- }
+ #endregion
- sb.AppendLine();
- }
+ #region Parse Xaml File
private static ImmutableArray ParseXamlFile(AdditionalText file, CancellationToken ct)
{
@@ -219,6 +163,10 @@ private static (string Summary, ImmutableArray Parameter
}
}
+ #endregion
+
+ #region Get Localization Keys
+
private static string GetLocalizationKeyFromInvocation(GeneratorSyntaxContext context, CancellationToken ct)
{
var invocation = (InvocationExpressionSyntax)context.Node;
@@ -232,6 +180,10 @@ private static string GetLocalizationKeyFromInvocation(GeneratorSyntaxContext co
return null;
}
+ #endregion
+
+ #region Get Plugin Class Info
+
private static PluginClassInfo GetPluginClassInfo(GeneratorSyntaxContext context, CancellationToken ct)
{
var classDecl = (ClassDeclarationSyntax)context.Node;
@@ -276,19 +228,46 @@ private static PluginClassInfo GetValidPluginInfo(
return null;
}
- private static List BuildParameters(LocalizableString ls)
+ #endregion
+
+ #region Generate Source
+
+ private static void GenerateSource(
+ SourceProductionContext context,
+ ImmutableArray localizedStrings,
+ IEnumerable unusedKeys,
+ OptimizationLevel optimizationLevel,
+ string assemblyName,
+ bool isCoreAssembly,
+ PluginClassInfo pluginInfo)
{
- var parameters = new List();
- for (var i = 0; i < 10; i++)
+ var sourceBuilder = new StringBuilder();
+ sourceBuilder.AppendLine("// ");
+ sourceBuilder.AppendLine("#nullable enable");
+
+ if (isCoreAssembly)
{
- if (!ls.Value.Contains($"{{{i}}}")) continue;
+ sourceBuilder.AppendLine("using Flow.Launcher.Core.Resource;");
+ }
- 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));
+ sourceBuilder.AppendLine($"namespace {assemblyName};");
+ sourceBuilder.AppendLine();
+ sourceBuilder.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{nameof(LocalizeSourceGenerator)}\", \"1.0.0\")]");
+ sourceBuilder.AppendLine($"public static class {ClassName}");
+ sourceBuilder.AppendLine("{");
+
+ foreach (var ls in localizedStrings)
+ {
+ if (optimizationLevel == OptimizationLevel.Release && unusedKeys.Contains(ls.Key))
+ continue;
+
+ GenerateDocComments(sourceBuilder, ls);
+ GenerateLocalizationMethod(sourceBuilder, ls, isCoreAssembly, pluginInfo);
}
- return parameters;
+
+ sourceBuilder.AppendLine("}");
+
+ context.AddSource($"{ClassName}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
private static void GenerateDocComments(StringBuilder sb, LocalizableString ls)
@@ -307,6 +286,60 @@ private static void GenerateDocComments(StringBuilder sb, LocalizableString ls)
sb.AppendLine("/// ");
}
+ private static void GenerateLocalizationMethod(
+ StringBuilder sb,
+ LocalizableString ls,
+ bool isCoreAssembly,
+ PluginClassInfo pluginInfo)
+ {
+ sb.Append($"public static string {ls.Key}(");
+ var parameters = BuildParameters(ls);
+ sb.Append(string.Join(", ", parameters.Select(p => $"{p.Type} {p.Name}")));
+ sb.Append(") => ");
+
+ var formatArgs = parameters.Count > 0
+ ? $", {string.Join(", ", parameters.Select(p => p.Name))}"
+ : string.Empty;
+
+ if (isCoreAssembly)
+ {
+ 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
+ {
+ sb.AppendLine("\"LOCALIZATION_ERROR\";");
+ }
+
+ sb.AppendLine();
+ }
+
+ private static List BuildParameters(LocalizableString ls)
+ {
+ var parameters = new List();
+ for (var i = 0; i < 10; i++)
+ {
+ if (!ls.Value.Contains($"{{{i}}}")) 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;
+ }
+
+ #endregion
+
+ #region Classes
+
public class MethodParameter
{
public string Name { get; }
@@ -364,5 +397,7 @@ public PluginClassInfo(string className, string propertyName, bool isValid)
IsValid = isValid;
}
}
+
+ #endregion
}
}
From 4091e4869457e6677e32ca7bd96e0ed4544845cd Mon Sep 17 00:00:00 2001
From: Jack251970 <1160210343@qq.com>
Date: Wed, 5 Mar 2025 20:26:32 +0800
Subject: [PATCH 3/8] Improve code quality & Add more diagnostic information
---
.../Localize/LocalizeSourceGenerator.cs | 170 +++++++++++++-----
1 file changed, 122 insertions(+), 48 deletions(-)
diff --git a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
index bed1dae..1f3a8c0 100644
--- a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
+++ b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
@@ -67,44 +67,65 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var compilation = context.CompilationProvider;
- var combined = localizedStrings.Combine(invocationKeys).Combine(pluginClasses).Combine(compilation);
+ var combined = localizedStrings.Combine(invocationKeys).Combine(pluginClasses).Combine(compilation).Combine(xamlFiles.Collect());
- context.RegisterSourceOutput(combined, (spc, data) =>
+ context.RegisterSourceOutput(combined, Execute);
+ }
+
+ ///
+ /// 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 Strings),
+ ImmutableArray PluginClassInfos),
+ Compilation Compilation),
+ ImmutableArray AdditionalTexts) data)
+ {
+ var xamlFiles = data.AdditionalTexts;
+ if (xamlFiles.Length == 0)
{
- var (Left, Right) = data;
- var localizedStringsList = Left.Left.Left;
- var usedKeys = Left.Left.Right;
- var pluginClassesList = Left.Right;
- var compilationData = Right;
+ spc.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.CouldNotFindResourceDictionaries,
+ Location.None
+ ));
+ return;
+ }
- var assemblyName = compilationData.AssemblyName ?? DefaultNamespace;
- var optimizationLevel = compilationData.Options.OptimizationLevel;
+ var compilationData = data.Item1.Compilation;
+ var pluginClassesList = data.Item1.Item1.PluginClassInfos;
+ var usedKeys = data.Item1.Item1.Item1.Strings;
+ var localizedStringsList = data.Item1.Item1.Item1.LocalizableStrings;
- var unusedKeys = localizedStringsList
- .Select(ls => ls.Key)
- .ToImmutableHashSet()
- .Except(usedKeys);
+ var assemblyName = compilationData.AssemblyName ?? DefaultNamespace;
+ var optimizationLevel = compilationData.Options.OptimizationLevel;
- foreach (var key in unusedKeys)
- {
- spc.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.LocalizationKeyUnused,
- Location.None,
- key));
- }
+ var unusedKeys = localizedStringsList
+ .Select(ls => ls.Key)
+ .ToImmutableHashSet()
+ .Except(usedKeys);
- var pluginInfo = GetValidPluginInfo(pluginClassesList, spc);
- var isCoreAssembly = assemblyName == CoreNamespace1 || assemblyName == CoreNamespace2;
-
- GenerateSource(
- spc,
- localizedStringsList,
- unusedKeys,
- optimizationLevel,
- assemblyName,
- isCoreAssembly,
- pluginInfo);
- });
+ foreach (var key in unusedKeys)
+ {
+ spc.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.LocalizationKeyUnused,
+ Location.None,
+ key));
+ }
+
+ var pluginInfo = GetValidPluginInfo(pluginClassesList, spc);
+ var isCoreAssembly = assemblyName == CoreNamespace1 || assemblyName == CoreNamespace2;
+
+ GenerateSource(
+ spc,
+ localizedStringsList,
+ unusedKeys,
+ optimizationLevel,
+ assemblyName,
+ isCoreAssembly,
+ pluginInfo);
}
#endregion
@@ -187,47 +208,93 @@ private static string GetLocalizationKeyFromInvocation(GeneratorSyntaxContext co
private static PluginClassInfo GetPluginClassInfo(GeneratorSyntaxContext context, CancellationToken ct)
{
var classDecl = (ClassDeclarationSyntax)context.Node;
+ var location = GetLocation(context.SemanticModel.SyntaxTree, classDecl);
if (!classDecl.BaseList?.Types.Any(t => t.Type.ToString() == PluginInterfaceName) ?? true)
+ {
+ // Cannot find class that implements IPluginI18n
return null;
+ }
var property = classDecl.Members
.OfType()
.FirstOrDefault(p => p.Type.ToString() == PluginContextTypeName);
-
if (property is null)
- return new PluginClassInfo(classDecl.Identifier.Text, null, false);
+ {
+ // Cannot find context
+ return new PluginClassInfo(location, classDecl.Identifier.Text, null, false, false, false);
+ }
var modifiers = property.Modifiers;
- var isValid = modifiers.Any(SyntaxKind.StaticKeyword) &&
- !modifiers.Any(SyntaxKind.PrivateKeyword) &&
- !modifiers.Any(SyntaxKind.ProtectedKeyword);
-
return new PluginClassInfo(
+ location,
classDecl.Identifier.Text,
property.Identifier.Text,
- isValid);
+ modifiers.Any(SyntaxKind.StaticKeyword),
+ modifiers.Any(SyntaxKind.PrivateKeyword),
+ modifiers.Any(SyntaxKind.ProtectedKeyword));
}
private static PluginClassInfo GetValidPluginInfo(
ImmutableArray pluginClasses,
SourceProductionContext context)
{
+ if (pluginClasses.All(p => p is null || p.PropertyName == null))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.CouldNotFindPluginEntryClass,
+ Location.None
+ ));
+ return null;
+ }
+
foreach (var pluginClass in pluginClasses)
{
- if (pluginClass?.IsValid == true)
+ if (pluginClass == null || pluginClass.PropertyName is null)
+ {
+ continue;
+ }
+
+ if (pluginClass.IsValid == true)
+ {
return pluginClass;
+ }
+
+ if (!pluginClass.IsStatic)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.ContextPropertyNotStatic,
+ pluginClass.Location,
+ pluginClass.PropertyName
+ ));
+ }
+
+ if (pluginClass.IsPrivate)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.ContextPropertyIsPrivate,
+ pluginClass.Location,
+ pluginClass.PropertyName
+ ));
+ }
- if (pluginClass.IsValid == false)
+ if (pluginClass.IsProtected)
{
- // TODO
- //context.ReportDiagnostic(Diagnostic.Create(
- // SourceGeneratorDiagnostics.InvalidPluginConfiguration,
- // Location.None));
+ context.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.ContextPropertyIsProtected,
+ pluginClass.Location,
+ pluginClass.PropertyName
+ ));
}
}
+
return null;
}
+ private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode classDeclaration)
+ {
+ return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan);
+ }
+
#endregion
#region Generate Source
@@ -384,17 +451,24 @@ public LocalizableString(string key, string value, string summary, IEnumerable $"{ClassName}.{PropertyName}";
+ public bool IsValid => PropertyName != null && IsStatic && (!IsPrivate) && (!IsProtected);
- public PluginClassInfo(string className, string propertyName, bool isValid)
+ public PluginClassInfo(Location location, string className, string propertyName, bool isStatic, bool isPrivate, bool isProtected)
{
+ Location = location;
ClassName = className;
PropertyName = propertyName;
- IsValid = isValid;
+ IsStatic = isStatic;
+ IsPrivate = isPrivate;
+ IsProtected = isProtected;
}
}
From f20d040bef0a7fe5dd13383651c6eaef01665506 Mon Sep 17 00:00:00 2001
From: Jack251970 <1160210343@qq.com>
Date: Wed, 5 Mar 2025 20:39:05 +0800
Subject: [PATCH 4/8] Fix version & Change file name
---
.../Localize/LocalizeSourceGenerator.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
index 1f3a8c0..a033803 100644
--- a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
+++ b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
@@ -319,7 +319,7 @@ private static void GenerateSource(
sourceBuilder.AppendLine($"namespace {assemblyName};");
sourceBuilder.AppendLine();
- sourceBuilder.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{nameof(LocalizeSourceGenerator)}\", \"1.0.0\")]");
+ sourceBuilder.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{nameof(LocalizeSourceGenerator)}\", \"0.0.1\")]");
sourceBuilder.AppendLine($"public static class {ClassName}");
sourceBuilder.AppendLine("{");
@@ -334,7 +334,7 @@ private static void GenerateSource(
sourceBuilder.AppendLine("}");
- context.AddSource($"{ClassName}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
+ context.AddSource($"{ClassName}.{assemblyName}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
private static void GenerateDocComments(StringBuilder sb, LocalizableString ls)
From f3ab347414672c898bfbb8729349d8f51874e4c1 Mon Sep 17 00:00:00 2001
From: Jack251970 <1160210343@qq.com>
Date: Wed, 5 Mar 2025 22:35:39 +0800
Subject: [PATCH 5/8] Improve code quality & Fix parse xaml file issue & Add
tab in output codes & Disable unused key feature
---
.../Localize/LocalizeSourceGenerator.cs | 265 +++++++++++++-----
1 file changed, 199 insertions(+), 66 deletions(-)
diff --git a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
index a033803..9f28506 100644
--- a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
+++ b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
@@ -32,6 +32,8 @@ public partial class LocalizeSourceGenerator : IIncrementalGenerator
private readonly Regex _languagesXamlRegex = new Regex(@"\\Languages\\[^\\]+\.xaml$", RegexOptions.IgnoreCase);
+ private static readonly Version PackageVersion = typeof(LocalizeSourceGenerator).Assembly.GetName().Version;
+
#endregion
#region Incremental Generator
@@ -50,13 +52,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.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.ToImmutableHashSet());
+ .Select((keys, _) => keys.Distinct().ToImmutableHashSet());
var pluginClasses = context.SyntaxProvider
.CreateSyntaxProvider(
@@ -79,7 +82,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
/// The provided data.
private void Execute(SourceProductionContext spc,
((((ImmutableArray LocalizableStrings,
- ImmutableHashSet Strings),
+ ImmutableHashSet InvocationKeys),
ImmutableArray PluginClassInfos),
Compilation Compilation),
ImmutableArray AdditionalTexts) data)
@@ -94,38 +97,26 @@ private void Execute(SourceProductionContext spc,
return;
}
- var compilationData = data.Item1.Compilation;
- var pluginClassesList = data.Item1.Item1.PluginClassInfos;
- var usedKeys = data.Item1.Item1.Item1.Strings;
- var localizedStringsList = data.Item1.Item1.Item1.LocalizableStrings;
-
- var assemblyName = compilationData.AssemblyName ?? DefaultNamespace;
- var optimizationLevel = compilationData.Options.OptimizationLevel;
-
- var unusedKeys = localizedStringsList
- .Select(ls => ls.Key)
- .ToImmutableHashSet()
- .Except(usedKeys);
+ 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;
- foreach (var key in unusedKeys)
- {
- spc.ReportDiagnostic(Diagnostic.Create(
- SourceGeneratorDiagnostics.LocalizationKeyUnused,
- Location.None,
- key));
- }
+ var assemblyName = compilation.AssemblyName ?? DefaultNamespace;
+ var optimizationLevel = compilation.Options.OptimizationLevel;
- var pluginInfo = GetValidPluginInfo(pluginClassesList, spc);
+ var pluginInfo = GetValidPluginInfo(pluginClasses, spc);
var isCoreAssembly = assemblyName == CoreNamespace1 || assemblyName == CoreNamespace2;
GenerateSource(
spc,
- localizedStringsList,
- unusedKeys,
+ xamlFiles[0],
+ localizedStrings,
optimizationLevel,
assemblyName,
isCoreAssembly,
- pluginInfo);
+ pluginInfo,
+ usedKeys);
}
#endregion
@@ -135,23 +126,38 @@ private void Execute(SourceProductionContext spc,
private static ImmutableArray ParseXamlFile(AdditionalText file, CancellationToken ct)
{
var content = file.GetText(ct)?.ToString();
- if (content is null) return ImmutableArray.Empty;
+ if (content is null)
+ {
+ return ImmutableArray.Empty;
+ }
var doc = XDocument.Parse(content);
- var ns = doc.Root?.GetNamespaceOfPrefix(XamlPrefix);
- if (ns is null) return ImmutableArray.Empty;
+ var systemNs = doc.Root?.GetNamespaceOfPrefix(XamlPrefix); // Should be "system"
+ var xNs = doc.Root?.GetNamespaceOfPrefix("x");
+ if (systemNs is null || xNs is null)
+ {
+ return ImmutableArray.Empty;
+ }
- return doc.Descendants(ns + XamlTag)
- .Select(element =>
+ var localizableStrings = new List();
+ foreach (var element in doc.Descendants(systemNs + XamlTag)) // "String" elements in system namespace
+ {
+ if (ct.IsCancellationRequested)
{
- var key = element.Attribute("Key")?.Value;
- var value = element.Value;
- var comment = element.PreviousNode as XComment;
-
- return key is null ? null : ParseLocalizableString(key, value, comment);
- })
- .Where(ls => ls != null)
- .ToImmutableArray();
+ return ImmutableArray.Empty;
+ }
+
+ var key = element.Attribute(xNs + "Key")?.Value; // Correctly get x:Key
+ var value = element.Value;
+ var comment = element.PreviousNode as XComment;
+
+ if (key != null)
+ {
+ localizableStrings.Add(ParseLocalizableString(key, value, comment));
+ }
+ }
+
+ return localizableStrings.ToImmutableArray();
}
private static LocalizableString ParseLocalizableString(string key, string value, XComment comment)
@@ -163,7 +169,9 @@ private static LocalizableString ParseLocalizableString(string key, string value
private static (string Summary, ImmutableArray Parameters) ParseComment(XComment comment)
{
if (comment == null || comment.Value == null)
+ {
return (null, ImmutableArray.Empty);
+ }
try
{
@@ -175,7 +183,6 @@ private static (string Summary, ImmutableArray Parameter
p.Attribute("name").Value,
p.Attribute("type").Value))
.ToImmutableArray();
-
return (summary, parameters);
}
catch
@@ -186,19 +193,47 @@ private static (string Summary, ImmutableArray Parameter
#endregion
- #region Get Localization Keys
+ #region Get Used Localization Keys
+ // TODO: Add support for usedKeys
private static string GetLocalizationKeyFromInvocation(GeneratorSyntaxContext context, CancellationToken ct)
{
+ if (ct.IsCancellationRequested)
+ {
+ return null;
+ }
+
var invocation = (InvocationExpressionSyntax)context.Node;
- if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
+ var expression = invocation.Expression;
+ var parts = new List();
+
+ // Traverse the member access hierarchy
+ while (expression is MemberAccessExpressionSyntax memberAccess)
{
- if (memberAccess.Expression is IdentifierNameSyntax identifierName && identifierName.Identifier.Text == ClassName)
- {
- return memberAccess.Name.Identifier.Text;
- }
+ parts.Add(memberAccess.Name.Identifier.Text);
+ expression = memberAccess.Expression;
}
- return null;
+
+ // Add the leftmost identifier
+ if (expression is IdentifierNameSyntax identifier)
+ {
+ parts.Add(identifier.Identifier.Text);
+ }
+ else
+ {
+ return null;
+ }
+
+ // 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)
+ {
+ return null;
+ }
+
+ return parts[1];
}
#endregion
@@ -300,66 +335,147 @@ private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode clas
#region Generate Source
private static void GenerateSource(
- SourceProductionContext context,
+ SourceProductionContext spc,
+ AdditionalText xamlFile,
ImmutableArray localizedStrings,
- IEnumerable unusedKeys,
OptimizationLevel optimizationLevel,
string assemblyName,
bool isCoreAssembly,
- PluginClassInfo pluginInfo)
+ 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);
+
+ foreach (var key in unusedKeys)
+ {
+ spc.ReportDiagnostic(Diagnostic.Create(
+ SourceGeneratorDiagnostics.LocalizationKeyUnused,
+ Location.None,
+ key));
+ }
+ }
+
var sourceBuilder = new StringBuilder();
- sourceBuilder.AppendLine("// ");
- sourceBuilder.AppendLine("#nullable enable");
+ // Generate header
+ GeneratedHeaderFromPath(sourceBuilder, xamlFile.Path);
+ sourceBuilder.AppendLine();
+
+ // Generate usings
if (isCoreAssembly)
{
sourceBuilder.AppendLine("using Flow.Launcher.Core.Resource;");
+ sourceBuilder.AppendLine();
}
+ // Generate nullable enable
+ sourceBuilder.AppendLine("#nullable enable");
+ sourceBuilder.AppendLine();
+
+ // Generate namespace
sourceBuilder.AppendLine($"namespace {assemblyName};");
sourceBuilder.AppendLine();
- sourceBuilder.AppendLine($"[System.CodeDom.Compiler.GeneratedCode(\"{nameof(LocalizeSourceGenerator)}\", \"0.0.1\")]");
+
+ // 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();
+
+ // Generate all unused keys
+ sourceBuilder.AppendLine("unusedKeys");
+ foreach (var key in unusedKeys)
+ {
+ sourceBuilder.AppendLine($"{key}");
+ }
+ sourceBuilder.AppendLine();
+
+ // Generate all used keys
+ sourceBuilder.AppendLine("usedKeys");
+ foreach (var key in usedKeys)
+ {
+ 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)
{
- if (optimizationLevel == OptimizationLevel.Release && unusedKeys.Contains(ls.Key))
+ // TODO: Add support for usedKeys
+ /*if (unusedKeys.Contains(ls.Key))
+ {
continue;
+ }*/
- GenerateDocComments(sourceBuilder, ls);
- GenerateLocalizationMethod(sourceBuilder, ls, isCoreAssembly, pluginInfo);
+ GenerateDocComments(sourceBuilder, ls, tabString);
+ GenerateLocalizationMethod(sourceBuilder, ls, isCoreAssembly, pluginInfo, tabString);
}
sourceBuilder.AppendLine("}");
- context.AddSource($"{ClassName}.{assemblyName}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
+ // Add source to context
+ spc.AddSource($"{ClassName}.{assemblyName}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
- private static void GenerateDocComments(StringBuilder sb, LocalizableString ls)
+ private static void GeneratedHeaderFromPath(StringBuilder sb, string xamlFilePath)
+ {
+ if (string.IsNullOrEmpty(xamlFilePath))
+ {
+ sb.AppendLine("/// ");
+ }
+ else
+ {
+ sb.AppendLine("/// ")
+ .AppendLine($"/// From: {xamlFilePath}")
+ .AppendLine("/// ");
+ }
+ }
+
+ private static void GenerateDocComments(StringBuilder sb, LocalizableString ls, string tabString)
{
if (ls.Summary != null)
{
- sb.AppendLine("/// ");
+ sb.AppendLine($"{tabString}/// ");
foreach (var line in ls.Summary.Split('\n'))
- sb.AppendLine($"/// {line.Trim()}");
- sb.AppendLine("/// ");
+ {
+ sb.AppendLine($"{tabString}/// {line.Trim()}");
+ }
+ sb.AppendLine($"{tabString}/// ");
}
- sb.AppendLine("/// ");
+ sb.AppendLine($"{tabString}/// ");
foreach (var line in ls.Value.Split('\n'))
- sb.AppendLine($"/// {line.Trim()}");
- sb.AppendLine("///
");
+ {
+ sb.AppendLine($"{tabString}/// {line.Trim()}");
+ }
+ sb.AppendLine($"{tabString}///
");
}
private static void GenerateLocalizationMethod(
StringBuilder sb,
LocalizableString ls,
bool isCoreAssembly,
- PluginClassInfo pluginInfo)
+ PluginClassInfo pluginInfo,
+ string tabString)
{
- sb.Append($"public static string {ls.Key}(");
+ 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(") => ");
@@ -393,7 +509,10 @@ private static List BuildParameters(LocalizableString ls)
var parameters = new List();
for (var i = 0; i < 10; i++)
{
- if (!ls.Value.Contains($"{{{i}}}")) continue;
+ if (!ls.Value.Contains($"{{{i}}}"))
+ {
+ continue;
+ }
var param = ls.Params.FirstOrDefault(p => p.Index == i);
parameters.Add(param is null
@@ -403,6 +522,20 @@ private static List BuildParameters(LocalizableString ls)
return parameters;
}
+ private static string Spacing(int n)
+ {
+ 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();
+ }
+
#endregion
#region Classes
From 25f4458ac45ddfe475c2c3aa5d9c5b4be13b42d4 Mon Sep 17 00:00:00 2001
From: Jack251970 <1160210343@qq.com>
Date: Thu, 6 Mar 2025 09:39:42 +0800
Subject: [PATCH 6/8] Improve code quality
---
.../Localize/LocalizeSourceGenerator.cs | 12 ++++--------
1 file changed, 4 insertions(+), 8 deletions(-)
diff --git a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
index 9f28506..0099460 100644
--- a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
+++ b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
@@ -273,7 +273,8 @@ private static PluginClassInfo GetValidPluginInfo(
ImmutableArray pluginClasses,
SourceProductionContext context)
{
- if (pluginClasses.All(p => p is null || p.PropertyName == null))
+ var nonNullExistClasses = pluginClasses.Where(p => p != null || p.PropertyName == null).ToArray();
+ if (nonNullExistClasses.Length == 0)
{
context.ReportDiagnostic(Diagnostic.Create(
SourceGeneratorDiagnostics.CouldNotFindPluginEntryClass,
@@ -282,13 +283,8 @@ private static PluginClassInfo GetValidPluginInfo(
return null;
}
- foreach (var pluginClass in pluginClasses)
+ foreach (var pluginClass in nonNullExistClasses)
{
- if (pluginClass == null || pluginClass.PropertyName is null)
- {
- continue;
- }
-
if (pluginClass.IsValid == true)
{
return pluginClass;
@@ -330,7 +326,7 @@ private static Location GetLocation(SyntaxTree syntaxTree, CSharpSyntaxNode clas
return Location.Create(syntaxTree, classDeclaration.GetLocation().SourceSpan);
}
- #endregion
+#endregion
#region Generate Source
From af377375c517d07585b50b3f45d1085cd91ed41d Mon Sep 17 00:00:00 2001
From: Jack251970 <1160210343@qq.com>
Date: Thu, 6 Mar 2025 09:45:32 +0800
Subject: [PATCH 7/8] Fix issue
---
.../Localize/LocalizeSourceGenerator.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
index 0099460..bafacbb 100644
--- a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
+++ b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
@@ -273,7 +273,7 @@ private static PluginClassInfo GetValidPluginInfo(
ImmutableArray pluginClasses,
SourceProductionContext context)
{
- var nonNullExistClasses = pluginClasses.Where(p => p != null || p.PropertyName == null).ToArray();
+ var nonNullExistClasses = pluginClasses.Where(p => p != null && p.PropertyName == null).ToArray();
if (nonNullExistClasses.Length == 0)
{
context.ReportDiagnostic(Diagnostic.Create(
From 90cb196b3ce75414c0c54398a97d5aa366e5aa8b Mon Sep 17 00:00:00 2001
From: Jack251970 <1160210343@qq.com>
Date: Thu, 6 Mar 2025 13:20:06 +0800
Subject: [PATCH 8/8] Fix issue
---
.../Localize/LocalizeSourceGenerator.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
index bafacbb..1d48f6e 100644
--- a/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
+++ b/Flow.Launcher.Localization.SourceGenerators/Localize/LocalizeSourceGenerator.cs
@@ -273,7 +273,7 @@ private static PluginClassInfo GetValidPluginInfo(
ImmutableArray pluginClasses,
SourceProductionContext context)
{
- var nonNullExistClasses = pluginClasses.Where(p => p != null && p.PropertyName == null).ToArray();
+ var nonNullExistClasses = pluginClasses.Where(p => p != null && p.PropertyName != null).ToArray();
if (nonNullExistClasses.Length == 0)
{
context.ReportDiagnostic(Diagnostic.Create(