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(