From 19dc0ff61b32f4b50e0360195983ae7f9ee540bd Mon Sep 17 00:00:00 2001 From: FranckSix Date: Mon, 15 Sep 2025 16:35:26 -0400 Subject: [PATCH 01/14] Splitting concepts responsibility Separated the concepts and responsibilities of the Code Generators to avoid the confusion caused by partial classes. I applied RawString to format the unit manager code. This makes the code more readable and eliminates the use of AppendLineLF. I reorganized the namespaces to focus efforts on the code generators and avoid the distractions of utility classes. All the business logic is now under Generators. Utilities and satellite classes are under Tools. Extension methods under Extensions. Diagnostics have been grouped under the Rules class. I added a new one to track fatal errors and list it in the output. I also added various extensions to streamline the code. Reworking the code, I noticed that by not using the CodeGeneration option. The different classes don't go through the CurrentUICulture (which is problematic if you're using the tool in a Roslyn project). Now that the class responsibilities are clearly defined, I could add a new Generator to the IStringLocalization model. This interface is used in WinUI and Blazor projects, among others. --- .../Aigamo.ResXGenerator.Tests.csproj | 80 +- Aigamo.ResXGenerator.Tests/CodeGenTests.cs | 486 +++---- Aigamo.ResXGenerator.Tests/GeneratorTests.cs | 1256 +++++++++-------- .../GithubIssues/Issue3/GeneratorTests.cs | 403 +++--- .../GroupResxFilesTests.cs | 156 +- .../HelperGeneratorTests.cs | 110 +- Aigamo.ResXGenerator.Tests/SettingsTests.cs | 13 +- Aigamo.ResXGenerator.sln.DotSettings | 9 +- Aigamo.ResXGenerator/Constants.cs | 24 - .../Extensions/EnumerableExtensions.cs | 13 + .../Extensions/StringExtensions.cs | 15 + .../Generators/ComboGenerator.cs | 82 ++ .../Generators/GeneratedOutput.cs | 14 + .../Generators/ResourceManagerGenerator.cs | 234 +++ .../Generators/StringBuilderGenerator.cs | 183 +++ Aigamo.ResXGenerator/IGenerator.cs | 18 - Aigamo.ResXGenerator/Rules.cs | 42 + Aigamo.ResXGenerator/SourceGenerator.cs | 54 +- .../StringBuilderExtensions.cs | 17 - .../StringBuilderGenerator.ComboGenerator.cs | 183 --- .../StringBuilderGenerator.ResourceManager.cs | 134 -- .../StringBuilderGenerator.cs | 251 ---- Aigamo.ResXGenerator/StringExtensions.cs | 10 - .../{ => Tools}/AdditionalTextWithHash.cs | 4 +- Aigamo.ResXGenerator/Tools/Constants.cs | 22 + .../{ => Tools}/CultureInfoCombo.cs | 9 +- .../GenFileOptions.cs} | 10 +- .../{ => Tools}/GlobalOptions.cs | 4 +- .../{ => Tools}/GroupResxFiles.cs | 42 +- .../{ => Tools}/GroupedAdditionalFile.cs | 21 +- Aigamo.ResXGenerator/Tools/IGenerator.cs | 14 + .../{ => Tools}/InnerClassVisibility.cs | 2 +- .../{ => Tools}/IsExternalInit.cs | 0 .../{ => Tools}/NullableAttributes.cs | 7 +- .../Tools/RegexDefinitions.cs | 15 + Aigamo.ResXGenerator/{ => Tools}/Utilities.cs | 1 + 36 files changed, 1996 insertions(+), 1942 deletions(-) delete mode 100644 Aigamo.ResXGenerator/Constants.cs create mode 100644 Aigamo.ResXGenerator/Extensions/EnumerableExtensions.cs create mode 100644 Aigamo.ResXGenerator/Extensions/StringExtensions.cs create mode 100644 Aigamo.ResXGenerator/Generators/ComboGenerator.cs create mode 100644 Aigamo.ResXGenerator/Generators/GeneratedOutput.cs create mode 100644 Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs create mode 100644 Aigamo.ResXGenerator/Generators/StringBuilderGenerator.cs delete mode 100644 Aigamo.ResXGenerator/IGenerator.cs create mode 100644 Aigamo.ResXGenerator/Rules.cs delete mode 100644 Aigamo.ResXGenerator/StringBuilderExtensions.cs delete mode 100644 Aigamo.ResXGenerator/StringBuilderGenerator.ComboGenerator.cs delete mode 100644 Aigamo.ResXGenerator/StringBuilderGenerator.ResourceManager.cs delete mode 100644 Aigamo.ResXGenerator/StringBuilderGenerator.cs delete mode 100644 Aigamo.ResXGenerator/StringExtensions.cs rename Aigamo.ResXGenerator/{ => Tools}/AdditionalTextWithHash.cs (81%) create mode 100644 Aigamo.ResXGenerator/Tools/Constants.cs rename Aigamo.ResXGenerator/{ => Tools}/CultureInfoCombo.cs (79%) rename Aigamo.ResXGenerator/{FileOptions.cs => Tools/GenFileOptions.cs} (96%) rename Aigamo.ResXGenerator/{ => Tools}/GlobalOptions.cs (98%) rename Aigamo.ResXGenerator/{ => Tools}/GroupResxFiles.cs (61%) rename Aigamo.ResXGenerator/{ => Tools}/GroupedAdditionalFile.cs (52%) create mode 100644 Aigamo.ResXGenerator/Tools/IGenerator.cs rename Aigamo.ResXGenerator/{ => Tools}/InnerClassVisibility.cs (73%) rename Aigamo.ResXGenerator/{ => Tools}/IsExternalInit.cs (100%) rename Aigamo.ResXGenerator/{ => Tools}/NullableAttributes.cs (98%) create mode 100644 Aigamo.ResXGenerator/Tools/RegexDefinitions.cs rename Aigamo.ResXGenerator/{ => Tools}/Utilities.cs (99%) diff --git a/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj b/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj index 0027e0e..fdc04ec 100644 --- a/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj +++ b/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj @@ -1,46 +1,46 @@ - + - - net8.0 - false - latest - enable - enable - + + net8.0 + false + latest + enable + enable + - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - $([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx - - - true - - - true - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + $([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx + + + true + + + true + + - - - + + + diff --git a/Aigamo.ResXGenerator.Tests/CodeGenTests.cs b/Aigamo.ResXGenerator.Tests/CodeGenTests.cs index d313ad1..27883df 100644 --- a/Aigamo.ResXGenerator.Tests/CodeGenTests.cs +++ b/Aigamo.ResXGenerator.Tests/CodeGenTests.cs @@ -1,4 +1,6 @@ -using FluentAssertions; +using Aigamo.ResXGenerator.Generators; +using Aigamo.ResXGenerator.Tools; +using FluentAssertions; using Xunit; using static System.Guid; @@ -6,211 +8,217 @@ namespace Aigamo.ResXGenerator.Tests; public class CodeGenTests { - private const string Text = @" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Oldest - - - Newest - -"; + private const string Text = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Oldest + + + Newest + + + """; - private const string TextDa = @" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - OldestDa - - - NewestDa - -"; + private const string TextDa = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + OldestDa + + + NewestDa + + + """; - private const string TextDaDk = @" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - OldestDaDK - - - NewestDaDK - -"; + private const string TextDaDk = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + OldestDaDK + + + NewestDaDK + + + """; private static void Generate( - IGenerator generator, + IResXGenerator generator, bool publicClass = true, bool staticClass = true, bool partial = false, @@ -218,34 +226,37 @@ private static void Generate( bool staticMembers = true ) { - var expected = $@"// ------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// ------------------------------------------------------------------------------ -#nullable enable -namespace Resources; -using static Aigamo.ResXGenerator.Helpers; + var expected = $$""" + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace Resources; + using static Aigamo.ResXGenerator.Helpers; -{(publicClass ? "public" : "internal")}{(partial ? " partial" : string.Empty)}{(staticClass ? " static" : string.Empty)} class ActivityEntrySortRuleNames -{{ + {{(publicClass ? "public" : "internal")}}{{(partial ? " partial" : string.Empty)}}{{(staticClass ? " static" : string.Empty)}} class ActivityEntrySortRuleNames + { - /// - /// Looks up a localized string similar to Oldest. - /// - public{(staticMembers ? " static" : string.Empty)} string{(nullForgivingOperators ? string.Empty : "?")} CreateDate => GetString_1030_6(""Oldest"", ""OldestDaDK"", ""OldestDa""); + /// + /// Looks up a localized string similar to Oldest. + /// + public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDate => GetString_1030_6("Oldest", "OldestDaDK", "OldestDa"); - /// - /// Looks up a localized string similar to Newest. - /// - public{(staticMembers ? " static" : string.Empty)} string{(nullForgivingOperators ? string.Empty : "?")} CreateDateDescending => GetString_1030_6(""Newest"", ""NewestDaDK"", ""NewestDa""); -}} -"; - var (_, SourceCode, ErrorsAndWarnings) = generator.Generate( - options: new FileOptions() + /// + /// Looks up a localized string similar to Newest. + /// + public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDateDescending => GetString_1030_6("Newest", "NewestDaDK", "NewestDa"); + } + + """; + + var result = generator.Generate( + new GenFileOptions { LocalNamespace = "VocaDb.Web.App_GlobalResources", EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", @@ -253,11 +264,11 @@ namespace Resources; ClassName = "ActivityEntrySortRuleNames", GroupedFile = new GroupedAdditionalFile( mainFile: new AdditionalTextWithHash(new AdditionalTextStub(string.Empty, Text), NewGuid()), - subFiles: new[] - { + subFiles: + [ new AdditionalTextWithHash(new AdditionalTextStub("test.da.rex", TextDa), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.da-dk.rex", TextDaDk), NewGuid()), - } + new AdditionalTextWithHash(new AdditionalTextStub("test.da-dk.rex", TextDaDk), NewGuid()) + ] ), PublicClass = publicClass, GenerateCode = true, @@ -265,18 +276,17 @@ namespace Resources; StaticClass = staticClass, PartialClass = partial, StaticMembers = staticMembers - } - ); - ErrorsAndWarnings.Should().BeNullOrEmpty(); - SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + }); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); } [Fact] public void Generate_StringBuilder_Public() { - var generator = new StringBuilderGenerator(); + var generator = new ResourceManagerGenerator(); Generate(generator); - Generate(generator, true, nullForgivingOperators: true); + Generate(generator, nullForgivingOperators: true); } } diff --git a/Aigamo.ResXGenerator.Tests/GeneratorTests.cs b/Aigamo.ResXGenerator.Tests/GeneratorTests.cs index 39168eb..5cfa003 100644 --- a/Aigamo.ResXGenerator.Tests/GeneratorTests.cs +++ b/Aigamo.ResXGenerator.Tests/GeneratorTests.cs @@ -1,4 +1,6 @@ -using FluentAssertions; +using Aigamo.ResXGenerator.Generators; +using Aigamo.ResXGenerator.Tools; +using FluentAssertions; using Microsoft.CodeAnalysis; using Xunit; using static System.Guid; @@ -7,619 +9,641 @@ namespace Aigamo.ResXGenerator.Tests; public class GeneratorTests { - private const string Text = @" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Oldest - - - Newest - -"; - - private static void Generate( - IGenerator generator, - bool publicClass = true, - bool staticClass = true, - bool partial = false, - bool nullForgivingOperators = false, - bool staticMembers = true - ) - { - var expected = $@"// ------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// ------------------------------------------------------------------------------ -#nullable enable -namespace Resources; -using System.Globalization; -using System.Resources; - -{(publicClass ? "public" : "internal")}{(staticClass ? " static" : string.Empty)}{(partial ? " partial" : string.Empty)} class ActivityEntrySortRuleNames -{{ - private static ResourceManager? s_resourceManager; - public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager(""VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames"", typeof(ActivityEntrySortRuleNames).Assembly); - public{(staticMembers ? " static" : string.Empty)} CultureInfo? CultureInfo {{ get; set; }} - - /// - /// Looks up a localized string similar to Oldest. - /// - public{(staticMembers ? " static" : string.Empty)} string{(nullForgivingOperators ? string.Empty : "?")} CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo){(nullForgivingOperators ? "!" : string.Empty)}; - - /// - /// Looks up a localized string similar to Newest. - /// - public{(staticMembers ? " static" : string.Empty)} string{(nullForgivingOperators ? string.Empty : "?")} CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo){(nullForgivingOperators ? "!" : string.Empty)}; -}} -"; - var (_, SourceCode, ErrorsAndWarnings) = generator.Generate( - options: new FileOptions() - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", - CustomToolNamespace = "Resources", - ClassName = "ActivityEntrySortRuleNames", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", Text), NewGuid()), - subFiles: Array.Empty() - ), - PublicClass = publicClass, - NullForgivingOperators = nullForgivingOperators, - StaticClass = staticClass, - PartialClass = partial, - StaticMembers = staticMembers - } - ); - ErrorsAndWarnings.Should().BeNullOrEmpty(); - SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } - - private static void GenerateInner( - IGenerator generator, - bool publicClass = true, - bool staticClass = false, - bool partial = false, - bool nullForgivingOperators = false, - bool staticMembers = true, - string innerClassName = "inner", - InnerClassVisibility innerClassVisibility = InnerClassVisibility.SameAsOuter, - string innerClassInstanceName = "" - ) - { - var expected = $@"// ------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// ------------------------------------------------------------------------------ -#nullable enable -namespace Resources; -using System.Globalization; -using System.Resources; - -{(publicClass ? "public" : "internal")}{(partial ? " partial" : string.Empty)}{(staticClass ? " static" : string.Empty)} class ActivityEntrySortRuleNames -{{{(string.IsNullOrEmpty(innerClassInstanceName) ? string.Empty : $"\n public {innerClassName} {innerClassInstanceName} {{ get; }} = new();\n")} - {(publicClass ? "public" : "internal")}{(partial ? " partial" : string.Empty)}{(staticClass ? " static" : string.Empty)} class {innerClassName} - {{ - private static ResourceManager? s_resourceManager; - public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager(""VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames"", typeof({innerClassName}).Assembly); - public{(staticMembers ? " static" : string.Empty)} CultureInfo? CultureInfo {{ get; set; }} - - /// - /// Looks up a localized string similar to Oldest. - /// - public{(staticMembers ? " static" : string.Empty)} string{(nullForgivingOperators ? string.Empty : "?")} CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo){(nullForgivingOperators ? "!" : string.Empty)}; - - /// - /// Looks up a localized string similar to Newest. - /// - public{(staticMembers ? " static" : string.Empty)} string{(nullForgivingOperators ? string.Empty : "?")} CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo){(nullForgivingOperators ? "!" : string.Empty)}; - }} -}} -"; - var (_, SourceCode, ErrorsAndWarnings) = generator.Generate( - options: new FileOptions() - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", - CustomToolNamespace = "Resources", - ClassName = "ActivityEntrySortRuleNames", - PublicClass = publicClass, - NullForgivingOperators = nullForgivingOperators, - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", Text), NewGuid()), - subFiles: Array.Empty() - ), - StaticClass = staticClass, - PartialClass = partial, - StaticMembers = staticMembers, - InnerClassName = innerClassName, - InnerClassVisibility = innerClassVisibility, - InnerClassInstanceName = innerClassInstanceName - } - ); - ErrorsAndWarnings.Should().BeNullOrEmpty(); - SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } - - [Fact] - public void Generate_StringBuilder_Public() - { - var generator = new StringBuilderGenerator(); - Generate(generator); - Generate(generator, true, nullForgivingOperators: true); - } - - [Fact] - public void Generate_StringBuilder_NonStatic() - { - var generator = new StringBuilderGenerator(); - Generate(generator, staticClass: false); - Generate(generator, staticClass: false, nullForgivingOperators: true); - } - - [Fact] - public void Generate_StringBuilder_Internal() - { - var generator = new StringBuilderGenerator(); - Generate(generator, false); - Generate(generator, false, nullForgivingOperators: true); - } - - [Fact] - public void Generate_StringBuilder_Partial() - { - var generator = new StringBuilderGenerator(); - Generate(generator, partial: true); - Generate(generator, partial: true, nullForgivingOperators: true); - } - - [Fact] - public void Generate_StringBuilder_NonStaticMembers() - { - var generator = new StringBuilderGenerator(); - Generate(generator, staticMembers: false); - Generate(generator, staticMembers: false, nullForgivingOperators: true); - } - - [Fact] - public void Generate_StringBuilder_Inner() - { - var generator = new StringBuilderGenerator(); - GenerateInner(generator); - } - - [Fact] - public void Generate_StringBuilder_InnerInstance() - { - var generator = new StringBuilderGenerator(); - GenerateInner(generator, innerClassInstanceName: "Resources", staticMembers: false); - } - - [Fact] - public void Generate_StringBuilder_NewLine() - { - var text = @" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - This entry has been deleted. It is still temporarily accessible, but won't show up in any of the listings. - - - This entry was merged to - - - Draft = entry is missing crucial information. This status indicates that you're requesting additional information to be added or corrected.<br /> -Finished = The entry has all the necessary information, but it hasn't been inspected by a trusted user yet.<br /> -Approved = The entry has been inspected and approved by a trusted user. Approved entries can only be edited by trusted users. - - - This entry is locked, meaning that only moderators are allowed to edit it. - - - Choose the language for this name. ""Original"" is the name in original language that isn't English, for example Japanese. If the original language is English, do not input a name in the ""Original"" language. - - - This page revision has been hidden. - -"; - var expected = @"// ------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// ------------------------------------------------------------------------------ -#nullable enable -namespace VocaDb.Web.App_GlobalResources; -using System.Globalization; -using System.Resources; - -public static class CommonMessages -{ - private static ResourceManager? s_resourceManager; - public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager(""VocaDb.Web.App_GlobalResources.CommonMessages"", typeof(CommonMessages).Assembly); - public static CultureInfo? CultureInfo { get; set; } - - /// - /// Looks up a localized string similar to This entry has been deleted. It is still temporarily accessible, but won't show up in any of the listings.. - /// - public static string? EntryDeleted => ResourceManager.GetString(nameof(EntryDeleted), CultureInfo); - - /// - /// Looks up a localized string similar to This entry was merged to. - /// - public static string? EntryMergedTo => ResourceManager.GetString(nameof(EntryMergedTo), CultureInfo); - - /// - /// Looks up a localized string similar to Draft = entry is missing crucial information. This status indicates that you're requesting additional information to be added or corrected.<br /> - /// Finished = The entry has all the necessary information, but it hasn't been inspected by a trusted user yet.<br /> - /// Approved = The entry has been inspected and approved by a trusted user. Approved entries can only be edited by trusted users.. - /// - public static string? EntryStatusExplanation => ResourceManager.GetString(nameof(EntryStatusExplanation), CultureInfo); - - /// - /// Looks up a localized string similar to This entry is locked, meaning that only moderators are allowed to edit it.. - /// - public static string? Locked => ResourceManager.GetString(nameof(Locked), CultureInfo); - - /// - /// Looks up a localized string similar to Choose the language for this name. "Original" is the name in original language that isn't English, for example Japanese. If the original language is English, do not input a name in the "Original" language.. - /// - public static string? NameLanguageHelp => ResourceManager.GetString(nameof(NameLanguageHelp), CultureInfo); - - /// - /// Looks up a localized string similar to This page revision has been hidden.. - /// - public static string? RevisionHidden => ResourceManager.GetString(nameof(RevisionHidden), CultureInfo); -} -"; - var generator = new StringBuilderGenerator(); - var (_, SourceCode, ErrorsAndWarnings) = generator.Generate( - options: new FileOptions() - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - CustomToolNamespace = null, - ClassName = "CommonMessages", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), - subFiles: Array.Empty() - ), - PublicClass = true, - NullForgivingOperators = false, - StaticClass = true, - StaticMembers = true - } - ); - ErrorsAndWarnings.Should().BeNullOrEmpty(); - SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } - - [Fact] - public void Generate_StringBuilder_Name_PartialXmlWorks() - { - var text = @" - - - Works. - -"; - - var expected = @"// ------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// ------------------------------------------------------------------------------ -#nullable enable -namespace VocaDb.Web.App_GlobalResources; -using System.Globalization; -using System.Resources; - -public static class CommonMessages -{ - private static ResourceManager? s_resourceManager; - public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager(""VocaDb.Web.App_GlobalResources.CommonMessages"", typeof(CommonMessages).Assembly); - public static CultureInfo? CultureInfo { get; set; } - - /// - /// Looks up a localized string similar to Works.. - /// - public static string? Works => ResourceManager.GetString(nameof(Works), CultureInfo); -} -"; - var generator = new StringBuilderGenerator(); - var (_, SourceCode, ErrorsAndWarnings) = generator.Generate( - options: new FileOptions() - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - CustomToolNamespace = null, - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), - subFiles: Array.Empty() - ), - ClassName = "CommonMessages", - PublicClass = true, - NullForgivingOperators = false, - StaticClass = true, - StaticMembers = true - } - ); - ErrorsAndWarnings.Should().BeNullOrEmpty(); - SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } - - [Fact] - public void Generate_StringBuilder_Name_DuplicatedataGivesWarning() - { - var text = @" - - - Works. - - - Doeesnt Work. - -"; - - var generator = new StringBuilderGenerator(); - var (_, _, ErrorsAndWarnings) = generator.Generate( - options: new FileOptions() - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), - subFiles: Array.Empty() - ), - CustomToolNamespace = null, - ClassName = "CommonMessages", - PublicClass = true, - NullForgivingOperators = false, - StaticClass = true - } - ); - var errs = ErrorsAndWarnings.ToList(); - errs.Should().NotBeNull(); - errs.Should().HaveCount(1); - errs[0].Id.Should().Be("AigamoResXGenerator001"); - errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); - errs[0].GetMessage().Should().Contain("DupKey"); - errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(5); - } - - [Fact] - public void Generate_StringBuilder_Name_MemberSameAsFileGivesWarning() - { - var text = @" - - - Works. - -"; - - var generator = new StringBuilderGenerator(); - var (_, _, ErrorsAndWarnings) = generator.Generate( - options: new FileOptions() - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), - subFiles: Array.Empty() - ), - CustomToolNamespace = null, - ClassName = "CommonMessages", - PublicClass = true, - NullForgivingOperators = false, - StaticClass = true - } - ); - var errs = ErrorsAndWarnings.ToList(); - errs.Should().NotBeNull(); - errs.Should().HaveCount(1); - errs[0].Id.Should().Be("AigamoResXGenerator002"); - errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); - errs[0].GetMessage().Should().Contain("CommonMessages"); - errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(2); - } - - [Fact] - public void GetLocalNamespace_ShouldNotGenerateIllegalNamespace() - { - var ns = Utilities.GetLocalNamespace("resx", "asd.asd", "path", "name", "root"); - ns.Should().Be("root"); - } - - [Fact] - public void ResxFileName_ShouldNotGenerateIllegalClassnames() - { - var ns = Utilities.GetClassNameFromPath("test.cshtml.resx"); - ns.Should().Be("test"); - } + private const string Text = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Oldest + + + Newest + + + """; + + private static void Generate( + IResXGenerator generator, + bool publicClass = true, + bool staticClass = true, + bool partial = false, + bool nullForgivingOperators = false, + bool staticMembers = true + ) + { + var expected = $$""" + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace Resources; + using System.Globalization; + using System.Resources; + + {{(publicClass ? "public" : "internal")}}{{(staticClass ? " static" : string.Empty)}}{{(partial ? " partial" : string.Empty)}} class ActivityEntrySortRuleNames + { + private static ResourceManager? s_resourceManager; + public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", typeof(ActivityEntrySortRuleNames).Assembly); + public{{(staticMembers ? " static" : string.Empty)}} CultureInfo? CultureInfo { get; set; } + + /// + /// Looks up a localized string similar to Oldest. + /// + public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo){{(nullForgivingOperators ? "!" : string.Empty)}}; + + /// + /// Looks up a localized string similar to Newest. + /// + public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo){{(nullForgivingOperators ? "!" : string.Empty)}}; + } + + """; + + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", + CustomToolNamespace = "Resources", + ClassName = "ActivityEntrySortRuleNames", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", Text), NewGuid()), + subFiles: [] + ), + PublicClass = publicClass, + NullForgivingOperators = nullForgivingOperators, + StaticClass = staticClass, + PartialClass = partial, + StaticMembers = staticMembers + } + ); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } + + private static void GenerateInner( + IResXGenerator generator, + bool publicClass = true, + bool staticClass = false, + bool partial = false, + bool nullForgivingOperators = false, + bool staticMembers = true, + string innerClassName = "inner", + InnerClassVisibility innerClassVisibility = InnerClassVisibility.SameAsOuter, + string innerClassInstanceName = "" + ) + { + var expected = $$""" + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace Resources; + using System.Globalization; + using System.Resources; + + {{(publicClass ? "public" : "internal")}}{{(partial ? " partial" : string.Empty)}}{{(staticClass ? " static" : string.Empty)}} class ActivityEntrySortRuleNames + {{{(string.IsNullOrEmpty(innerClassInstanceName) ? string.Empty : $"\n public {innerClassName} {innerClassInstanceName} {{ get; }} = new();\n")}} + {{(publicClass ? "public" : "internal")}}{{(partial ? " partial" : string.Empty)}}{{(staticClass ? " static" : string.Empty)}} class {{innerClassName}} + { + private static ResourceManager? s_resourceManager; + public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", typeof({{innerClassName}}).Assembly); + public{{(staticMembers ? " static" : string.Empty)}} CultureInfo? CultureInfo { get; set; } + + /// + /// Looks up a localized string similar to Oldest. + /// + public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo){{(nullForgivingOperators ? "!" : string.Empty)}}; + + /// + /// Looks up a localized string similar to Newest. + /// + public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo){{(nullForgivingOperators ? "!" : string.Empty)}}; + } + } + + """; + + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", + CustomToolNamespace = "Resources", + ClassName = "ActivityEntrySortRuleNames", + PublicClass = publicClass, + NullForgivingOperators = nullForgivingOperators, + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", Text), NewGuid()), + subFiles: [] + ), + StaticClass = staticClass, + PartialClass = partial, + StaticMembers = staticMembers, + InnerClassName = innerClassName, + InnerClassVisibility = innerClassVisibility, + InnerClassInstanceName = innerClassInstanceName + } + ); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } + + [Fact] + public void Generate_StringBuilder_Public() + { + var generator = new ResourceManagerGenerator(); + Generate(generator); + Generate(generator, nullForgivingOperators: true); + } + + [Fact] + public void Generate_StringBuilder_NonStatic() + { + var generator = new ResourceManagerGenerator(); + Generate(generator, staticClass: false); + Generate(generator, staticClass: false, nullForgivingOperators: true); + } + + [Fact] + public void Generate_StringBuilder_Internal() + { + var generator = new ResourceManagerGenerator(); + Generate(generator, false); + Generate(generator, false, nullForgivingOperators: true); + } + + [Fact] + public void Generate_StringBuilder_Partial() + { + var generator = new ResourceManagerGenerator(); + Generate(generator, partial: true); + Generate(generator, partial: true, nullForgivingOperators: true); + } + + [Fact] + public void Generate_StringBuilder_NonStaticMembers() + { + var generator = new ResourceManagerGenerator(); + Generate(generator, staticMembers: false); + Generate(generator, staticMembers: false, nullForgivingOperators: true); + } + + [Fact] + public void Generate_StringBuilder_Inner() + { + var generator = new ResourceManagerGenerator(); + GenerateInner(generator); + } + + [Fact] + public void Generate_StringBuilder_InnerInstance() + { + var generator = new ResourceManagerGenerator(); + GenerateInner(generator, innerClassInstanceName: "Resources", staticMembers: false); + } + + [Fact] + public void Generate_StringBuilder_NewLine() + { + const string text = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + This entry has been deleted. It is still temporarily accessible, but won't show up in any of the listings. + + + This entry was merged to + + + Draft = entry is missing crucial information. This status indicates that you're requesting additional information to be added or corrected.<br /> + Finished = The entry has all the necessary information, but it hasn't been inspected by a trusted user yet.<br /> + Approved = The entry has been inspected and approved by a trusted user. Approved entries can only be edited by trusted users. + + + This entry is locked, meaning that only moderators are allowed to edit it. + + + Choose the language for this name. "Original" is the name in original language that isn't English, for example Japanese. If the original language is English, do not input a name in the "Original" language. + + + This page revision has been hidden. + + + """; + + const string expected = """ + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace VocaDb.Web.App_GlobalResources; + using System.Globalization; + using System.Resources; + + public static class CommonMessages + { + private static ResourceManager? s_resourceManager; + public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.CommonMessages", typeof(CommonMessages).Assembly); + public static CultureInfo? CultureInfo { get; set; } + + /// + /// Looks up a localized string similar to This entry has been deleted. It is still temporarily accessible, but won't show up in any of the listings.. + /// + public static string? EntryDeleted => ResourceManager.GetString(nameof(EntryDeleted), CultureInfo); + + /// + /// Looks up a localized string similar to This entry was merged to. + /// + public static string? EntryMergedTo => ResourceManager.GetString(nameof(EntryMergedTo), CultureInfo); + + /// + /// Looks up a localized string similar to Draft = entry is missing crucial information. This status indicates that you're requesting additional information to be added or corrected.<br /> + /// Finished = The entry has all the necessary information, but it hasn't been inspected by a trusted user yet.<br /> + /// Approved = The entry has been inspected and approved by a trusted user. Approved entries can only be edited by trusted users.. + /// + public static string? EntryStatusExplanation => ResourceManager.GetString(nameof(EntryStatusExplanation), CultureInfo); + + /// + /// Looks up a localized string similar to This entry is locked, meaning that only moderators are allowed to edit it.. + /// + public static string? Locked => ResourceManager.GetString(nameof(Locked), CultureInfo); + + /// + /// Looks up a localized string similar to Choose the language for this name. "Original" is the name in original language that isn't English, for example Japanese. If the original language is English, do not input a name in the "Original" language.. + /// + public static string? NameLanguageHelp => ResourceManager.GetString(nameof(NameLanguageHelp), CultureInfo); + + /// + /// Looks up a localized string similar to This page revision has been hidden.. + /// + public static string? RevisionHidden => ResourceManager.GetString(nameof(RevisionHidden), CultureInfo); + } + + """; + var generator = new ResourceManagerGenerator(); + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + CustomToolNamespace = null, + ClassName = "CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), + subFiles: [] + ), + PublicClass = true, + NullForgivingOperators = false, + StaticClass = true, + StaticMembers = true + } + ); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } + + [Fact] + public void Generate_StringBuilder_Name_PartialXmlWorks() + { + const string text = """ + + + + Works. + + + """; + + const string expected = """ + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace VocaDb.Web.App_GlobalResources; + using System.Globalization; + using System.Resources; + + public static class CommonMessages + { + private static ResourceManager? s_resourceManager; + public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.CommonMessages", typeof(CommonMessages).Assembly); + public static CultureInfo? CultureInfo { get; set; } + + /// + /// Looks up a localized string similar to Works.. + /// + public static string? Works => ResourceManager.GetString(nameof(Works), CultureInfo); + } + + """; + + var generator = new ResourceManagerGenerator(); + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + CustomToolNamespace = null, + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), + subFiles: [] + ), + ClassName = "CommonMessages", + PublicClass = true, + NullForgivingOperators = false, + StaticClass = true, + StaticMembers = true + } + ); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } + + [Fact] + public void Generate_StringBuilder_Name_DuplicateDataGivesWarning() + { + const string text = """ + + + + Works. + + + Doeesnt Work. + + + """; + + var generator = new ResourceManagerGenerator(); + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), + subFiles: [] + ), + CustomToolNamespace = null, + ClassName = "CommonMessages", + PublicClass = true, + NullForgivingOperators = false, + StaticClass = true + } + ); + var errs = result.ErrorsAndWarnings.ToList(); + errs.Should().NotBeNull(); + errs.Should().HaveCount(1); + errs[0].Id.Should().Be("AigamoResXGenerator001"); + errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); + errs[0].GetMessage().Should().Contain("DupKey"); + errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(5); + } + + [Fact] + public void Generate_StringBuilder_Name_MemberSameAsFileGivesWarning() + { + const string text = """ + + + + Works. + + + """; + + var generator = new ResourceManagerGenerator(); + var results = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), + subFiles: [] + ), + CustomToolNamespace = null, + ClassName = "CommonMessages", + PublicClass = true, + NullForgivingOperators = false, + StaticClass = true + } + ); + var errs = results.ErrorsAndWarnings.ToList(); + errs.Should().NotBeNull(); + errs.Should().HaveCount(1); + errs[0].Id.Should().Be("AigamoResXGenerator002"); + errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); + errs[0].GetMessage().Should().Contain("CommonMessages"); + errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(2); + } + + [Fact] + public void GetLocalNamespace_ShouldNotGenerateIllegalNamespace() + { + var ns = Utilities.GetLocalNamespace("resx", "asd.asd", "path", "name", "root"); + ns.Should().Be("root"); + } + + [Fact] + public void ResxFileName_ShouldNotGenerateIllegalClassNames() + { + var ns = Utilities.GetClassNameFromPath("test.cshtml.resx"); + ns.Should().Be("test"); + } } diff --git a/Aigamo.ResXGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs b/Aigamo.ResXGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs index 86e0a09..4950714 100644 --- a/Aigamo.ResXGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs +++ b/Aigamo.ResXGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs @@ -1,4 +1,6 @@ using System.Xml; +using Aigamo.ResXGenerator.Generators; +using Aigamo.ResXGenerator.Tools; using FluentAssertions; using Xunit; using static System.Guid; @@ -7,205 +9,212 @@ namespace Aigamo.ResXGenerator.Tests.GithubIssues.Issue3; public class GeneratorTests { - [Fact] - public void Generate_StringBuilder_Name_NotValidIdentifier() - { - var text = @" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - String '{0}' is not a valid identifier. - -"; + [Fact] + public void Generate_StringBuilder_Name_NotValidIdentifier() + { + const string text = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + String '{0}' is not a valid identifier. + + + """; - var expected = @"// ------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// ------------------------------------------------------------------------------ -#nullable enable -namespace VocaDb.Web.App_GlobalResources; -using System.Globalization; -using System.Resources; + const string expected = """ + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace VocaDb.Web.App_GlobalResources; + using System.Globalization; + using System.Resources; -public static class CommonMessages -{ - private static ResourceManager? s_resourceManager; - public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager(""VocaDb.Web.App_GlobalResources.CommonMessages"", typeof(CommonMessages).Assembly); - public static CultureInfo? CultureInfo { get; set; } + public static class CommonMessages + { + private static ResourceManager? s_resourceManager; + public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.CommonMessages", typeof(CommonMessages).Assembly); + public static CultureInfo? CultureInfo { get; set; } - /// - /// Looks up a localized string similar to String '{0}' is not a valid identifier.. - /// - public static string? Invalid_identifier__0_ => ResourceManager.GetString(""Invalid identifier {0}"", CultureInfo); -} -"; - var generator = new StringBuilderGenerator(); - var source = generator.Generate( - new FileOptions() - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - CustomToolNamespace = null, - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), - subFiles: Array.Empty() - ), - ClassName = "CommonMessages", - PublicClass = true, - NullForgivingOperators = false, - StaticClass = true, - StaticMembers = true - } - ); - source.ErrorsAndWarnings.Should().BeNullOrEmpty(); - source.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } + /// + /// Looks up a localized string similar to String '{0}' is not a valid identifier.. + /// + public static string? Invalid_identifier__0_ => ResourceManager.GetString("Invalid identifier {0}", CultureInfo); + } + + """; + + var generator = new ResourceManagerGenerator(); + var source = generator.Generate( + new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + CustomToolNamespace = null, + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), + subFiles: [] + ), + ClassName = "CommonMessages", + PublicClass = true, + NullForgivingOperators = false, + StaticClass = true, + StaticMembers = true + } + ); + source.ErrorsAndWarnings.Should().BeNullOrEmpty(); + source.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } - [Fact] - public void Generate_StringBuilder_Value_InvalidCharacter() - { - var text = $@" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Old{"\0"}est - - - Newest - -"; - var generator = new StringBuilderGenerator(); - var options = new FileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - CustomToolNamespace = "Resources", - ClassName = "ActivityEntrySortRuleNames", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), - subFiles: Array.Empty() - ), - PublicClass = true, - NullForgivingOperators = false, - StaticClass = true - }; - generator.Invoking(subject => subject.Generate(options)).Should().Throw(); - } + [Fact] + public void Generate_StringBuilder_Value_InvalidCharacter() + { + const string text = $""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Old{"\0"}est + + + Newest + + + """; + var generator = new ResourceManagerGenerator(); + var options = new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + CustomToolNamespace = "Resources", + ClassName = "ActivityEntrySortRuleNames", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), + subFiles: [] + ), + PublicClass = true, + NullForgivingOperators = false, + StaticClass = true + }; + generator.Invoking(subject => subject.Generate(options)).Should().Throw(); + } } diff --git a/Aigamo.ResXGenerator.Tests/GroupResxFilesTests.cs b/Aigamo.ResXGenerator.Tests/GroupResxFilesTests.cs index 7e73e96..703e5c5 100644 --- a/Aigamo.ResXGenerator.Tests/GroupResxFilesTests.cs +++ b/Aigamo.ResXGenerator.Tests/GroupResxFilesTests.cs @@ -1,4 +1,5 @@ -using FluentAssertions; +using Aigamo.ResXGenerator.Tools; +using FluentAssertions; using Xunit; using static System.Guid; @@ -11,20 +12,18 @@ public void CompareGroupedAdditionalFile_SameRoot_SameSubFiles_DifferentOrder() { var v1 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), - new[] - { - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), - }); + [ + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")) + ]); var v2 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), - new[] - { + [ new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")), - } + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")) + ] ); v1.Should().Be(v2); } @@ -34,20 +33,17 @@ public void CompareGroupedAdditionalFile_SameRoot_DiffSubFilesNames() { var v1 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), - new[] - { + [ new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.en.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.fr.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), - }); + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.fr.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")) + ]); var v2 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), - new[] - { - + [ new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.de.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.ro.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")), - } + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.ro.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")) + ] ); v1.Should().NotBe(v2); } @@ -57,20 +53,17 @@ public void CompareGroupedAdditionalFile_SameRoot_DiffSubFileContent() { var v1 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), - new[] - { + [ new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("771F9C76-D9F4-4AF4-95D2-B3426F9EC15A")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), - }); + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")) + ]); var v2 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), - new[] - { - + [ new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")), - } + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")) + ] ); v1.Should().NotBe(v2); } @@ -80,26 +73,24 @@ public void CompareGroupedAdditionalFile_DiffRootContent_SameSubFiles() { var v1 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), - new[] - { + [ new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), - }); + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")) + ]); var v2 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("A7E92264-8047-4668-979F-6EFC14EBAFC5")), - new[] - { + [ new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")), - } + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")) + ] ); v1.Should().NotBe(v2); } static readonly (string Path, Guid Hash)[] s_data = - { + [ (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx", Parse("00000000-0000-0000-0000-000000000001")), (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx", Parse("00000000-0000-0000-0000-000000000002")), (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx", Parse("00000000-0000-0000-0000-000000000003")), @@ -133,52 +124,52 @@ static readonly (string Path, Guid Hash)[] s_data = (@"D:\src\xhg\y\Areas\QxModule\QtrController.zh-cn.resx", Parse("00000000-0000-0000-0000-000000000031")), (@"D:\src\xhg\y\DataAnnotations\DataAnnotation.da.resx", Parse("00000000-0000-0000-0000-000000000032")), (@"D:\src\xhg\y\DataAnnotations\DataAnnotation.resx", Parse("00000000-0000-0000-0000-000000000033")), - (@"D:\src\xhg\y\DataAnnotations\DataAnnotation2.resx", Parse("00000000-0000-0000-0000-000000000034")), - }; + (@"D:\src\xhg\y\DataAnnotations\DataAnnotation2.resx", Parse("00000000-0000-0000-0000-000000000034")) + ]; [Fact] public void FileGrouping() { - var result = GroupResxFiles.Group(s_data.Select(x => new AdditionalTextWithHash(new AdditionalTextStub(x.Path), x.Hash)).OrderBy(x => NewGuid()).ToArray()); + var result = GroupResxFiles.Group(s_data.Select(x => new AdditionalTextWithHash(new AdditionalTextStub(x.Path), x.Hash)).OrderBy(_ => NewGuid()).ToArray()); var testData = new List { - new GroupedAdditionalFile( + new( mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("00000000-0000-0000-0000-000000000002")), - subFiles: new[] - { + subFiles: + [ new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("00000000-0000-0000-0000-000000000001")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("00000000-0000-0000-0000-000000000003")), - } + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("00000000-0000-0000-0000-000000000003")) + ] ), - new GroupedAdditionalFile( + new( mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.resx"), Parse("00000000-0000-0000-0000-000000000005")), - subFiles: new[] - { + subFiles: + [ new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.da.resx"), Parse("00000000-0000-0000-0000-000000000004")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.vi.resx"), Parse("00000000-0000-0000-0000-000000000006")), - } + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.vi.resx"), Parse("00000000-0000-0000-0000-000000000006")) + ] ), - new GroupedAdditionalFile( + new( mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\Identity\Pages\Login.resx"), Parse("00000000-0000-0000-0000-000000000008")), - subFiles: new[] - { + subFiles: + [ new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\Identity\Pages\Login.da.resx"), Parse("00000000-0000-0000-0000-000000000007")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\Identity\Pages\Login.vi.resx"), Parse("00000000-0000-0000-0000-000000000009")), - } + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\Identity\Pages\Login.vi.resx"), Parse("00000000-0000-0000-0000-000000000009")) + ] ), - new GroupedAdditionalFile( + new( mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.resx"), Parse("00000000-0000-0000-0000-000000000011")), - subFiles: new[] - { + subFiles: + [ new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.da.resx"), Parse("00000000-0000-0000-0000-000000000010")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.vi.resx"), Parse("00000000-0000-0000-0000-000000000012")), - } + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.vi.resx"), Parse("00000000-0000-0000-0000-000000000012")) + ] ), - new GroupedAdditionalFile( + new( mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.resx"), Parse("00000000-0000-0000-0000-000000000026")), - subFiles: new[] - { + subFiles: + [ new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.cs-cz.resx"), Parse("00000000-0000-0000-0000-000000000013")), new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.da.resx"), Parse("00000000-0000-0000-0000-000000000014")), new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.de.resx"), Parse("00000000-0000-0000-0000-000000000015")), @@ -196,43 +187,38 @@ public void FileGrouping() new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.sv.resx"), Parse("00000000-0000-0000-0000-000000000028")), new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.tr.resx"), Parse("00000000-0000-0000-0000-000000000029")), new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.vi.resx"), Parse("00000000-0000-0000-0000-000000000030")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.zh-cn.resx"), Parse("00000000-0000-0000-0000-000000000031")), - }), - new GroupedAdditionalFile( + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.zh-cn.resx"), Parse("00000000-0000-0000-0000-000000000031")) + ]), + new( mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation.resx"), Parse("00000000-0000-0000-0000-000000000033")), - subFiles: new[] - { + subFiles: + [ new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation.da.resx"), Parse("00000000-0000-0000-0000-000000000032")) - } + ] ), - new GroupedAdditionalFile( + new( mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation2.resx"), Parse("00000000-0000-0000-0000-000000000034")), - subFiles: Array.Empty() + subFiles: [] ) }; var resAsList = result.ToList(); resAsList.Count.Should().Be(testData.Count); - foreach (var groupedAdditionalFile in testData) - { - resAsList.Should().Contain(groupedAdditionalFile); - } + testData.ForEach(groupedAdditionalFile => resAsList.Should().Contain(groupedAdditionalFile)); } [Fact] public void ResxGrouping() { - var result = GroupResxFiles.DetectChildCombos(GroupResxFiles.Group(s_data.Select(x => new AdditionalTextWithHash(new AdditionalTextStub(x.Path), NewGuid())).OrderBy(x => NewGuid()).ToArray()).ToArray()).ToList(); + var result = GroupResxFiles.DetectChildCombos(GroupResxFiles.Group(s_data.Select(x => new AdditionalTextWithHash(new AdditionalTextStub(x.Path), NewGuid())).OrderBy(_ => NewGuid()).ToArray()).ToArray()).ToList(); var expected = new List { - new CultureInfoCombo(new[] - { + new([ new AdditionalTextWithHash(new AdditionalTextStub("test.da.resx"), NewGuid()), new AdditionalTextWithHash(new AdditionalTextStub("test.vi.resx"), NewGuid()) - }), - new CultureInfoCombo(new[]{ new AdditionalTextWithHash(new AdditionalTextStub("test.da.resx"), NewGuid())}), - new CultureInfoCombo(Array.Empty()), - new CultureInfoCombo(new[] - { + ]), + new([new AdditionalTextWithHash(new AdditionalTextStub("test.da.resx"), NewGuid())]), + new([]), + new([ new AdditionalTextWithHash(new AdditionalTextStub("test.cs-cz.resx"), NewGuid()), new AdditionalTextWithHash(new AdditionalTextStub("test.da.resx"), NewGuid()), new AdditionalTextWithHash(new AdditionalTextStub("test.de.resx"), NewGuid()), @@ -250,8 +236,8 @@ public void ResxGrouping() new AdditionalTextWithHash(new AdditionalTextStub("test.sv.resx"), NewGuid()), new AdditionalTextWithHash(new AdditionalTextStub("test.tr.resx"), NewGuid()), new AdditionalTextWithHash(new AdditionalTextStub("test.vi.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.zh-cn.resx"), NewGuid()), - }), + new AdditionalTextWithHash(new AdditionalTextStub("test.zh-cn.resx"), NewGuid()) + ]), }; result.Count.Should().Be(expected.Count); result.Should().BeEquivalentTo(expected); diff --git a/Aigamo.ResXGenerator.Tests/HelperGeneratorTests.cs b/Aigamo.ResXGenerator.Tests/HelperGeneratorTests.cs index 7a38837..7181ca5 100644 --- a/Aigamo.ResXGenerator.Tests/HelperGeneratorTests.cs +++ b/Aigamo.ResXGenerator.Tests/HelperGeneratorTests.cs @@ -1,4 +1,6 @@ -using FluentAssertions; +using Aigamo.ResXGenerator.Generators; +using Aigamo.ResXGenerator.Tools; +using FluentAssertions; using Xunit; namespace Aigamo.ResXGenerator.Tests; @@ -8,65 +10,69 @@ public class HelperGeneratorTests [Fact] public void CanGenerateCombo() { - var (generatedFileName, sourceCode, errorsAndWarnings) = new StringBuilderGenerator() + var result = new ComboGenerator() .Generate( - combo: new CultureInfoCombo( - files: new[] - { + new CultureInfoCombo( + files: + [ new AdditionalTextWithHash(new AdditionalTextStub("test.da.rex"), Guid.NewGuid()), new AdditionalTextWithHash(new AdditionalTextStub("test.da-dk.rex"), Guid.NewGuid()) - } + ] ), - cancellationToken: default + CancellationToken.None ); - var expected = @"// ------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// ------------------------------------------------------------------------------ -#nullable enable -namespace Aigamo.ResXGenerator; -internal static partial class Helpers -{ - public static string GetString_1030_6(string fallback, string da_DK, string da) => System.Globalization.CultureInfo.CurrentUICulture.LCID switch - { - 1030 => da_DK, - 6 => da, - _ => fallback - }; -} -"; - errorsAndWarnings.Should().BeNullOrEmpty(); - generatedFileName.Should().Be("Aigamo.ResXGenerator.1030_6.g.cs"); - sourceCode.Should().Be(expected); + const string expected = """ + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace Aigamo.ResXGenerator; + internal static partial class Helpers + { + public static string GetString_1030_6(string fallback, string da_DK, string da) => System.Globalization.CultureInfo.CurrentUICulture.LCID switch + { + 1030 => da_DK, + 6 => da, + _ => fallback + }; + } + + """; + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.FileName.Should().Be("Aigamo.ResXGenerator.1030_6.g.cs"); + result.SourceCode.Should().Be(expected); } [Fact] public void CanGenerateEmptyCombo() { - var (generatedFileName, sourceCode, errorsAndWarnings) = new StringBuilderGenerator().Generate(new CultureInfoCombo(), default); - var expected = @"// ------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// ------------------------------------------------------------------------------ -#nullable enable -namespace Aigamo.ResXGenerator; -internal static partial class Helpers -{ - public static string GetString_(string fallback) => System.Globalization.CultureInfo.CurrentUICulture.LCID switch - { - _ => fallback - }; -} -"; - errorsAndWarnings.Should().BeNullOrEmpty(); - generatedFileName.Should().Be("Aigamo.ResXGenerator..g.cs"); - sourceCode.Should().Be(expected); + var result = new ComboGenerator().Generate(new CultureInfoCombo(), CancellationToken.None); + const string expected = """ + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace Aigamo.ResXGenerator; + internal static partial class Helpers + { + public static string GetString_(string fallback) => System.Globalization.CultureInfo.CurrentUICulture.LCID switch + { + _ => fallback + }; + } + + """; + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.FileName.Should().Be("Aigamo.ResXGenerator..g.cs"); + result.SourceCode.Should().Be(expected); } } diff --git a/Aigamo.ResXGenerator.Tests/SettingsTests.cs b/Aigamo.ResXGenerator.Tests/SettingsTests.cs index 87c9c33..ee1efb9 100644 --- a/Aigamo.ResXGenerator.Tests/SettingsTests.cs +++ b/Aigamo.ResXGenerator.Tests/SettingsTests.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Aigamo.ResXGenerator.Tools; using FluentAssertions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; @@ -84,7 +85,7 @@ public void GlobalSettings_CanReadAll() [Fact] public void FileDefaults() { - var fileOptions = FileOptions.Select( + var fileOptions = GenFileOptions.Select( file: new GroupedAdditionalFile( mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), subFiles: Array.Empty() @@ -131,7 +132,7 @@ public void FileSettings_RespectsEmptyRootNamespace( string expectedEmbeddedFilename ) { - var fileOptions = FileOptions.Select( + var fileOptions = GenFileOptions.Select( file: new GroupedAdditionalFile( mainFile: new AdditionalTextWithHash(new AdditionalTextStub(mainFile), Guid.NewGuid()), subFiles: Array.Empty() @@ -173,7 +174,7 @@ string expectedEmbeddedFilename [Fact] public void File_PostFix() { - var fileOptions = FileOptions.Select( + var fileOptions = GenFileOptions.Select( file: new GroupedAdditionalFile( mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), subFiles: Array.Empty() @@ -191,7 +192,7 @@ public void File_PostFix() [Fact] public void FileSettings_CanReadAll() { - var fileOptions = FileOptions.Select( + var fileOptions = GenFileOptions.Select( file: new GroupedAdditionalFile( mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), subFiles: Array.Empty() @@ -255,7 +256,7 @@ public void FileSettings_RespectsGlobalDefaults() ), token: default ); - var fileOptions = FileOptions.Select( + var fileOptions = GenFileOptions.Select( file: new GroupedAdditionalFile( mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), subFiles: Array.Empty() @@ -285,7 +286,7 @@ public void FileSettings_RespectsGlobalDefaults() [Fact] public void FileSettings_CanSkipIndividualFile() { - var fileOptions = FileOptions.Select( + var fileOptions = GenFileOptions.Select( file: new GroupedAdditionalFile( mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), subFiles: Array.Empty() diff --git a/Aigamo.ResXGenerator.sln.DotSettings b/Aigamo.ResXGenerator.sln.DotSettings index 8f42476..ee9862a 100644 --- a/Aigamo.ResXGenerator.sln.DotSettings +++ b/Aigamo.ResXGenerator.sln.DotSettings @@ -193,4 +193,11 @@ </TypePattern> </Patterns> <Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb" /> - True \ No newline at end of file + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="s_" Suffix="" Style="aaBb" /></Policy> + True + True + True + True + True + True + True \ No newline at end of file diff --git a/Aigamo.ResXGenerator/Constants.cs b/Aigamo.ResXGenerator/Constants.cs deleted file mode 100644 index 312014c..0000000 --- a/Aigamo.ResXGenerator/Constants.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Aigamo.ResXGenerator; - -internal static class Constants -{ - public const string SystemDiagnosticsCodeAnalysis = - $"{nameof(System)}.{nameof(System.Diagnostics)}.{nameof(System.Diagnostics.CodeAnalysis)}"; - - public const string SystemGlobalization = $"{nameof(System)}.{nameof(System.Globalization)}"; - public const string SystemResources = $"{nameof(System)}.{nameof(System.Resources)}"; - - public const string AutoGeneratedHeader = - @"// ------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// ------------------------------------------------------------------------------"; - - public const string s_resourceManagerVariable = "s_resourceManager"; - public const string ResourceManagerVariable = "ResourceManager"; - public const string CultureInfoVariable = "CultureInfo"; -} diff --git a/Aigamo.ResXGenerator/Extensions/EnumerableExtensions.cs b/Aigamo.ResXGenerator/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..c31644e --- /dev/null +++ b/Aigamo.ResXGenerator/Extensions/EnumerableExtensions.cs @@ -0,0 +1,13 @@ +namespace Aigamo.ResXGenerator.Extensions; +public static class EnumerableExtensions +{ + public static void ForEach(this IEnumerable col, Action action) + { + foreach (var i in col) action(i); + } + + public static void ForEach(this IEnumerable> col, Action action) + { + foreach (var i in col) action(i.Item1, i.Item2, i.Item3); + } +} diff --git a/Aigamo.ResXGenerator/Extensions/StringExtensions.cs b/Aigamo.ResXGenerator/Extensions/StringExtensions.cs new file mode 100644 index 0000000..ef1fab5 --- /dev/null +++ b/Aigamo.ResXGenerator/Extensions/StringExtensions.cs @@ -0,0 +1,15 @@ +using System.Web; +using Aigamo.ResXGenerator.Tools; + +namespace Aigamo.ResXGenerator.Extensions; + +internal static class StringExtensions +{ + public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrEmpty(value); + + public static string ToXmlCommentSafe(this string input, string indent) + { + var lines = HttpUtility.HtmlEncode(input.Trim())?.Split(["\n\r", "\r", "\n"], StringSplitOptions.None) ?? []; + return string.Join($"{Environment.NewLine}{indent}/// ", lines); + } +} diff --git a/Aigamo.ResXGenerator/Generators/ComboGenerator.cs b/Aigamo.ResXGenerator/Generators/ComboGenerator.cs new file mode 100644 index 0000000..978fc76 --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/ComboGenerator.cs @@ -0,0 +1,82 @@ +using System.Globalization; +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Tools; + +namespace Aigamo.ResXGenerator.Generators; + +public sealed class ComboGenerator : StringBuilderGenerator, IComboGenerator +{ + private const string OutputStringFilenameFormat = "Aigamo.ResXGenerator.{0}.g.cs"; + private static readonly Dictionary> s_allChildren = new(); + + /// + /// Build all CultureInfo children + /// + static ComboGenerator() + { + var all = CultureInfo.GetCultures(CultureTypes.AllCultures); + + all.ForEach(cultureInfo => + { + if (cultureInfo.LCID == 4096 || cultureInfo.IsNeutralCulture || cultureInfo.Name.IsNullOrEmpty()) + return; + + var parent = cultureInfo.Parent; + if (!s_allChildren.TryGetValue(parent.LCID, out var v)) + s_allChildren[parent.LCID] = v = []; + v.Add(cultureInfo.LCID); + }); + } + + public override GeneratedOutput Generate(CultureInfoCombo options, CancellationToken cancellationToken = default) + { + var definedLanguages = options.GetDefinedLanguages(); + var builder = GetBuilder("Aigamo.ResXGenerator"); + + builder.AppendLine("internal static partial class Helpers"); + builder.AppendLine("{"); + + builder.Append(" public static string GetString_"); + var functionNamePostFix = FunctionNamePostFix(definedLanguages); + builder.Append(functionNamePostFix); + builder.Append("(string fallback"); + definedLanguages.ForEach((name, _, _) => + { + builder.Append(", "); + builder.Append("string "); + builder.Append(name); + }); + + builder.Append(") => "); + builder.Append(Constants.SystemGlobalization); + builder.AppendLine(".CultureInfo.CurrentUICulture.LCID switch"); + builder.AppendLine(" {"); + var already = new HashSet(); + definedLanguages.ForEach((name, lcid, _) => + { + var findParents = FindParents(lcid).Except(already).ToList(); + findParents + .Select(parent => + { + already.Add(parent); + return $" {parent} => {name.Replace('-', '_')},"; + }) + .ForEach(l => builder.AppendLine(l)); + }); + + builder.AppendLine(" _ => fallback"); + builder.AppendLine(" };"); + builder.AppendLine("}"); + + return new GeneratedOutput(string.Format(OutputStringFilenameFormat, functionNamePostFix), builder.ToString()); + } + + public string GeneratedFileName(CultureInfoCombo combo) + { + var definedLanguages = combo.GetDefinedLanguages(); + var functionNamePostFix = FunctionNamePostFix(definedLanguages); + return string.Format(OutputStringFilenameFormat, functionNamePostFix) ; + } + + private static IEnumerable FindParents(int toFind) => s_allChildren.TryGetValue(toFind, out var v) ? v.Prepend(toFind) : [toFind]; +} diff --git a/Aigamo.ResXGenerator/Generators/GeneratedOutput.cs b/Aigamo.ResXGenerator/Generators/GeneratedOutput.cs new file mode 100644 index 0000000..971e8ac --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/GeneratedOutput.cs @@ -0,0 +1,14 @@ +using Microsoft.CodeAnalysis; + +namespace Aigamo.ResXGenerator.Generators; +public class GeneratedOutput(string fileName, string sourceCode, IEnumerable errorsAndWarnings) +{ + public GeneratedOutput(string fileName, string sourceCode) : this(fileName, sourceCode, []) + { + + } + + public string FileName { get; internal set; } = fileName; + public string SourceCode { get; internal set; } = sourceCode; + public IEnumerable ErrorsAndWarnings { get; internal set; } = errorsAndWarnings; +} diff --git a/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs b/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs new file mode 100644 index 0000000..466febe --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs @@ -0,0 +1,234 @@ +using System.Collections.Immutable; +using System.Text; +using System.Xml; +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Tools; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; + +namespace Aigamo.ResXGenerator.Generators; + +public sealed class ResourceManagerGenerator : StringBuilderGenerator, IResXGenerator +{ + public override GeneratedOutput Generate(GenFileOptions options, CancellationToken cancellationToken = default) + { + var errorsAndWarnings = new List(); + var generatedFileName = $"{options.LocalNamespace}.{options.ClassName}.g.cs"; + + var content = options.GroupedFile.MainFile.File.GetText(cancellationToken); + if (content is null) return new GeneratedOutput (options.GroupedFile.MainFile.File.Path, "//ERROR reading file:", errorsAndWarnings); + + // HACK: netstandard2.0 doesn't support improved interpolated strings? + var builder = GetBuilder(options.CustomToolNamespace ?? options.LocalNamespace); + + if (options.GenerateCode) + AppendCodeUsings(builder); + else + AppendResourceManagerUsings(builder); + + builder.Append(options.PublicClass ? "public" : "internal"); + builder.Append(options.StaticClass ? " static" : string.Empty); + builder.Append(options.PartialClass ? " partial class " : " class "); + builder.AppendLine(options.ClassName); + builder.AppendLine("{"); + + var indent = " "; + string containerClassName = options.ClassName; + + if (options.InnerClassVisibility != InnerClassVisibility.NotGenerated) + { + containerClassName = string.IsNullOrEmpty(options.InnerClassName) ? "Resources" : options.InnerClassName; + if (!string.IsNullOrEmpty(options.InnerClassInstanceName)) + { + if (options.StaticClass || options.StaticMembers) + { + errorsAndWarnings.Add(Diagnostic.Create( + descriptor: Rules.MemberWithStaticError, + location: Location.Create( + filePath: options.GroupedFile.MainFile.File.Path, + textSpan: new TextSpan(), + lineSpan: new LinePositionSpan() + ) + )); + } + + builder.Append(indent); + builder.Append("public "); + builder.Append(containerClassName); + builder.Append(" "); + builder.Append(options.InnerClassInstanceName); + builder.AppendLine(" { get; } = new();"); + builder.AppendLine(); + } + + builder.Append(indent); + builder.Append(GetInnerClassVisibility(options)); + builder.Append(options.StaticClass ? " static" : string.Empty); + builder.Append(options.PartialClass ? " partial class " : " class "); + + builder.AppendLine(containerClassName); + builder.Append(indent); + builder.AppendLine("{"); + + indent += " "; + } + + if (options.GenerateCode) + GenerateCode(options, content, indent, containerClassName, builder, errorsAndWarnings, cancellationToken); + else + GenerateResourceManager(options, content, indent, containerClassName, builder, errorsAndWarnings, cancellationToken); + + if (options.InnerClassVisibility != InnerClassVisibility.NotGenerated) + { + builder.AppendLine(" }"); + } + + builder.AppendLine("}"); + + return new GeneratedOutput(generatedFileName, builder.ToString(), errorsAndWarnings); + } + + private static void GenerateCode( + GenFileOptions options, + SourceText content, + string indent, + string containerClassName, + StringBuilder builder, + List errorsAndWarnings, + CancellationToken cancellationToken) + { + var combo = new CultureInfoCombo(options.GroupedFile.SubFiles); + var definedLanguages = combo.GetDefinedLanguages(); + + var fallback = ReadResxFile(content); + var subfiles = definedLanguages.Select(lang => + { + var subcontent = lang.FileWithHash.File.GetText(cancellationToken); + return subcontent is null + ? null + : ReadResxFile(subcontent)? + .GroupBy(x => x.key) + .ToImmutableDictionary(x => x.Key, x => x.First().value); + }).ToList(); + if (fallback is null || subfiles.Any(x => x is null)) + { + builder.AppendFormat("//could not read {0} or one of its children", options.GroupedFile.MainFile.File.Path); + return; + } + + var alreadyAddedMembers = new HashSet(); + fallback.ForEach((key, value, line) => + { + cancellationToken.ThrowIfCancellationRequested(); + if ( + !GenerateMember( + indent, + builder, + options, + key, + value, + line, + alreadyAddedMembers, + errorsAndWarnings, + containerClassName, + out _ + ) + ) return; + + builder.Append(" => GetString_"); + builder.Append(FunctionNamePostFix(definedLanguages)); + builder.Append("("); + builder.Append(SymbolDisplay.FormatLiteral(value, true)); + + subfiles.ForEach(xml => + { + builder.Append(", "); + if (!xml!.TryGetValue(key, out var langValue)) + langValue = value; + builder.Append(SymbolDisplay.FormatLiteral(langValue, true)); + }); + + builder.AppendLine(");"); + }); + } + + private static void GenerateResourceManager( + GenFileOptions options, + SourceText content, + string indent, + string containerClassName, + StringBuilder builder, + List errorsAndWarnings, + CancellationToken cancellationToken) + { + GenerateResourceManagerMembers(builder, indent, containerClassName, options); + + var members = ReadResxFile(content); + if (members is null) + { + return; + } + + var alreadyAddedMembers = new HashSet { Constants.CultureInfoVariable }; + members.ForEach((key, value, line) => + { + cancellationToken.ThrowIfCancellationRequested(); + CreateMember( + indent, + builder, + options, + key, + value, + line, + alreadyAddedMembers, + errorsAndWarnings, + containerClassName + ); + }); + } + + private static void CreateMember( + string indent, + StringBuilder builder, + GenFileOptions options, + string name, + string value, + IXmlLineInfo line, + HashSet alreadyAddedMembers, + List errorsAndWarnings, + string containerClassName + ) + { + if (!GenerateMember(indent, builder, options, name, value, line, alreadyAddedMembers, errorsAndWarnings, containerClassName, out var resourceAccessByName)) + { + return; + } + + if (resourceAccessByName) + { + builder.Append(" => ResourceManager.GetString(nameof("); + builder.Append(name); + builder.Append("), "); + } + else + { + builder.Append(@" => ResourceManager.GetString("""); + builder.Append(name.Replace(@"""", @"\""")); + builder.Append(@""", "); + } + + builder.Append(Constants.CultureInfoVariable); + builder.Append(")"); + builder.Append(options.NullForgivingOperators ? "!" : null); + builder.AppendLine(";"); + } + + private static string GetInnerClassVisibility(GenFileOptions options) + { + if (options.InnerClassVisibility == InnerClassVisibility.SameAsOuter) + return options.PublicClass ? "public" : "internal"; + + return options.InnerClassVisibility.ToString().ToLowerInvariant(); + } +} diff --git a/Aigamo.ResXGenerator/Generators/StringBuilderGenerator.cs b/Aigamo.ResXGenerator/Generators/StringBuilderGenerator.cs new file mode 100644 index 0000000..3e8553a --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/StringBuilderGenerator.cs @@ -0,0 +1,183 @@ +using System.Globalization; +using System.Resources; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Linq; +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Tools; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Aigamo.ResXGenerator.Generators; + +public abstract class StringBuilderGenerator : IGenerator +{ + private static Location GetMemberLocation(GenFileOptions fileOptions, IXmlLineInfo line, string memberName) => + Location.Create( + filePath: fileOptions.GroupedFile.MainFile.File.Path, + textSpan: new TextSpan(), + lineSpan: new LinePositionSpan( + start: new LinePosition(line.LineNumber - 1, line.LinePosition - 1), + end: new LinePosition(line.LineNumber - 1, line.LinePosition - 1 + memberName.Length) + ) + ); + + public abstract GeneratedOutput Generate(T options, CancellationToken cancellationToken = default); + + protected static StringBuilder GetBuilder(string withNamespace) + { + var builder = new StringBuilder(); + + builder.AppendLine(Constants.AutoGeneratedHeader); + builder.AppendLine("#nullable enable"); + + builder.Append("namespace "); + builder.Append(withNamespace); + builder.AppendLine(";"); + + return builder; + } + + protected static bool GenerateMember( + string indent, + StringBuilder builder, + GenFileOptions options, + string name, + string neutralValue, + IXmlLineInfo line, + HashSet alreadyAddedMembers, + List errorsAndWarnings, + string containerClassName, + out bool resourceAccessByName + ) //Check + { + string memberName; + + if (RegexDefinitions.ValidMemberNamePattern.IsMatch(name)) + { + memberName = name; + resourceAccessByName = true; + } + else + { + memberName = RegexDefinitions.InvalidMemberNameSymbols.Replace(name, "_"); + resourceAccessByName = false; + } + + if (!alreadyAddedMembers.Add(memberName)) + { + errorsAndWarnings.Add(Diagnostic.Create( + descriptor: Rules.DuplicateWarning, + location: GetMemberLocation(options, line, memberName), memberName + )); + return false; + } + + if (memberName == containerClassName) + { + errorsAndWarnings.Add(Diagnostic.Create( + descriptor: Rules.MemberSameAsClassWarning, + location: GetMemberLocation(options, line, memberName), memberName + )); + return false; + } + + builder.AppendLine(); + + builder.Append(indent); + builder.AppendLine("/// "); + + builder.Append(indent); + builder.Append("/// Looks up a localized string similar to "); + builder.Append(neutralValue.ToXmlCommentSafe(indent)); + builder.AppendLine("."); + + builder.Append(indent); + builder.AppendLine("/// "); + + builder.Append(indent); + builder.Append("public "); + builder.Append(options.StaticMembers ? "static " : string.Empty); + builder.Append("string"); + builder.Append(options.NullForgivingOperators ? null : "?"); + builder.Append(" "); + builder.Append(memberName); + return true; + } + + protected static IEnumerable<(string key, string value, IXmlLineInfo line)>? ReadResxFile(SourceText content) + { + using var reader = new StringReader(content.ToString()); + + if (XDocument.Load(reader, LoadOptions.SetLineInfo).Root is { } element) + return element + .Descendants() + .Where(static data => data.Name == "data") + .Select(static data => ( + key: data.Attribute("name")!.Value, + value: data.Descendants("value").First().Value, + line: (IXmlLineInfo)data.Attribute("name")! + )); + + return null; + } + protected static void AppendResourceManagerUsings(StringBuilder builder) + { + builder.Append("using "); + builder.Append(Constants.SystemGlobalization); + builder.AppendLine(";"); + + builder.Append("using "); + builder.Append(Constants.SystemResources); + builder.AppendLine(";"); + + builder.AppendLine(); + } + + protected static void AppendCodeUsings(StringBuilder builder) + { + builder.AppendLine("using static Aigamo.ResXGenerator.Helpers;"); + builder.AppendLine(); + } + + protected static void GenerateResourceManagerMembers( + StringBuilder builder, + string indent, + string containerClassName, + GenFileOptions options + ) + { + builder.Append(indent); + builder.Append("private static "); + builder.Append(nameof(ResourceManager)); + builder.Append("? "); + builder.Append(Constants.SResourceManagerVariable); + builder.AppendLine(";"); + + builder.Append(indent); + builder.Append("public static "); + builder.Append(nameof(ResourceManager)); + builder.Append(" "); + builder.Append(Constants.ResourceManagerVariable); + builder.Append(" => "); + builder.Append(Constants.SResourceManagerVariable); + builder.Append(" ??= new "); + builder.Append(nameof(ResourceManager)); + builder.Append("(\""); + builder.Append(options.EmbeddedFilename); + builder.Append("\", typeof("); + builder.Append(containerClassName); + builder.AppendLine(").Assembly);"); + + builder.Append(indent); + builder.Append("public "); + builder.Append(options.StaticMembers ? "static " : string.Empty); + builder.Append(nameof(CultureInfo)); + builder.Append("? "); + builder.Append(Constants.CultureInfoVariable); + builder.AppendLine(" { get; set; }"); + } + + protected static string FunctionNamePostFix(IReadOnlyList<(string Name, int LCID, AdditionalTextWithHash FileWithHash)>? definedLanguages) => string.Join("_", definedLanguages?.Select(x => x.LCID) ?? []); +} diff --git a/Aigamo.ResXGenerator/IGenerator.cs b/Aigamo.ResXGenerator/IGenerator.cs deleted file mode 100644 index 4e5fd09..0000000 --- a/Aigamo.ResXGenerator/IGenerator.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace Aigamo.ResXGenerator; - -public interface IGenerator -{ - /// - /// Generate source file with properties for each translated resource - /// - (string GeneratedFileName, string SourceCode, IEnumerable ErrorsAndWarnings) - Generate(FileOptions options, CancellationToken cancellationToken = default); - - /// - /// Generate helper functions to determine which translated resource to use in the current moment - /// - (string GeneratedFileName, string SourceCode, IEnumerable ErrorsAndWarnings) - Generate(CultureInfoCombo combo, CancellationToken cancellationToken); -} diff --git a/Aigamo.ResXGenerator/Rules.cs b/Aigamo.ResXGenerator/Rules.cs new file mode 100644 index 0000000..8c5c941 --- /dev/null +++ b/Aigamo.ResXGenerator/Rules.cs @@ -0,0 +1,42 @@ +using Microsoft.CodeAnalysis; + +namespace Aigamo.ResXGenerator; + +internal static class Rules +{ + public static readonly DiagnosticDescriptor DuplicateWarning = new( + id: "AigamoResXGenerator001", + title: "Duplicate member", + messageFormat: "Ignored added member '{0}'", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor MemberSameAsClassWarning = new( + id: "AigamoResXGenerator002", + title: "Member same name as class", + messageFormat: "Ignored member '{0}' has same name as class", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor MemberWithStaticError = new( + id: "AigamoResXGenerator003", + title: "Incompatible settings", + messageFormat: "Cannot have static members/class with an class instance", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + public static DiagnosticDescriptor FatalError(string resourceDetail, Exception exception) => new( + id: "AigamoResXGenerator999", + title: "Fatal Error generated", + messageFormat: $"An error occured on generation file {resourceDetail} error {exception.Message}", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); +} diff --git a/Aigamo.ResXGenerator/SourceGenerator.cs b/Aigamo.ResXGenerator/SourceGenerator.cs index 370a69a..2765d77 100644 --- a/Aigamo.ResXGenerator/SourceGenerator.cs +++ b/Aigamo.ResXGenerator/SourceGenerator.cs @@ -1,11 +1,15 @@ -using Microsoft.CodeAnalysis; +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Generators; +using Aigamo.ResXGenerator.Tools; +using Microsoft.CodeAnalysis; namespace Aigamo.ResXGenerator; [Generator] public class SourceGenerator : IIncrementalGenerator { - private static readonly IGenerator s_generator = new StringBuilderGenerator(); + private IComboGenerator ComboGenerator { get; } = new ComboGenerator(); + private IResXGenerator ResXGenerator { get; } = new ResourceManagerGenerator(); public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -20,36 +24,50 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var inputs = monitor .Combine(globalOptions) .Combine(context.AnalyzerConfigOptionsProvider) - .Select(static (x, _) => FileOptions.Select( + .Select(static (x, _) => GenFileOptions.Select( file: x.Left.Left, options: x.Right, globalOptions: x.Left.Right )) - .Where(static x => x.IsValid && !x.SkipFile); + .Where(static x => x is { IsValid: true, SkipFile: false }); - context.RegisterSourceOutput(inputs, (ctx, file) => + GenerateResXFiles(context, inputs); + GenerateResxCombos(context, monitor); + } + + private void GenerateResxCombos(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider monitor) + { + var detectAllCombosOfResx = monitor.Collect().SelectMany((x, _) => GroupResxFiles.DetectChildCombos(x)); + context.RegisterSourceOutput(detectAllCombosOfResx, (ctx, combo) => { - var (generatedFileName, sourceCode, errorsAndWarnings) = - s_generator.Generate(file, ctx.CancellationToken); - foreach (var sourceErrorsAndWarning in errorsAndWarnings) + try { - ctx.ReportDiagnostic(sourceErrorsAndWarning); + var output = ComboGenerator.Generate(combo, ctx.CancellationToken); + output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); + ctx.AddSource(output.FileName, output.SourceCode); + } + catch (Exception e) + { + ctx.ReportDiagnostic(Diagnostic.Create(Rules.FatalError(ComboGenerator.GeneratedFileName(combo), e), Location.None)); } - - ctx.AddSource(generatedFileName, sourceCode); }); + } - var detectAllCombosOfResx = monitor.Collect().SelectMany((x, _) => GroupResxFiles.DetectChildCombos(x)); - context.RegisterSourceOutput(detectAllCombosOfResx, (ctx, combo) => + private void GenerateResXFiles(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) + { + context.RegisterSourceOutput(inputs, (ctx, file) => { - var (generatedFileName, sourceCode, errorsAndWarnings) = - s_generator.Generate(combo, ctx.CancellationToken); - foreach (var sourceErrorsAndWarning in errorsAndWarnings) + try + { + var output = ResXGenerator.Generate(file, ctx.CancellationToken); + output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); + ctx.AddSource(output.FileName, output.SourceCode); + } + catch (Exception e) { - ctx.ReportDiagnostic(sourceErrorsAndWarning); + ctx.ReportDiagnostic(Diagnostic.Create(Rules.FatalError(file.ClassName, e), Location.None)); } - ctx.AddSource(generatedFileName, sourceCode); }); } } diff --git a/Aigamo.ResXGenerator/StringBuilderExtensions.cs b/Aigamo.ResXGenerator/StringBuilderExtensions.cs deleted file mode 100644 index 5735bf2..0000000 --- a/Aigamo.ResXGenerator/StringBuilderExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Text; - -namespace Aigamo.ResXGenerator; - -internal static class StringBuilderExtensions -{ - public static void AppendLineLF(this StringBuilder builder) - { - builder.Append('\n'); - } - - public static void AppendLineLF(this StringBuilder builder, string value) - { - builder.Append(value); - builder.AppendLineLF(); - } -} diff --git a/Aigamo.ResXGenerator/StringBuilderGenerator.ComboGenerator.cs b/Aigamo.ResXGenerator/StringBuilderGenerator.ComboGenerator.cs deleted file mode 100644 index 612dcce..0000000 --- a/Aigamo.ResXGenerator/StringBuilderGenerator.ComboGenerator.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System.Collections.Immutable; -using System.Globalization; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; - -namespace Aigamo.ResXGenerator -{ - - public sealed partial class StringBuilderGenerator : IGenerator - { - static readonly Dictionary> s_allChildren = new(); - - /// - /// Build all CultureInfo children - /// - static StringBuilderGenerator() - { - var all = CultureInfo.GetCultures(CultureTypes.AllCultures); - - foreach (var cultureInfo in all) - { - if (cultureInfo.LCID == 4096 || cultureInfo.IsNeutralCulture || cultureInfo.Name.IsNullOrEmpty()) - { - continue; - } - var parent = cultureInfo.Parent; - if (!s_allChildren.TryGetValue(parent.LCID, out var v)) - s_allChildren[parent.LCID] = v = new List(); - v.Add(cultureInfo.LCID); - } - } - - public ( - string GeneratedFileName, - string SourceCode, - IEnumerable ErrorsAndWarnings - ) Generate( - CultureInfoCombo combo, - CancellationToken cancellationToken - ) - { - var definedLanguages = combo.GetDefinedLanguages(); - var builder = GetBuilder("Aigamo.ResXGenerator"); - - builder.AppendLineLF("internal static partial class Helpers"); - builder.AppendLineLF("{"); - - builder.Append(" public static string GetString_"); - var functionNamePostFix = FunctionNamePostFix(definedLanguages); - builder.Append(functionNamePostFix); - builder.Append("(string fallback"); - foreach (var (name, _, _) in definedLanguages) - { - builder.Append(", "); - builder.Append("string "); - builder.Append(name); - } - - builder.Append(") => "); - builder.Append(Constants.SystemGlobalization); - builder.AppendLineLF(".CultureInfo.CurrentUICulture.LCID switch"); - builder.AppendLineLF(" {"); - var already = new HashSet(); - foreach (var (name, lcid, _) in definedLanguages) - { - static IEnumerable FindParents(int toFind) - { - yield return toFind; - if (!s_allChildren.TryGetValue(toFind, out var v)) - { - yield break; - } - - foreach (var parents in v) - { - yield return parents; - } - } - - var findParents = FindParents(lcid).Except(already).ToList(); - foreach (var parent in findParents) - { - already.Add(parent); - builder.Append(" "); - builder.Append(parent); - builder.Append(" => "); - builder.Append(name.Replace('-', '_')); - builder.AppendLineLF(","); - } - } - - builder.AppendLineLF(" _ => fallback"); - builder.AppendLineLF(" };"); - builder.AppendLineLF("}"); - - return ( - GeneratedFileName: "Aigamo.ResXGenerator." + functionNamePostFix + ".g.cs", - SourceCode: builder.ToString(), - ErrorsAndWarnings: Array.Empty() - ); - } - - private static string FunctionNamePostFix( - IReadOnlyList<(string Name, int LCID, AdditionalTextWithHash FileWithHash)>? definedLanguages - ) => string.Join("_", definedLanguages?.Select(x => x.LCID) ?? Array.Empty()); - - private static void AppendCodeUsings(StringBuilder builder) - { - builder.AppendLineLF("using static Aigamo.ResXGenerator.Helpers;"); - builder.AppendLineLF(); - } - - private void GenerateCode( - FileOptions options, - SourceText content, - string indent, - string containerClassName, - StringBuilder builder, - List errorsAndWarnings, - CancellationToken cancellationToken - ) - { - var combo = new CultureInfoCombo(options.GroupedFile.SubFiles); - var definedLanguages = combo.GetDefinedLanguages(); - - var fallback = ReadResxFile(content); - var subfiles = definedLanguages.Select(lang => - { - var subcontent = lang.FileWithHash.File.GetText(cancellationToken); - return subcontent is null - ? null - : ReadResxFile(subcontent)? - .GroupBy(x => x.key) - .ToImmutableDictionary(x => x.Key, x => x.First().value); - }).ToList(); - if (fallback is null || subfiles.Any(x => x is null)) - { - builder.AppendFormat("//could not read {0} or one of its children", options.GroupedFile.MainFile.File.Path); - return; - } - - var alreadyAddedMembers = new HashSet(); - foreach (var (key, value, line) in fallback) - { - cancellationToken.ThrowIfCancellationRequested(); - if ( - !GenerateMember( - indent, - builder, - options, - key, - value, - line, - alreadyAddedMembers, - errorsAndWarnings, - containerClassName, - out _ - ) - ) - { - continue; - } - - builder.Append(" => GetString_"); - builder.Append(FunctionNamePostFix(definedLanguages)); - builder.Append("("); - builder.Append(SymbolDisplay.FormatLiteral(value, true)); - - foreach (var xml in subfiles) - { - builder.Append(", "); - if (!xml!.TryGetValue(key, out var langValue)) - langValue = value; - builder.Append(SymbolDisplay.FormatLiteral(langValue, true)); - } - - builder.AppendLineLF(");"); - } - } - } -} diff --git a/Aigamo.ResXGenerator/StringBuilderGenerator.ResourceManager.cs b/Aigamo.ResXGenerator/StringBuilderGenerator.ResourceManager.cs deleted file mode 100644 index 741cfd5..0000000 --- a/Aigamo.ResXGenerator/StringBuilderGenerator.ResourceManager.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System.Globalization; -using System.Resources; -using System.Text; -using System.Xml; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; - -namespace Aigamo.ResXGenerator; - -public sealed partial class StringBuilderGenerator : IGenerator -{ - private void GenerateResourceManager( - FileOptions options, - SourceText content, - string indent, - string containerClassName, - StringBuilder builder, - List errorsAndWarnings, - CancellationToken cancellationToken - ) - { - GenerateResourceManagerMembers(builder, indent, containerClassName, options); - - var members = ReadResxFile(content); - if (members is null) - { - return; - } - - var alreadyAddedMembers = new HashSet() { Constants.CultureInfoVariable }; - foreach (var (key, value, line) in members) - { - cancellationToken.ThrowIfCancellationRequested(); - CreateMember( - indent, - builder, - options, - key, - value, - line, - alreadyAddedMembers, - errorsAndWarnings, - containerClassName - ); - } - } - - private static void CreateMember( - string indent, - StringBuilder builder, - FileOptions options, - string name, - string value, - IXmlLineInfo line, - HashSet alreadyAddedMembers, - List errorsAndWarnings, - string containerclassname - ) - { - if (!GenerateMember(indent, builder, options, name, value, line, alreadyAddedMembers, errorsAndWarnings, containerclassname, out var resourceAccessByName)) - { - return; - } - - if (resourceAccessByName) - { - builder.Append(" => ResourceManager.GetString(nameof("); - builder.Append(name); - builder.Append("), "); - } - else - { - builder.Append(@" => ResourceManager.GetString("""); - builder.Append(name.Replace(@"""", @"\""")); - builder.Append(@""", "); - } - - builder.Append(Constants.CultureInfoVariable); - builder.Append(")"); - builder.Append(options.NullForgivingOperators ? "!" : null); - builder.AppendLineLF(";"); - } - - private static void AppendResourceManagerUsings(StringBuilder builder) - { - builder.Append("using "); - builder.Append(Constants.SystemGlobalization); - builder.AppendLineLF(";"); - - builder.Append("using "); - builder.Append(Constants.SystemResources); - builder.AppendLineLF(";"); - - builder.AppendLineLF(); - } - - private static void GenerateResourceManagerMembers( - StringBuilder builder, - string indent, - string containerClassName, - FileOptions options - ) - { - builder.Append(indent); - builder.Append("private static "); - builder.Append(nameof(ResourceManager)); - builder.Append("? "); - builder.Append(Constants.s_resourceManagerVariable); - builder.AppendLineLF(";"); - - builder.Append(indent); - builder.Append("public static "); - builder.Append(nameof(ResourceManager)); - builder.Append(" "); - builder.Append(Constants.ResourceManagerVariable); - builder.Append(" => "); - builder.Append(Constants.s_resourceManagerVariable); - builder.Append(" ??= new "); - builder.Append(nameof(ResourceManager)); - builder.Append("(\""); - builder.Append(options.EmbeddedFilename); - builder.Append("\", typeof("); - builder.Append(containerClassName); - builder.AppendLineLF(").Assembly);"); - - builder.Append(indent); - builder.Append("public "); - builder.Append(options.StaticMembers ? "static " : string.Empty); - builder.Append(nameof(CultureInfo)); - builder.Append("? "); - builder.Append(Constants.CultureInfoVariable); - builder.AppendLineLF(" { get; set; }"); - } -} diff --git a/Aigamo.ResXGenerator/StringBuilderGenerator.cs b/Aigamo.ResXGenerator/StringBuilderGenerator.cs deleted file mode 100644 index 8997c13..0000000 --- a/Aigamo.ResXGenerator/StringBuilderGenerator.cs +++ /dev/null @@ -1,251 +0,0 @@ -using System.Text; -using System.Text.RegularExpressions; -using System.Web; -using System.Xml; -using System.Xml.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; - -namespace Aigamo.ResXGenerator; - -public sealed partial class StringBuilderGenerator : IGenerator -{ - private static readonly Regex s_validMemberNamePattern = new( - pattern: @"^[\p{L}\p{Nl}_][\p{Cf}\p{L}\p{Mc}\p{Mn}\p{Nd}\p{Nl}\p{Pc}]*$", - options: RegexOptions.Compiled | RegexOptions.CultureInvariant - ); - - private static readonly Regex s_invalidMemberNameSymbols = new( - pattern: @"[^\p{Cf}\p{L}\p{Mc}\p{Mn}\p{Nd}\p{Nl}\p{Pc}]", - options: RegexOptions.Compiled | RegexOptions.CultureInvariant - ); - - private static readonly DiagnosticDescriptor s_duplicateWarning = new( - id: "AigamoResXGenerator001", - title: "Duplicate member", - messageFormat: "Ignored added member '{0}'", - category: "ResXGenerator", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); - - private static readonly DiagnosticDescriptor s_memberSameAsClassWarning = new( - id: "AigamoResXGenerator002", - title: "Member same name as class", - messageFormat: "Ignored member '{0}' has same name as class", - category: "ResXGenerator", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); - - private static readonly DiagnosticDescriptor s_memberWithStaticError = new( - id: "AigamoResXGenerator003", - title: "Incompatible settings", - messageFormat: "Cannot have static members/class with an class instance", - category: "ResXGenerator", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true - ); - - public ( - string GeneratedFileName, - string SourceCode, - IEnumerable ErrorsAndWarnings - ) Generate( - FileOptions options, - CancellationToken cancellationToken = default - ) - { - var errorsAndWarnings = new List(); - var generatedFileName = $"{options.LocalNamespace}.{options.ClassName}.g.cs"; - - var content = options.GroupedFile.MainFile.File.GetText(cancellationToken); - if (content is null) return (generatedFileName, "//ERROR reading file:" + options.GroupedFile.MainFile.File.Path, errorsAndWarnings); - - // HACK: netstandard2.0 doesn't support improved interpolated strings? - var builder = GetBuilder(options.CustomToolNamespace ?? options.LocalNamespace); - - if (options.GenerateCode) - AppendCodeUsings(builder); - else - AppendResourceManagerUsings(builder); - - builder.Append(options.PublicClass ? "public" : "internal"); - builder.Append(options.StaticClass ? " static" : string.Empty); - builder.Append(options.PartialClass ? " partial class " : " class "); - builder.AppendLineLF(options.ClassName); - builder.AppendLineLF("{"); - - var indent = " "; - string containerClassName = options.ClassName; - - if (options.InnerClassVisibility != InnerClassVisibility.NotGenerated) - { - containerClassName = string.IsNullOrEmpty(options.InnerClassName) ? "Resources" : options.InnerClassName; - if (!string.IsNullOrEmpty(options.InnerClassInstanceName)) - { - if (options.StaticClass || options.StaticMembers) - { - errorsAndWarnings.Add(Diagnostic.Create( - descriptor: s_memberWithStaticError, - location: Location.Create( - filePath: options.GroupedFile.MainFile.File.Path, - textSpan: new TextSpan(), - lineSpan: new LinePositionSpan() - ) - )); - } - - builder.Append(indent); - builder.Append("public "); - builder.Append(containerClassName); - builder.Append(" "); - builder.Append(options.InnerClassInstanceName); - builder.AppendLineLF(" { get; } = new();"); - builder.AppendLineLF(); - } - - builder.Append(indent); - builder.Append(options.InnerClassVisibility == InnerClassVisibility.SameAsOuter - ? options.PublicClass ? "public" : "internal" - : options.InnerClassVisibility.ToString().ToLowerInvariant()); - builder.Append(options.StaticClass ? " static" : string.Empty); - builder.Append(options.PartialClass ? " partial class " : " class "); - - builder.AppendLineLF(containerClassName); - builder.Append(indent); - builder.AppendLineLF("{"); - - indent += " "; - - } - - if (options.GenerateCode) - GenerateCode(options, content, indent, containerClassName, builder, errorsAndWarnings, cancellationToken); - else - GenerateResourceManager(options, content, indent, containerClassName, builder, errorsAndWarnings, cancellationToken); - - if (options.InnerClassVisibility != InnerClassVisibility.NotGenerated) - { - builder.AppendLineLF(" }"); - } - - builder.AppendLineLF("}"); - - return ( - GeneratedFileName: generatedFileName, - SourceCode: builder.ToString(), - ErrorsAndWarnings: errorsAndWarnings - ); - } - - private static IEnumerable<(string key, string value, IXmlLineInfo line)>? ReadResxFile(SourceText content) - { - using var reader = new StringReader(content.ToString()); - - if (XDocument.Load(reader, LoadOptions.SetLineInfo).Root is { } element) - return element - .Descendants() - .Where(static data => data.Name == "data") - .Select(static data => ( - key: data.Attribute("name")!.Value, - value: data.Descendants("value").First().Value, - line: (IXmlLineInfo)data.Attribute("name")! - )); - - return null; - } - - private static bool GenerateMember( - string indent, - StringBuilder builder, - FileOptions options, - string name, - string neutralValue, - IXmlLineInfo line, - HashSet alreadyAddedMembers, - List errorsAndWarnings, - string containerClassName, - out bool resourceAccessByName - ) - { - string memberName; - - if (s_validMemberNamePattern.IsMatch(name)) - { - memberName = name; - resourceAccessByName = true; - } - else - { - memberName = s_invalidMemberNameSymbols.Replace(name, "_"); - resourceAccessByName = false; - } - - static Location GetMemberLocation(FileOptions fileOptions, IXmlLineInfo line, string memberName) => - Location.Create( - filePath: fileOptions.GroupedFile.MainFile.File.Path, - textSpan: new TextSpan(), - lineSpan: new LinePositionSpan( - start: new LinePosition(line.LineNumber - 1, line.LinePosition - 1), - end: new LinePosition(line.LineNumber - 1, line.LinePosition - 1 + memberName.Length) - ) - ); - - if (!alreadyAddedMembers.Add(memberName)) - { - errorsAndWarnings.Add(Diagnostic.Create( - descriptor: s_duplicateWarning, - location: GetMemberLocation(options, line, memberName), memberName - )); - return false; - } - - if (memberName == containerClassName) - { - errorsAndWarnings.Add(Diagnostic.Create( - descriptor: s_memberSameAsClassWarning, - location: GetMemberLocation(options, line, memberName), memberName - )); - return false; - } - - builder.AppendLineLF(); - - builder.Append(indent); - builder.AppendLineLF("/// "); - - builder.Append(indent); - builder.Append("/// Looks up a localized string similar to "); - builder.Append(HttpUtility.HtmlEncode(neutralValue.Trim().Replace("\r\n", "\n").Replace("\r", "\n") - .Replace("\n", Environment.NewLine + indent + "/// "))); - builder.AppendLineLF("."); - - builder.Append(indent); - builder.AppendLineLF("/// "); - - builder.Append(indent); - builder.Append("public "); - builder.Append(options.StaticMembers ? "static " : string.Empty); - builder.Append("string"); - builder.Append(options.NullForgivingOperators ? null : "?"); - builder.Append(" "); - builder.Append(memberName); - return true; - } - - private static StringBuilder GetBuilder(string withNamespace) - { - var builder = new StringBuilder(); - - builder.AppendLineLF(Constants.AutoGeneratedHeader); - builder.AppendLineLF("#nullable enable"); - - builder.Append("namespace "); - builder.Append(withNamespace); - builder.AppendLineLF(";"); - - return builder; - } - -} diff --git a/Aigamo.ResXGenerator/StringExtensions.cs b/Aigamo.ResXGenerator/StringExtensions.cs deleted file mode 100644 index 549eb67..0000000 --- a/Aigamo.ResXGenerator/StringExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Aigamo.ResXGenerator; - -internal static class StringExtensions -{ - public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrEmpty(value); - - public static string? NullIfEmpty(this string? value) => value.IsNullOrEmpty() ? null : value; -} diff --git a/Aigamo.ResXGenerator/AdditionalTextWithHash.cs b/Aigamo.ResXGenerator/Tools/AdditionalTextWithHash.cs similarity index 81% rename from Aigamo.ResXGenerator/AdditionalTextWithHash.cs rename to Aigamo.ResXGenerator/Tools/AdditionalTextWithHash.cs index 5899180..a7e77b9 100644 --- a/Aigamo.ResXGenerator/AdditionalTextWithHash.cs +++ b/Aigamo.ResXGenerator/Tools/AdditionalTextWithHash.cs @@ -1,6 +1,6 @@ using Microsoft.CodeAnalysis; -namespace Aigamo.ResXGenerator; +namespace Aigamo.ResXGenerator.Tools; public readonly record struct AdditionalTextWithHash(AdditionalText File, Guid Hash) { @@ -13,7 +13,7 @@ public override int GetHashCode() { unchecked { - return (File.GetHashCode() * 397) ^ Hash.GetHashCode(); + return File.GetHashCode() * 397 ^ Hash.GetHashCode(); } } diff --git a/Aigamo.ResXGenerator/Tools/Constants.cs b/Aigamo.ResXGenerator/Tools/Constants.cs new file mode 100644 index 0000000..a3dc016 --- /dev/null +++ b/Aigamo.ResXGenerator/Tools/Constants.cs @@ -0,0 +1,22 @@ +namespace Aigamo.ResXGenerator.Tools; + +internal static class Constants +{ + public const string SystemGlobalization = $"{nameof(System)}.{nameof(System.Globalization)}"; + public const string SystemResources = $"{nameof(System)}.{nameof(System.Resources)}"; + public const string AutoGeneratedHeader = + """ + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + """; + + public const string SResourceManagerVariable = "s_resourceManager"; + public const string ResourceManagerVariable = "ResourceManager"; + public const string CultureInfoVariable = "CultureInfo"; +} diff --git a/Aigamo.ResXGenerator/CultureInfoCombo.cs b/Aigamo.ResXGenerator/Tools/CultureInfoCombo.cs similarity index 79% rename from Aigamo.ResXGenerator/CultureInfoCombo.cs rename to Aigamo.ResXGenerator/Tools/CultureInfoCombo.cs index dac55b8..0a2a148 100644 --- a/Aigamo.ResXGenerator/CultureInfoCombo.cs +++ b/Aigamo.ResXGenerator/Tools/CultureInfoCombo.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Aigamo.ResXGenerator.Tools; namespace Aigamo.ResXGenerator; @@ -14,7 +15,7 @@ public CultureInfoCombo(IReadOnlyList? files) .Select(x => (Path.GetExtension(Path.GetFileNameWithoutExtension(x.File.Path)).TrimStart('.'), y: x)) .OrderByDescending(x => x.Item1.Length) .ThenBy(y => y.Item1) - .ToList() ?? new List<(string, AdditionalTextWithHash)>(); + .ToList() ?? []; } public IReadOnlyList<(string Iso, AdditionalTextWithHash File)> CultureInfos { get;} @@ -22,12 +23,12 @@ public CultureInfoCombo(IReadOnlyList? files) public IReadOnlyList<(string Name, int LCID, AdditionalTextWithHash FileWithHash)> GetDefinedLanguages() => CultureInfos? .Select(x => (x.File, new CultureInfo(x.Iso))) .Select(x => (Name: x.Item2.Name.Replace('-', '_'), x.Item2.LCID, x.File)) - .ToList() ?? new List<(string Name, int LCID, AdditionalTextWithHash FileWithHash)>(); + .ToList() ?? []; public bool Equals(CultureInfoCombo other) { - return (CultureInfos ?? Array.Empty<(string Iso, AdditionalTextWithHash File)>()).Select(x => x.Iso) - .SequenceEqual(other.CultureInfos?.Select(x => x.Iso) ?? Array.Empty()); + return (CultureInfos ?? []).Select(x => x.Iso) + .SequenceEqual(other.CultureInfos?.Select(x => x.Iso) ?? []); } public override int GetHashCode() diff --git a/Aigamo.ResXGenerator/FileOptions.cs b/Aigamo.ResXGenerator/Tools/GenFileOptions.cs similarity index 96% rename from Aigamo.ResXGenerator/FileOptions.cs rename to Aigamo.ResXGenerator/Tools/GenFileOptions.cs index 5747032..34444e5 100644 --- a/Aigamo.ResXGenerator/FileOptions.cs +++ b/Aigamo.ResXGenerator/Tools/GenFileOptions.cs @@ -1,8 +1,8 @@ using Microsoft.CodeAnalysis.Diagnostics; -namespace Aigamo.ResXGenerator; +namespace Aigamo.ResXGenerator.Tools; -public readonly record struct FileOptions +public readonly record struct GenFileOptions { public string InnerClassInstanceName { get; init; } public string InnerClassName { get; init; } @@ -21,7 +21,7 @@ public readonly record struct FileOptions public bool SkipFile { get; init; } public bool IsValid { get; init; } - public FileOptions( + public GenFileOptions( GroupedAdditionalFile groupedFile, AnalyzerConfigOptions options, GlobalOptions globalOptions @@ -142,13 +142,13 @@ GlobalOptions globalOptions IsValid = globalOptions.IsValid; } - public static FileOptions Select( + public static GenFileOptions Select( GroupedAdditionalFile file, AnalyzerConfigOptionsProvider options, GlobalOptions globalOptions ) { - return new FileOptions( + return new GenFileOptions( groupedFile: file, options: options.GetOptions(file.MainFile.File), globalOptions: globalOptions diff --git a/Aigamo.ResXGenerator/GlobalOptions.cs b/Aigamo.ResXGenerator/Tools/GlobalOptions.cs similarity index 98% rename from Aigamo.ResXGenerator/GlobalOptions.cs rename to Aigamo.ResXGenerator/Tools/GlobalOptions.cs index 5a7f6c4..0e560f8 100644 --- a/Aigamo.ResXGenerator/GlobalOptions.cs +++ b/Aigamo.ResXGenerator/Tools/GlobalOptions.cs @@ -1,4 +1,5 @@ -using Microsoft.CodeAnalysis.Diagnostics; +using Aigamo.ResXGenerator.Tools; +using Microsoft.CodeAnalysis.Diagnostics; namespace Aigamo.ResXGenerator; @@ -38,6 +39,7 @@ public GlobalOptions(AnalyzerConfigOptions options) { IsValid = false; } + ProjectName = projectName!; // Code from: https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md#consume-msbuild-properties-and-metadata diff --git a/Aigamo.ResXGenerator/GroupResxFiles.cs b/Aigamo.ResXGenerator/Tools/GroupResxFiles.cs similarity index 61% rename from Aigamo.ResXGenerator/GroupResxFiles.cs rename to Aigamo.ResXGenerator/Tools/GroupResxFiles.cs index c93a2c6..347d883 100644 --- a/Aigamo.ResXGenerator/GroupResxFiles.cs +++ b/Aigamo.ResXGenerator/Tools/GroupResxFiles.cs @@ -1,6 +1,6 @@ -using Microsoft.CodeAnalysis; +using Aigamo.ResXGenerator.Extensions; -namespace Aigamo.ResXGenerator; +namespace Aigamo.ResXGenerator.Tools; public static class GroupResxFiles { @@ -8,23 +8,22 @@ public static IEnumerable Group(IReadOnlyList(); var res = new Dictionary>(); - foreach (var file in allFilesWithHash) + allFilesWithHash.ForEach(file => { cancellationToken.ThrowIfCancellationRequested(); var path = file.File.Path; var pathName = Path.GetDirectoryName(path); var baseName = Utilities.GetBaseName(path); - if (Path.GetFileNameWithoutExtension(path) == baseName) - { - var key = pathName + "\\" + baseName; - //it should be impossible to exist already, but VS sometimes throws error about duplicate key added. Keep the original entry, not the new one - if (!lookup.ContainsKey(key)) - lookup.Add(key, file); - res.Add(file, new List()); - } - } - foreach (var fileWithHash in allFilesWithHash) + if (Path.GetFileNameWithoutExtension(path) != baseName) return; + + var key = pathName + "\\" + baseName; + //it should be impossible to exist already, but VS sometimes throws error about duplicate key added. Keep the original entry, not the new one + if (!lookup.ContainsKey(key)) + lookup.Add(key, file); + res.Add(file, []); + }); + allFilesWithHash.ForEach(fileWithHash => { cancellationToken.ThrowIfCancellationRequested(); @@ -32,20 +31,17 @@ public static IEnumerable Group(IReadOnlyList { cancellationToken.ThrowIfCancellationRequested(); - - yield return new GroupedAdditionalFile(file.Key, file.Value); - } + return new GroupedAdditionalFile(file.Key, file.Value); + }); } public static IEnumerable DetectChildCombos(IReadOnlyList groupedAdditionalFiles) diff --git a/Aigamo.ResXGenerator/GroupedAdditionalFile.cs b/Aigamo.ResXGenerator/Tools/GroupedAdditionalFile.cs similarity index 52% rename from Aigamo.ResXGenerator/GroupedAdditionalFile.cs rename to Aigamo.ResXGenerator/Tools/GroupedAdditionalFile.cs index 9c0da63..6b680d8 100644 --- a/Aigamo.ResXGenerator/GroupedAdditionalFile.cs +++ b/Aigamo.ResXGenerator/Tools/GroupedAdditionalFile.cs @@ -1,4 +1,6 @@ -namespace Aigamo.ResXGenerator; +using Aigamo.ResXGenerator.Extensions; + +namespace Aigamo.ResXGenerator.Tools; public readonly record struct GroupedAdditionalFile { @@ -11,28 +13,17 @@ public GroupedAdditionalFile(AdditionalTextWithHash mainFile, IReadOnlyList x.File.Path, StringComparer.Ordinal).ToArray(); } - public bool Equals(GroupedAdditionalFile other) - { - return MainFile.Equals(other.MainFile) && SubFiles.SequenceEqual(other.SubFiles); - } + public bool Equals(GroupedAdditionalFile other) => MainFile.Equals(other.MainFile) && SubFiles.SequenceEqual(other.SubFiles); public override int GetHashCode() { unchecked { var hashCode = MainFile.GetHashCode(); - - foreach (var additionalText in SubFiles) - { - hashCode = (hashCode * 397) ^ additionalText.GetHashCode(); - } - + SubFiles.ForEach(additionalText => hashCode = hashCode * 397 ^ additionalText.GetHashCode()); return hashCode; } } - public override string ToString() - { - return $"{nameof(MainFile)}: {MainFile}, {nameof(SubFiles)}: {string.Join("; ", SubFiles ?? Array.Empty())}"; - } + public override string ToString() => $"{nameof(MainFile)}: {MainFile}, {nameof(SubFiles)}: {string.Join("; ", SubFiles ?? [])}"; } diff --git a/Aigamo.ResXGenerator/Tools/IGenerator.cs b/Aigamo.ResXGenerator/Tools/IGenerator.cs new file mode 100644 index 0000000..202a99e --- /dev/null +++ b/Aigamo.ResXGenerator/Tools/IGenerator.cs @@ -0,0 +1,14 @@ +using Aigamo.ResXGenerator.Generators; + +namespace Aigamo.ResXGenerator.Tools; + +public interface IGenerator +{ + GeneratedOutput Generate(T options, CancellationToken cancellationToken = default); +} + +public interface IResXGenerator : IGenerator; +public interface IComboGenerator : IGenerator +{ + string GeneratedFileName(CultureInfoCombo combo); +} diff --git a/Aigamo.ResXGenerator/InnerClassVisibility.cs b/Aigamo.ResXGenerator/Tools/InnerClassVisibility.cs similarity index 73% rename from Aigamo.ResXGenerator/InnerClassVisibility.cs rename to Aigamo.ResXGenerator/Tools/InnerClassVisibility.cs index 1a6d0ce..c50dc63 100644 --- a/Aigamo.ResXGenerator/InnerClassVisibility.cs +++ b/Aigamo.ResXGenerator/Tools/InnerClassVisibility.cs @@ -1,4 +1,4 @@ -namespace Aigamo.ResXGenerator; +namespace Aigamo.ResXGenerator.Tools; public enum InnerClassVisibility { diff --git a/Aigamo.ResXGenerator/IsExternalInit.cs b/Aigamo.ResXGenerator/Tools/IsExternalInit.cs similarity index 100% rename from Aigamo.ResXGenerator/IsExternalInit.cs rename to Aigamo.ResXGenerator/Tools/IsExternalInit.cs diff --git a/Aigamo.ResXGenerator/NullableAttributes.cs b/Aigamo.ResXGenerator/Tools/NullableAttributes.cs similarity index 98% rename from Aigamo.ResXGenerator/NullableAttributes.cs rename to Aigamo.ResXGenerator/Tools/NullableAttributes.cs index f4729e0..29c3148 100644 --- a/Aigamo.ResXGenerator/NullableAttributes.cs +++ b/Aigamo.ResXGenerator/Tools/NullableAttributes.cs @@ -6,7 +6,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace System.Diagnostics.CodeAnalysis +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Aigamo.ResXGenerator.Tools; + +namespace Aigamo.ResXGenerator.Tools { #if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 /// Specifies that null is allowed as an input even if the corresponding type disallows it. diff --git a/Aigamo.ResXGenerator/Tools/RegexDefinitions.cs b/Aigamo.ResXGenerator/Tools/RegexDefinitions.cs new file mode 100644 index 0000000..43a8db5 --- /dev/null +++ b/Aigamo.ResXGenerator/Tools/RegexDefinitions.cs @@ -0,0 +1,15 @@ +using System.Text.RegularExpressions; + +namespace Aigamo.ResXGenerator.Tools; +internal static class RegexDefinitions +{ + public static readonly Regex ValidMemberNamePattern = new( + pattern: @"^[\p{L}\p{Nl}_][\p{Cf}\p{L}\p{Mc}\p{Mn}\p{Nd}\p{Nl}\p{Pc}]*$", + options: RegexOptions.Compiled | RegexOptions.CultureInvariant + ); + + public static readonly Regex InvalidMemberNameSymbols = new( + pattern: @"[^\p{Cf}\p{L}\p{Mc}\p{Mn}\p{Nd}\p{Nl}\p{Pc}]", + options: RegexOptions.Compiled | RegexOptions.CultureInvariant + ); +} diff --git a/Aigamo.ResXGenerator/Utilities.cs b/Aigamo.ResXGenerator/Tools/Utilities.cs similarity index 99% rename from Aigamo.ResXGenerator/Utilities.cs rename to Aigamo.ResXGenerator/Tools/Utilities.cs index 0103dd7..a99194c 100644 --- a/Aigamo.ResXGenerator/Utilities.cs +++ b/Aigamo.ResXGenerator/Tools/Utilities.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Text.RegularExpressions; +using Aigamo.ResXGenerator.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Aigamo.ResXGenerator; From c5c0f5b1bcfe4c4e656e6c78cf0d60438232815c Mon Sep 17 00:00:00 2001 From: FranckSix Date: Sat, 20 Sep 2025 20:32:11 -0400 Subject: [PATCH 02/14] Added compatibility with IStringLocalizer Using heritage pattern over the generators to clarify witch every class do. To clarify and simplify concept, separation of concepts. Unification of all Diagnostics in a class Analyzer to respect the Analyzer contract from MS RoslynAnalyser. Rebrand the SourceGenerator class to switch over a new option GenerationType : - ResourceManager (Default) - CodeGeneration (In addidition to GenerateCode, to maintenability of existing code) - StringLocaliser (new generator includes registration classes to simplifing adding resources to the CollectionService) I added a demo project to demonstrate the new feature. I optimized the uses of IncrementalProviders to avoid to apply the same filter over and over. Now we have thee bloc of generation - ResourcesManager - CodeGeneration - ComboGeneration - StringLocaliser - NamespaceRegistrator - GlobalRegistrator --- .editorconfig | 44 +- .../AdditionalTextStub.cs | 16 +- .../Aigamo.ResXGenerator.Tests.csproj | 8 +- Aigamo.ResXGenerator.Tests/CodeGenTests.cs | 340 ++------ .../CodeResXTestsHelpers.cs | 230 +++++ .../GeneratorLocalizerTests.cs | 294 +++++++ Aigamo.ResXGenerator.Tests/GeneratorTests.cs | 256 +----- .../GithubIssues/Issue3/GeneratorTests.cs | 5 +- .../GlobalRegisterTests.cs | 90 ++ .../GroupResxFilesTests.cs | 231 +++-- .../IntegrationTests/Test4.da-us.da-dk.resx | 107 +++ .../IntegrationTests/Test4.da-us.da.resx | 107 +++ .../IntegrationTests/Test4.da-us.resx | 107 +++ .../IntegrationTests/Test4.resx | 126 +++ .../LocalizerRegisterTests.cs | 77 ++ Aigamo.ResXGenerator.Tests/SettingsTests.cs | 794 ++++++++++-------- Aigamo.ResXGenerator.Tests/UtilitiesTests.cs | 62 +- Aigamo.ResXGenerator.sln | 6 + .../Aigamo.ResXGenerator.csproj | 2 +- Aigamo.ResXGenerator/Analyser.cs | 90 ++ .../AnalyzerReleases.Shipped.md | 10 +- .../AnalyzerReleases.Unshipped.md | 11 +- .../Extensions/BoolExtensions.cs | 5 + .../Extensions/EnumerableExtensions.cs | 13 +- .../Extensions/StringExtensions.cs | 19 +- .../Generators/CodeGenerator.cs | 83 ++ .../Generators/ComboGenerator.cs | 127 +-- .../Generators/GeneratedOutput.cs | 14 - .../Generators/GeneratorBase.cs | 40 + .../Generators/LocalizerGenerator.cs | 63 ++ .../LocalizerGlobalRegisterGenerator.cs | 41 + .../Generators/LocalizerRegisterGenerator.cs | 43 + .../Generators/ResourceManagerGenerator.cs | 297 ++----- .../Generators/StringBuilderGenerator.cs | 183 ---- Aigamo.ResXGenerator/Models/ComboItem.cs | 5 + Aigamo.ResXGenerator/Models/FallBackItem.cs | 5 + .../Models/GenFilesNamespace.cs | 13 + .../Models/GeneratedOutput.cs | 5 + .../Properties/launchSettings.json | 17 +- Aigamo.ResXGenerator/Rules.cs | 42 - Aigamo.ResXGenerator/SourceGenerator.cs | 235 ++++-- .../Tools/AdditionalTextWithHash.cs | 24 +- Aigamo.ResXGenerator/Tools/Constants.cs | 1 + .../Tools/CultureInfoCombo.cs | 5 +- Aigamo.ResXGenerator/Tools/GenFileOptions.cs | 11 + Aigamo.ResXGenerator/Tools/GenerationType.cs | 9 + Aigamo.ResXGenerator/Tools/GlobalOptions.cs | 13 +- Aigamo.ResXGenerator/Tools/IGenerator.cs | 10 +- .../Tools/IntegrityValidator.cs | 115 +++ .../Tools/RegexDefinitions.cs | 24 +- .../Tools/StringBuilderGeneratorHelper.cs | 190 +++++ Aigamo.ResXGenerator/Tools/Utilities.cs | 322 +++---- .../build/Aigamo.ResXGenerator.props | 2 + DemoLocalization/DemoLocalization.csproj | 33 + DemoLocalization/Langage.cs | 7 + DemoLocalization/MainProcess.cs | 16 + DemoLocalization/Options.cs | 13 + DemoLocalization/Program.cs | 38 + DemoLocalization/Resource.da.resx | 123 +++ DemoLocalization/Resource.fr.resx | 123 +++ DemoLocalization/Resource.resx | 123 +++ README.md | 100 ++- 62 files changed, 3675 insertions(+), 1890 deletions(-) create mode 100644 Aigamo.ResXGenerator.Tests/CodeResXTestsHelpers.cs create mode 100644 Aigamo.ResXGenerator.Tests/GeneratorLocalizerTests.cs create mode 100644 Aigamo.ResXGenerator.Tests/GlobalRegisterTests.cs create mode 100644 Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da-dk.resx create mode 100644 Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da.resx create mode 100644 Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.resx create mode 100644 Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.resx create mode 100644 Aigamo.ResXGenerator.Tests/LocalizerRegisterTests.cs create mode 100644 Aigamo.ResXGenerator/Analyser.cs create mode 100644 Aigamo.ResXGenerator/Extensions/BoolExtensions.cs create mode 100644 Aigamo.ResXGenerator/Generators/CodeGenerator.cs delete mode 100644 Aigamo.ResXGenerator/Generators/GeneratedOutput.cs create mode 100644 Aigamo.ResXGenerator/Generators/GeneratorBase.cs create mode 100644 Aigamo.ResXGenerator/Generators/LocalizerGenerator.cs create mode 100644 Aigamo.ResXGenerator/Generators/LocalizerGlobalRegisterGenerator.cs create mode 100644 Aigamo.ResXGenerator/Generators/LocalizerRegisterGenerator.cs delete mode 100644 Aigamo.ResXGenerator/Generators/StringBuilderGenerator.cs create mode 100644 Aigamo.ResXGenerator/Models/ComboItem.cs create mode 100644 Aigamo.ResXGenerator/Models/FallBackItem.cs create mode 100644 Aigamo.ResXGenerator/Models/GenFilesNamespace.cs create mode 100644 Aigamo.ResXGenerator/Models/GeneratedOutput.cs delete mode 100644 Aigamo.ResXGenerator/Rules.cs create mode 100644 Aigamo.ResXGenerator/Tools/GenerationType.cs create mode 100644 Aigamo.ResXGenerator/Tools/IntegrityValidator.cs create mode 100644 Aigamo.ResXGenerator/Tools/StringBuilderGeneratorHelper.cs create mode 100644 DemoLocalization/DemoLocalization.csproj create mode 100644 DemoLocalization/Langage.cs create mode 100644 DemoLocalization/MainProcess.cs create mode 100644 DemoLocalization/Options.cs create mode 100644 DemoLocalization/Program.cs create mode 100644 DemoLocalization/Resource.da.resx create mode 100644 DemoLocalization/Resource.fr.resx create mode 100644 DemoLocalization/Resource.resx diff --git a/.editorconfig b/.editorconfig index 5cecb19..7e4089c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,7 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -indent_style = tab +indent_style = space indent_size = 4 # XML project files @@ -68,11 +68,13 @@ dotnet_naming_style.pascal_case_style.capitalization = pascal_case # Use PascalCase for constant fields dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style dotnet_naming_symbols.constant_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.applicable_accessibilities = * dotnet_naming_symbols.constant_fields.required_modifiers = const -tab_width=4 +tab_width= 4 +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_namespace_match_folder = true:suggestion ############################### # C# Coding Conventions # ############################### @@ -82,12 +84,12 @@ csharp_style_var_for_built_in_types = true:silent csharp_style_var_when_type_is_apparent = true:silent csharp_style_var_elsewhere = true:silent # Expression-bodied members -csharp_style_expression_bodied_methods = false:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_operators = false:silent -csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_methods = true:suggestion +csharp_style_expression_bodied_constructors = true:suggestion +csharp_style_expression_bodied_operators = true:suggestion +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_accessors = true:suggestion # Pattern matching preferences csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion csharp_style_pattern_matching_over_as_with_null_check = true:suggestion @@ -132,9 +134,21 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false # Wrapping preferences csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true -############################### -# VB Coding Conventions # -############################### -[*.vb] -# Modifier preferences -visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion +csharp_style_prefer_simple_property_accessors = true:suggestion +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_static_anonymous_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion +csharp_style_prefer_implicitly_typed_lambda_expression = true:suggestion diff --git a/Aigamo.ResXGenerator.Tests/AdditionalTextStub.cs b/Aigamo.ResXGenerator.Tests/AdditionalTextStub.cs index 26df91e..d19eb34 100644 --- a/Aigamo.ResXGenerator.Tests/AdditionalTextStub.cs +++ b/Aigamo.ResXGenerator.Tests/AdditionalTextStub.cs @@ -1,19 +1,13 @@ -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; namespace Aigamo.ResXGenerator.Tests; -internal class AdditionalTextStub : AdditionalText +internal class AdditionalTextStub(string path, string? text = null) : AdditionalText { - private readonly SourceText? _text; + private readonly SourceText? _text = text is null ? null : SourceText.From(text); - public override string Path { get; } + public override string Path { get; } = path; - public AdditionalTextStub(string path, string? text = null) - { - _text = text is null ? null : SourceText.From(text); - Path = path; - } - - public override SourceText? GetText(CancellationToken cancellationToken = new()) => _text; + public override SourceText? GetText(CancellationToken cancellationToken = new()) => _text; } diff --git a/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj b/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj index fdc04ec..f946bf0 100644 --- a/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj +++ b/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj @@ -8,7 +8,7 @@ enable - + @@ -17,6 +17,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -33,11 +34,14 @@ $([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx - true + CodeGeneration true + + StringLocalizer + diff --git a/Aigamo.ResXGenerator.Tests/CodeGenTests.cs b/Aigamo.ResXGenerator.Tests/CodeGenTests.cs index 27883df..0638c1b 100644 --- a/Aigamo.ResXGenerator.Tests/CodeGenTests.cs +++ b/Aigamo.ResXGenerator.Tests/CodeGenTests.cs @@ -2,291 +2,81 @@ using Aigamo.ResXGenerator.Tools; using FluentAssertions; using Xunit; -using static System.Guid; namespace Aigamo.ResXGenerator.Tests; public class CodeGenTests { - private const string Text = """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Oldest - - - Newest - - - """; + private static void Generate( + IResXGenerator generator, + bool publicClass = true, + bool staticClass = true, + bool partial = false, + bool nullForgivingOperators = false, + bool staticMembers = true + ) + { + var expected = $$""" + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace Resources; + using static Aigamo.ResXGenerator.Helpers; - private const string TextDa = """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - OldestDa - - - NewestDa - - - """; + {{(publicClass ? "public" : "internal")}}{{(partial ? " partial" : string.Empty)}}{{(staticClass ? " static" : string.Empty)}} class ActivityEntrySortRuleNames + { - private const string TextDaDk = """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - OldestDaDK - - - NewestDaDK - - - """; - private static void Generate( - IResXGenerator generator, - bool publicClass = true, - bool staticClass = true, - bool partial = false, - bool nullForgivingOperators = false, - bool staticMembers = true - ) - { - var expected = $$""" - // ------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - // ------------------------------------------------------------------------------ - #nullable enable - namespace Resources; - using static Aigamo.ResXGenerator.Helpers; + /// + /// Looks up a localized string similar to Oldest. + /// + public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDate => GetString_1030_6("Oldest", "OldestDaDK", "OldestDa"); - {{(publicClass ? "public" : "internal")}}{{(partial ? " partial" : string.Empty)}}{{(staticClass ? " static" : string.Empty)}} class ActivityEntrySortRuleNames - { + /// + /// Looks up a localized string similar to Newest. + /// + public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDateDescending => GetString_1030_6("Newest", "NewestDaDK", "NewestDa"); + } - /// - /// Looks up a localized string similar to Oldest. - /// - public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDate => GetString_1030_6("Oldest", "OldestDaDK", "OldestDa"); + """; - /// - /// Looks up a localized string similar to Newest. - /// - public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDateDescending => GetString_1030_6("Newest", "NewestDaDK", "NewestDa"); - } + var result = generator.Generate( + new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", + CustomToolNamespace = "Resources", + ClassName = "ActivityEntrySortRuleNames", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub(string.Empty, CodeResXTestsHelpers.GetText()), Guid.NewGuid()), + subFiles: + [ + new AdditionalTextWithHash(new AdditionalTextStub("test.da.rex", CodeResXTestsHelpers.GetText("Da")), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.da-dk.rex", CodeResXTestsHelpers.GetText("DaDK")), Guid.NewGuid()) + ] + ), + PublicClass = publicClass, + GenerateCode = true, + NullForgivingOperators = nullForgivingOperators, + StaticClass = staticClass, + PartialClass = partial, + StaticMembers = staticMembers + }); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } - """; - var result = generator.Generate( - new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", - CustomToolNamespace = "Resources", - ClassName = "ActivityEntrySortRuleNames", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(string.Empty, Text), NewGuid()), - subFiles: - [ - new AdditionalTextWithHash(new AdditionalTextStub("test.da.rex", TextDa), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.da-dk.rex", TextDaDk), NewGuid()) - ] - ), - PublicClass = publicClass, - GenerateCode = true, - NullForgivingOperators = nullForgivingOperators, - StaticClass = staticClass, - PartialClass = partial, - StaticMembers = staticMembers - }); - result.ErrorsAndWarnings.Should().BeNullOrEmpty(); - result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } - - - [Fact] - public void Generate_StringBuilder_Public() - { - var generator = new ResourceManagerGenerator(); - Generate(generator); - Generate(generator, nullForgivingOperators: true); - } + [Fact] + public void Generate_StringBuilder_Public() + { + var generator = new CodeGenerator(); + Generate(generator); + Generate(generator, nullForgivingOperators: true); + } } diff --git a/Aigamo.ResXGenerator.Tests/CodeResXTestsHelpers.cs b/Aigamo.ResXGenerator.Tests/CodeResXTestsHelpers.cs new file mode 100644 index 0000000..8437414 --- /dev/null +++ b/Aigamo.ResXGenerator.Tests/CodeResXTestsHelpers.cs @@ -0,0 +1,230 @@ +namespace Aigamo.ResXGenerator.Tests; + +internal static class CodeResXTestsHelpers +{ + public static string GetText() => GetText(string.Empty); + public static string GetText(string language) => $""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Oldest{language} + + + Newest{language} + + + """; + + public static string GetDaTextWithDuplicates() => """ + + + + Works. + + + Doeesnt Work. + + + """; + + public static string GetTextWithNewline() => """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + This entry has been deleted. It is still temporarily accessible, but won't show up in any of the listings. + + + This entry was merged to + + + Draft = entry is missing crucial information. This status indicates that you're requesting additional information to be added or corrected.<br /> + Finished = The entry has all the necessary information, but it hasn't been inspected by a trusted user yet.<br /> + Approved = The entry has been inspected and approved by a trusted user. Approved entries can only be edited by trusted users. + + + This entry is locked, meaning that only moderators are allowed to edit it. + + + Choose the language for this name. "Original" is the name in original language that isn't English, for example Japanese. If the original language is English, do not input a name in the "Original" language. + + + This page revision has been hidden. + + + """; +} diff --git a/Aigamo.ResXGenerator.Tests/GeneratorLocalizerTests.cs b/Aigamo.ResXGenerator.Tests/GeneratorLocalizerTests.cs new file mode 100644 index 0000000..09c712f --- /dev/null +++ b/Aigamo.ResXGenerator.Tests/GeneratorLocalizerTests.cs @@ -0,0 +1,294 @@ +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Generators; +using Aigamo.ResXGenerator.Tools; +using FluentAssertions; +using Microsoft.CodeAnalysis; +using Xunit; + +namespace Aigamo.ResXGenerator.Tests; + +public class GeneratorLocalizerTests +{ + private static void Generate( + IResXGenerator generator, + bool publicClass = false, + bool nullForgivingOperators = false + ) + { + var expected = $$""" + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + {{nullForgivingOperators.InterpolateCondition("#nullable disable", "#nullable enable")}} + using Microsoft.Extensions.Localization; + using System.Text; + + namespace VocaDb.Web.App_GlobalResources; + + public interface IActivityEntrySortRuleNames + { + /// + /// Looks up a localized string similar to Oldest. + /// + string CreateDate {get;} + /// + /// Looks up a localized string similar to Newest. + /// + string CreateDateDescending {get;} + } + + {{publicClass.InterpolateCondition("public", "internal")}} class ActivityEntrySortRuleNames(IStringLocalizer stringLocalizer) : IActivityEntrySortRuleNames + { + public string CreateDate => stringLocalizer["CreateDate"]; + public string CreateDateDescending => stringLocalizer["CreateDateDescending"]; + } + """; + + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", + GenerationType = GenerationType.StringLocalizer, + ClassName = "ActivityEntrySortRuleNames", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetText()), Guid.NewGuid()), + subFiles: [] + ), + PublicClass = publicClass, + NullForgivingOperators = nullForgivingOperators + } + ); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } + + [Fact] + public void Generate_Localizer_Public() + { + var generator = new LocalizerGenerator(); + Generate(generator); + Generate(generator, publicClass: true); + } + + [Fact] + public void Generate_Localizer_NullForgivingOperators() + { + var generator = new LocalizerGenerator(); + Generate(generator); + Generate(generator, nullForgivingOperators: true); + } + + [Fact] + public void Generate_Localizer_NewLine() + { + const string expected = """ + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + using Microsoft.Extensions.Localization; + using System.Text; + + namespace VocaDb.Web.App_GlobalResources; + + public interface ICommonMessages + { + /// + /// Looks up a localized string similar to This entry has been deleted. It is still temporarily accessible, but won't show up in any of the listings.. + /// + string EntryDeleted {get;} + /// + /// Looks up a localized string similar to This entry was merged to. + /// + string EntryMergedTo {get;} + /// + /// Looks up a localized string similar to Draft = entry is missing crucial information. This status indicates that you're requesting additional information to be added or corrected.<br /> + /// Finished = The entry has all the necessary information, but it hasn't been inspected by a trusted user yet.<br /> + /// Approved = The entry has been inspected and approved by a trusted user. Approved entries can only be edited by trusted users.. + /// + string EntryStatusExplanation {get;} + /// + /// Looks up a localized string similar to This entry is locked, meaning that only moderators are allowed to edit it.. + /// + string Locked {get;} + /// + /// Looks up a localized string similar to Choose the language for this name. "Original" is the name in original language that isn't English, for example Japanese. If the original language is English, do not input a name in the "Original" language.. + /// + string NameLanguageHelp {get;} + /// + /// Looks up a localized string similar to This page revision has been hidden.. + /// + string RevisionHidden {get;} + } + + internal class CommonMessages(IStringLocalizer stringLocalizer) : ICommonMessages + { + public string EntryDeleted => stringLocalizer["EntryDeleted"]; + public string EntryMergedTo => stringLocalizer["EntryMergedTo"]; + public string EntryStatusExplanation => stringLocalizer["EntryStatusExplanation"]; + public string Locked => stringLocalizer["Locked"]; + public string NameLanguageHelp => stringLocalizer["NameLanguageHelp"]; + public string RevisionHidden => stringLocalizer["RevisionHidden"]; + } + """; + var generator = new LocalizerGenerator(); + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + GenerationType = GenerationType.StringLocalizer, + ClassName = "CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetTextWithNewline()), Guid.NewGuid()), + subFiles: [] + ) + } + ); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } + + [Fact] + public void Generate_Localizer_Name_DuplicateDataGivesWarning() + { + var generator = new LocalizerGenerator(); + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetDaTextWithDuplicates()), Guid.NewGuid()), + subFiles: [] + ), + GenerationType = GenerationType.StringLocalizer, + ClassName = "CommonMessages" + } + ); + var errs = result.ErrorsAndWarnings.ToList(); + errs.Should().NotBeNull(); + errs.Should().HaveCount(1); + errs[0].Id.Should().Be(Analyser.DuplicateWarning.Id); + errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); + errs[0].GetMessage().Should().Contain("DupKey"); + errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(5); + } + + [Fact] + public void Generate_Localizer_Name_MemberSameAsFileGivesWarning() + { + const string text = """ + + + + Works. + + + """; + + var generator = new LocalizerGenerator(); + var results = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), + subFiles: [] + ), + GenerationType = GenerationType.StringLocalizer, + ClassName = "CommonMessages" + } + ); + var errs = results.ErrorsAndWarnings.ToList(); + errs.Should().NotBeNull(); + errs.Should().HaveCount(1); + errs[0].Id.Should().Be(Analyser.MemberSameAsClassWarning.Id); + errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); + errs[0].GetMessage().Should().Contain("CommonMessages"); + errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(2); + } + + [Fact] + public void Generate_Localizer_StaticAndPartial_Options_Prohibited() + { + const string text = """ + + + + Works. + + + """; + + var generator = new LocalizerGenerator(); + var results = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), + subFiles: [] + ), + GenerationType = GenerationType.StringLocalizer, + ClassName = "CommonMessages", + StaticClass = true, + PartialClass = true + } + ); + var errs = results.ErrorsAndWarnings.ToList(); + errs.Should().NotBeNull(); + errs.Should().HaveCount(2); + errs[0].Id.Should().Be(Analyser.LocalizerStaticError.Id); + errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); + errs[1].Id.Should().Be(Analyser.LocalizerPartialError.Id); + errs[1].Severity.Should().Be(DiagnosticSeverity.Warning); + } + + [Fact] + public void Generate_Localizer_CustomNamespace_Option_Prohibited() + { + const string text = """ + + + + Works. + + + """; + + var generator = new LocalizerGenerator(); + var results = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + CustomToolNamespace = "Resources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), + subFiles: [] + ), + GenerationType = GenerationType.StringLocalizer, + ClassName = "CommonMessages" + } + ); + var errs = results.ErrorsAndWarnings.ToList(); + errs.Should().NotBeNull(); + errs.Should().HaveCount(1); + errs[0].Id.Should().Be(Analyser.LocalizationIncoherentNamespace.Id); + errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); + } +} diff --git a/Aigamo.ResXGenerator.Tests/GeneratorTests.cs b/Aigamo.ResXGenerator.Tests/GeneratorTests.cs index 5cfa003..48a58f8 100644 --- a/Aigamo.ResXGenerator.Tests/GeneratorTests.cs +++ b/Aigamo.ResXGenerator.Tests/GeneratorTests.cs @@ -3,82 +3,11 @@ using FluentAssertions; using Microsoft.CodeAnalysis; using Xunit; -using static System.Guid; namespace Aigamo.ResXGenerator.Tests; public class GeneratorTests { - private const string Text = """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Oldest - - - Newest - - - """; - private static void Generate( IResXGenerator generator, bool publicClass = true, @@ -129,7 +58,7 @@ namespace Resources; CustomToolNamespace = "Resources", ClassName = "ActivityEntrySortRuleNames", GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", Text), NewGuid()), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetText()), Guid.NewGuid()), subFiles: [] ), PublicClass = publicClass, @@ -201,7 +130,7 @@ namespace Resources; PublicClass = publicClass, NullForgivingOperators = nullForgivingOperators, GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", Text), NewGuid()), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetText()), Guid.NewGuid()), subFiles: [] ), StaticClass = staticClass, @@ -273,149 +202,6 @@ public void Generate_StringBuilder_InnerInstance() [Fact] public void Generate_StringBuilder_NewLine() { - const string text = """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - This entry has been deleted. It is still temporarily accessible, but won't show up in any of the listings. - - - This entry was merged to - - - Draft = entry is missing crucial information. This status indicates that you're requesting additional information to be added or corrected.<br /> - Finished = The entry has all the necessary information, but it hasn't been inspected by a trusted user yet.<br /> - Approved = The entry has been inspected and approved by a trusted user. Approved entries can only be edited by trusted users. - - - This entry is locked, meaning that only moderators are allowed to edit it. - - - Choose the language for this name. "Original" is the name in original language that isn't English, for example Japanese. If the original language is English, do not input a name in the "Original" language. - - - This page revision has been hidden. - - - """; - const string expected = """ // ------------------------------------------------------------------------------ // @@ -479,7 +265,7 @@ public static class CommonMessages CustomToolNamespace = null, ClassName = "CommonMessages", GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetTextWithNewline()), Guid.NewGuid()), subFiles: [] ), PublicClass = true, @@ -540,7 +326,7 @@ public static class CommonMessages EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", CustomToolNamespace = null, GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), subFiles: [] ), ClassName = "CommonMessages", @@ -557,18 +343,6 @@ public static class CommonMessages [Fact] public void Generate_StringBuilder_Name_DuplicateDataGivesWarning() { - const string text = """ - - - - Works. - - - Doeesnt Work. - - - """; - var generator = new ResourceManagerGenerator(); var result = generator.Generate( options: new GenFileOptions @@ -576,7 +350,7 @@ public void Generate_StringBuilder_Name_DuplicateDataGivesWarning() LocalNamespace = "VocaDb.Web.App_GlobalResources", EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetDaTextWithDuplicates()), Guid.NewGuid()), subFiles: [] ), CustomToolNamespace = null, @@ -589,7 +363,7 @@ public void Generate_StringBuilder_Name_DuplicateDataGivesWarning() var errs = result.ErrorsAndWarnings.ToList(); errs.Should().NotBeNull(); errs.Should().HaveCount(1); - errs[0].Id.Should().Be("AigamoResXGenerator001"); + errs[0].Id.Should().Be(Analyser.DuplicateWarning.Id); errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); errs[0].GetMessage().Should().Contain("DupKey"); errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(5); @@ -614,7 +388,7 @@ public void Generate_StringBuilder_Name_MemberSameAsFileGivesWarning() LocalNamespace = "VocaDb.Web.App_GlobalResources", EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), subFiles: [] ), CustomToolNamespace = null, @@ -627,23 +401,9 @@ public void Generate_StringBuilder_Name_MemberSameAsFileGivesWarning() var errs = results.ErrorsAndWarnings.ToList(); errs.Should().NotBeNull(); errs.Should().HaveCount(1); - errs[0].Id.Should().Be("AigamoResXGenerator002"); + errs[0].Id.Should().Be(Analyser.MemberSameAsClassWarning.Id); errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); errs[0].GetMessage().Should().Contain("CommonMessages"); errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(2); } - - [Fact] - public void GetLocalNamespace_ShouldNotGenerateIllegalNamespace() - { - var ns = Utilities.GetLocalNamespace("resx", "asd.asd", "path", "name", "root"); - ns.Should().Be("root"); - } - - [Fact] - public void ResxFileName_ShouldNotGenerateIllegalClassNames() - { - var ns = Utilities.GetClassNameFromPath("test.cshtml.resx"); - ns.Should().Be("test"); - } } diff --git a/Aigamo.ResXGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs b/Aigamo.ResXGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs index 4950714..1c89053 100644 --- a/Aigamo.ResXGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs +++ b/Aigamo.ResXGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs @@ -3,7 +3,6 @@ using Aigamo.ResXGenerator.Tools; using FluentAssertions; using Xunit; -using static System.Guid; namespace Aigamo.ResXGenerator.Tests.GithubIssues.Issue3; @@ -115,7 +114,7 @@ public static class CommonMessages EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", CustomToolNamespace = null, GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), subFiles: [] ), ClassName = "CommonMessages", @@ -208,7 +207,7 @@ public void Generate_StringBuilder_Value_InvalidCharacter() CustomToolNamespace = "Resources", ClassName = "ActivityEntrySortRuleNames", GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), subFiles: [] ), PublicClass = true, diff --git a/Aigamo.ResXGenerator.Tests/GlobalRegisterTests.cs b/Aigamo.ResXGenerator.Tests/GlobalRegisterTests.cs new file mode 100644 index 0000000..9fd1ada --- /dev/null +++ b/Aigamo.ResXGenerator.Tests/GlobalRegisterTests.cs @@ -0,0 +1,90 @@ +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Generators; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; +using FluentAssertions; +using Xunit; + +namespace Aigamo.ResXGenerator.Tests; + +public class GlobalRegisterTests +{ + private static void Generate( + IGlobalRegisterGenerator generator, + bool nullForgivingOperators = false) + { + string expected = $$""" + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + {{nullForgivingOperators.InterpolateCondition("#nullable disable", "#nullable enable")}} + using Microsoft.Extensions.DependencyInjection; + using VocaDb.Web.App_GlobalResources; + using VocaDb.Web.App_LocalResources; + + namespace ResXGenerator.Registration; + + public static class ResXGeneratorRegistrationExtension + { + public static IServiceCollection UsingResXGenerator(this IServiceCollection services) + { + services.UsingVocaDbWebAppGlobalResourcesResX(); + services.UsingVocaDbWebAppLocalResourcesResX(); + + return services; + } + } + """; + + var result = generator.Generate( + options: + [ + new GenFilesNamespace( + "VocaDb.Web.App_GlobalResources", + [ + new GenFileOptions + { + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", + NullForgivingOperators = nullForgivingOperators, + } + ]), + new GenFilesNamespace( + "VocaDb.Web.App_LocalResources", + [ + new GenFileOptions + { + EmbeddedFilename = "VocaDb.Web.App_LocalResources.LocalViewResources", + NullForgivingOperators = nullForgivingOperators, + }, + new GenFileOptions + { + EmbeddedFilename = "VocaDb.Web.App_LocalResources.SpecificResources", + NullForgivingOperators = nullForgivingOperators, + } + ]) + ] + ); + + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } + + [Fact] + public void CanRegisterLocalClass() + { + var generator = new LocalizerGlobalRegisterGenerator(); + Generate(generator); + } + + [Fact] + public void CanRegisterLocalClassWithNullForgivingOperators() + { + var generator = new LocalizerGlobalRegisterGenerator(); + Generate(generator, true); + } +} diff --git a/Aigamo.ResXGenerator.Tests/GroupResxFilesTests.cs b/Aigamo.ResXGenerator.Tests/GroupResxFilesTests.cs index 703e5c5..2f43bc4 100644 --- a/Aigamo.ResXGenerator.Tests/GroupResxFilesTests.cs +++ b/Aigamo.ResXGenerator.Tests/GroupResxFilesTests.cs @@ -1,7 +1,6 @@ using Aigamo.ResXGenerator.Tools; using FluentAssertions; using Xunit; -using static System.Guid; namespace Aigamo.ResXGenerator.Tests; @@ -11,18 +10,18 @@ public class GroupResxFilesTests public void CompareGroupedAdditionalFile_SameRoot_SameSubFiles_DifferentOrder() { var v1 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( - @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), + @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Guid.Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), [ - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")) + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Guid.Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Guid.Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")) ]); var v2 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( - @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), + @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Guid.Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), [ - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")) + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Guid.Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Guid.Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")) ] ); v1.Should().Be(v2); @@ -32,17 +31,17 @@ public void CompareGroupedAdditionalFile_SameRoot_SameSubFiles_DifferentOrder() public void CompareGroupedAdditionalFile_SameRoot_DiffSubFilesNames() { var v1 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( - @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), + @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Guid.Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), [ - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.en.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.fr.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")) + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.en.resx"), Guid.Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.fr.resx"), Guid.Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")) ]); var v2 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( - @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), + @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Guid.Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), [ - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.de.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.ro.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")) + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.de.resx"), Guid.Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.ro.resx"), Guid.Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")) ] ); v1.Should().NotBe(v2); @@ -52,17 +51,17 @@ public void CompareGroupedAdditionalFile_SameRoot_DiffSubFilesNames() public void CompareGroupedAdditionalFile_SameRoot_DiffSubFileContent() { var v1 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( - @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), + @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Guid.Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), [ - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("771F9C76-D9F4-4AF4-95D2-B3426F9EC15A")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")) + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Guid.Parse("771F9C76-D9F4-4AF4-95D2-B3426F9EC15A")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Guid.Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")) ]); var v2 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( - @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), + @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Guid.Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), [ - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")) + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Guid.Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Guid.Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")) ] ); v1.Should().NotBe(v2); @@ -72,18 +71,18 @@ public void CompareGroupedAdditionalFile_SameRoot_DiffSubFileContent() public void CompareGroupedAdditionalFile_DiffRootContent_SameSubFiles() { var v1 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( - @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), + @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Guid.Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")), [ - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")) + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Guid.Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Guid.Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")) ]); var v2 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub( - @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("A7E92264-8047-4668-979F-6EFC14EBAFC5")), + @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Guid.Parse("A7E92264-8047-4668-979F-6EFC14EBAFC5")), [ - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")) + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Guid.Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Guid.Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")) ] ); v1.Should().NotBe(v2); @@ -91,113 +90,113 @@ public void CompareGroupedAdditionalFile_DiffRootContent_SameSubFiles() static readonly (string Path, Guid Hash)[] s_data = [ - (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx", Parse("00000000-0000-0000-0000-000000000001")), - (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx", Parse("00000000-0000-0000-0000-000000000002")), - (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx", Parse("00000000-0000-0000-0000-000000000003")), - (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.da.resx", Parse("00000000-0000-0000-0000-000000000004")), - (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.resx", Parse("00000000-0000-0000-0000-000000000005")), - (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.vi.resx", Parse("00000000-0000-0000-0000-000000000006")), - (@"D:\src\xhg\y\Areas\Identity\Pages\Login.da.resx", Parse("00000000-0000-0000-0000-000000000007")), - (@"D:\src\xhg\y\Areas\Identity\Pages\Login.resx", Parse("00000000-0000-0000-0000-000000000008")), - (@"D:\src\xhg\y\Areas\Identity\Pages\Login.vi.resx", Parse("00000000-0000-0000-0000-000000000009")), - (@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.da.resx", Parse("00000000-0000-0000-0000-000000000010")), - (@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.resx", Parse("00000000-0000-0000-0000-000000000011")), - (@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.vi.resx", Parse("00000000-0000-0000-0000-000000000012")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.cs-cz.resx", Parse("00000000-0000-0000-0000-000000000013")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.da.resx", Parse("00000000-0000-0000-0000-000000000014")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.de.resx", Parse("00000000-0000-0000-0000-000000000015")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.es.resx", Parse("00000000-0000-0000-0000-000000000016")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.fi.resx", Parse("00000000-0000-0000-0000-000000000017")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.fr.resx", Parse("00000000-0000-0000-0000-000000000018")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.it.resx", Parse("00000000-0000-0000-0000-000000000019")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.lt.resx", Parse("00000000-0000-0000-0000-000000000020")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.lv.resx", Parse("00000000-0000-0000-0000-000000000021")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.nb-no.resx", Parse("00000000-0000-0000-0000-000000000022")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.nl.resx", Parse("00000000-0000-0000-0000-000000000023")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.nn-no.resx", Parse("00000000-0000-0000-0000-000000000024")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.pl.resx", Parse("00000000-0000-0000-0000-000000000025")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.resx", Parse("00000000-0000-0000-0000-000000000026")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.ru.resx", Parse("00000000-0000-0000-0000-000000000027")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.sv.resx", Parse("00000000-0000-0000-0000-000000000028")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.tr.resx", Parse("00000000-0000-0000-0000-000000000029")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.vi.resx", Parse("00000000-0000-0000-0000-000000000030")), - (@"D:\src\xhg\y\Areas\QxModule\QtrController.zh-cn.resx", Parse("00000000-0000-0000-0000-000000000031")), - (@"D:\src\xhg\y\DataAnnotations\DataAnnotation.da.resx", Parse("00000000-0000-0000-0000-000000000032")), - (@"D:\src\xhg\y\DataAnnotations\DataAnnotation.resx", Parse("00000000-0000-0000-0000-000000000033")), - (@"D:\src\xhg\y\DataAnnotations\DataAnnotation2.resx", Parse("00000000-0000-0000-0000-000000000034")) + (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx", Guid.Parse("00000000-0000-0000-0000-000000000001")), + (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx", Guid.Parse("00000000-0000-0000-0000-000000000002")), + (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx", Guid.Parse("00000000-0000-0000-0000-000000000003")), + (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.da.resx", Guid.Parse("00000000-0000-0000-0000-000000000004")), + (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.resx", Guid.Parse("00000000-0000-0000-0000-000000000005")), + (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.vi.resx", Guid.Parse("00000000-0000-0000-0000-000000000006")), + (@"D:\src\xhg\y\Areas\Identity\Pages\Login.da.resx", Guid.Parse("00000000-0000-0000-0000-000000000007")), + (@"D:\src\xhg\y\Areas\Identity\Pages\Login.resx", Guid.Parse("00000000-0000-0000-0000-000000000008")), + (@"D:\src\xhg\y\Areas\Identity\Pages\Login.vi.resx", Guid.Parse("00000000-0000-0000-0000-000000000009")), + (@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.da.resx", Guid.Parse("00000000-0000-0000-0000-000000000010")), + (@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.resx", Guid.Parse("00000000-0000-0000-0000-000000000011")), + (@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.vi.resx", Guid.Parse("00000000-0000-0000-0000-000000000012")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.cs-cz.resx", Guid.Parse("00000000-0000-0000-0000-000000000013")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.da.resx", Guid.Parse("00000000-0000-0000-0000-000000000014")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.de.resx", Guid.Parse("00000000-0000-0000-0000-000000000015")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.es.resx", Guid.Parse("00000000-0000-0000-0000-000000000016")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.fi.resx", Guid.Parse("00000000-0000-0000-0000-000000000017")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.fr.resx", Guid.Parse("00000000-0000-0000-0000-000000000018")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.it.resx", Guid.Parse("00000000-0000-0000-0000-000000000019")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.lt.resx", Guid.Parse("00000000-0000-0000-0000-000000000020")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.lv.resx", Guid.Parse("00000000-0000-0000-0000-000000000021")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.nb-no.resx", Guid.Parse("00000000-0000-0000-0000-000000000022")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.nl.resx", Guid.Parse("00000000-0000-0000-0000-000000000023")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.nn-no.resx", Guid.Parse("00000000-0000-0000-0000-000000000024")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.pl.resx", Guid.Parse("00000000-0000-0000-0000-000000000025")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.resx", Guid.Parse("00000000-0000-0000-0000-000000000026")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.ru.resx", Guid.Parse("00000000-0000-0000-0000-000000000027")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.sv.resx", Guid.Parse("00000000-0000-0000-0000-000000000028")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.tr.resx", Guid.Parse("00000000-0000-0000-0000-000000000029")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.vi.resx", Guid.Parse("00000000-0000-0000-0000-000000000030")), + (@"D:\src\xhg\y\Areas\QxModule\QtrController.zh-cn.resx", Guid.Parse("00000000-0000-0000-0000-000000000031")), + (@"D:\src\xhg\y\DataAnnotations\DataAnnotation.da.resx", Guid.Parse("00000000-0000-0000-0000-000000000032")), + (@"D:\src\xhg\y\DataAnnotations\DataAnnotation.resx", Guid.Parse("00000000-0000-0000-0000-000000000033")), + (@"D:\src\xhg\y\DataAnnotations\DataAnnotation2.resx", Guid.Parse("00000000-0000-0000-0000-000000000034")) ]; [Fact] public void FileGrouping() { - var result = GroupResxFiles.Group(s_data.Select(x => new AdditionalTextWithHash(new AdditionalTextStub(x.Path), x.Hash)).OrderBy(_ => NewGuid()).ToArray()); + var result = GroupResxFiles.Group(s_data.Select(x => new AdditionalTextWithHash(new AdditionalTextStub(x.Path), x.Hash)).OrderBy(_ => Guid.NewGuid()).ToArray()); var testData = new List { new( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("00000000-0000-0000-0000-000000000002")), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Guid.Parse("00000000-0000-0000-0000-000000000002")), subFiles: [ - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("00000000-0000-0000-0000-000000000001")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("00000000-0000-0000-0000-000000000003")) + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Guid.Parse("00000000-0000-0000-0000-000000000001")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Guid.Parse("00000000-0000-0000-0000-000000000003")) ] ), new( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.resx"), Parse("00000000-0000-0000-0000-000000000005")), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.resx"), Guid.Parse("00000000-0000-0000-0000-000000000005")), subFiles: [ - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.da.resx"), Parse("00000000-0000-0000-0000-000000000004")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.vi.resx"), Parse("00000000-0000-0000-0000-000000000006")) + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.da.resx"), Guid.Parse("00000000-0000-0000-0000-000000000004")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.vi.resx"), Guid.Parse("00000000-0000-0000-0000-000000000006")) ] ), new( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\Identity\Pages\Login.resx"), Parse("00000000-0000-0000-0000-000000000008")), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\Identity\Pages\Login.resx"), Guid.Parse("00000000-0000-0000-0000-000000000008")), subFiles: [ - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\Identity\Pages\Login.da.resx"), Parse("00000000-0000-0000-0000-000000000007")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\Identity\Pages\Login.vi.resx"), Parse("00000000-0000-0000-0000-000000000009")) + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\Identity\Pages\Login.da.resx"), Guid.Parse("00000000-0000-0000-0000-000000000007")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\Identity\Pages\Login.vi.resx"), Guid.Parse("00000000-0000-0000-0000-000000000009")) ] ), new( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.resx"), Parse("00000000-0000-0000-0000-000000000011")), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.resx"), Guid.Parse("00000000-0000-0000-0000-000000000011")), subFiles: [ - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.da.resx"), Parse("00000000-0000-0000-0000-000000000010")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.vi.resx"), Parse("00000000-0000-0000-0000-000000000012")) + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.da.resx"), Guid.Parse("00000000-0000-0000-0000-000000000010")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.vi.resx"), Guid.Parse("00000000-0000-0000-0000-000000000012")) ] ), new( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.resx"), Parse("00000000-0000-0000-0000-000000000026")), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.resx"), Guid.Parse("00000000-0000-0000-0000-000000000026")), subFiles: [ - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.cs-cz.resx"), Parse("00000000-0000-0000-0000-000000000013")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.da.resx"), Parse("00000000-0000-0000-0000-000000000014")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.de.resx"), Parse("00000000-0000-0000-0000-000000000015")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.es.resx"), Parse("00000000-0000-0000-0000-000000000016")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.fi.resx"), Parse("00000000-0000-0000-0000-000000000017")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.fr.resx"), Parse("00000000-0000-0000-0000-000000000018")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.it.resx"), Parse("00000000-0000-0000-0000-000000000019")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.lt.resx"), Parse("00000000-0000-0000-0000-000000000020")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.lv.resx"), Parse("00000000-0000-0000-0000-000000000021")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.nb-no.resx"), Parse("00000000-0000-0000-0000-000000000022")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.nl.resx"), Parse("00000000-0000-0000-0000-000000000023")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.nn-no.resx"), Parse("00000000-0000-0000-0000-000000000024")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.pl.resx"), Parse("00000000-0000-0000-0000-000000000025")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.ru.resx"), Parse("00000000-0000-0000-0000-000000000027")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.sv.resx"), Parse("00000000-0000-0000-0000-000000000028")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.tr.resx"), Parse("00000000-0000-0000-0000-000000000029")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.vi.resx"), Parse("00000000-0000-0000-0000-000000000030")), - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.zh-cn.resx"), Parse("00000000-0000-0000-0000-000000000031")) + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.cs-cz.resx"), Guid.Parse("00000000-0000-0000-0000-000000000013")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.da.resx"), Guid.Parse("00000000-0000-0000-0000-000000000014")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.de.resx"), Guid.Parse("00000000-0000-0000-0000-000000000015")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.es.resx"), Guid.Parse("00000000-0000-0000-0000-000000000016")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.fi.resx"), Guid.Parse("00000000-0000-0000-0000-000000000017")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.fr.resx"), Guid.Parse("00000000-0000-0000-0000-000000000018")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.it.resx"), Guid.Parse("00000000-0000-0000-0000-000000000019")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.lt.resx"), Guid.Parse("00000000-0000-0000-0000-000000000020")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.lv.resx"), Guid.Parse("00000000-0000-0000-0000-000000000021")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.nb-no.resx"), Guid.Parse("00000000-0000-0000-0000-000000000022")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.nl.resx"), Guid.Parse("00000000-0000-0000-0000-000000000023")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.nn-no.resx"), Guid.Parse("00000000-0000-0000-0000-000000000024")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.pl.resx"), Guid.Parse("00000000-0000-0000-0000-000000000025")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.ru.resx"), Guid.Parse("00000000-0000-0000-0000-000000000027")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.sv.resx"), Guid.Parse("00000000-0000-0000-0000-000000000028")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.tr.resx"), Guid.Parse("00000000-0000-0000-0000-000000000029")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.vi.resx"), Guid.Parse("00000000-0000-0000-0000-000000000030")), + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.zh-cn.resx"), Guid.Parse("00000000-0000-0000-0000-000000000031")) ]), new( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation.resx"), Parse("00000000-0000-0000-0000-000000000033")), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation.resx"), Guid.Parse("00000000-0000-0000-0000-000000000033")), subFiles: [ - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation.da.resx"), Parse("00000000-0000-0000-0000-000000000032")) + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation.da.resx"), Guid.Parse("00000000-0000-0000-0000-000000000032")) ] ), new( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation2.resx"), Parse("00000000-0000-0000-0000-000000000034")), + mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation2.resx"), Guid.Parse("00000000-0000-0000-0000-000000000034")), subFiles: [] ) }; @@ -209,34 +208,34 @@ public void FileGrouping() [Fact] public void ResxGrouping() { - var result = GroupResxFiles.DetectChildCombos(GroupResxFiles.Group(s_data.Select(x => new AdditionalTextWithHash(new AdditionalTextStub(x.Path), NewGuid())).OrderBy(_ => NewGuid()).ToArray()).ToArray()).ToList(); + var result = GroupResxFiles.DetectChildCombos(GroupResxFiles.Group(s_data.Select(x => new AdditionalTextWithHash(new AdditionalTextStub(x.Path), Guid.NewGuid())).OrderBy(_ => Guid.NewGuid()).ToArray()).ToArray()).ToList(); var expected = new List { new([ - new AdditionalTextWithHash(new AdditionalTextStub("test.da.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.vi.resx"), NewGuid()) + new AdditionalTextWithHash(new AdditionalTextStub("test.da.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.vi.resx"), Guid.NewGuid()) ]), - new([new AdditionalTextWithHash(new AdditionalTextStub("test.da.resx"), NewGuid())]), + new([new AdditionalTextWithHash(new AdditionalTextStub("test.da.resx"), Guid.NewGuid())]), new([]), new([ - new AdditionalTextWithHash(new AdditionalTextStub("test.cs-cz.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.da.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.de.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.es.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.fi.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.fr.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.it.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.lt.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.lv.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.nb-no.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.nl.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.nn-no.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.pl.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.ru.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.sv.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.tr.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.vi.resx"), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.zh-cn.resx"), NewGuid()) + new AdditionalTextWithHash(new AdditionalTextStub("test.cs-cz.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.da.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.de.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.es.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.fi.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.fr.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.it.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.lt.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.lv.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.nb-no.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.nl.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.nn-no.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.pl.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.ru.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.sv.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.tr.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.vi.resx"), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.zh-cn.resx"), Guid.NewGuid()) ]), }; result.Count.Should().Be(expected.Count); diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da-dk.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da-dk.resx new file mode 100644 index 0000000..b37f7ba --- /dev/null +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da-dk.resx @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + OldestDaDK + + + NewestDaDK + + diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da.resx new file mode 100644 index 0000000..a9eb06d --- /dev/null +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da.resx @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + OldestDa + + + NewestDa + + diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.resx new file mode 100644 index 0000000..67239d7 --- /dev/null +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.resx @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + OldestEnUs + + + NewestEnUs + + diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.resx new file mode 100644 index 0000000..ec0a18e --- /dev/null +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Oldest + + + Newest + + \ No newline at end of file diff --git a/Aigamo.ResXGenerator.Tests/LocalizerRegisterTests.cs b/Aigamo.ResXGenerator.Tests/LocalizerRegisterTests.cs new file mode 100644 index 0000000..2372125 --- /dev/null +++ b/Aigamo.ResXGenerator.Tests/LocalizerRegisterTests.cs @@ -0,0 +1,77 @@ +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Generators; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; +using FluentAssertions; +using Xunit; + +namespace Aigamo.ResXGenerator.Tests; + +public class LocalizerRegisterTests +{ + private static void Generate( + ILocalRegisterGenerator generator, + bool nullForgivingOperators = false) + { + string expected = $$""" + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + {{nullForgivingOperators.InterpolateCondition("#nullable disable", "#nullable enable")}} + using Microsoft.Extensions.DependencyInjection; + + namespace VocaDb.Web.App_GlobalResources; + + public static class VocaDbWebAppGlobalResourcesRegistrationExtensions + { + public static IServiceCollection UsingVocaDbWebAppGlobalResourcesResX(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + return services; + } + } + """; + + var result = generator.Generate( + options: new GenFilesNamespace( + "VocaDb.Web.App_GlobalResources", + [ + new GenFileOptions + { + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", + ClassName = "ActivityEntrySortRuleNames", + NullForgivingOperators = nullForgivingOperators, + }, + new GenFileOptions + { + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.LocalResources", + ClassName = "LocalResources", + NullForgivingOperators = nullForgivingOperators, + } + ] + )); + + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } + + [Fact] + public void CanRegisterLocalClass() + { + var generator = new LocalizerRegisterGenerator(); + Generate(generator); + } + + [Fact] + public void CanRegisterLocalClassWithNullForgivingOperators() + { + var generator = new LocalizerRegisterGenerator(); + Generate(generator, true); + } +} diff --git a/Aigamo.ResXGenerator.Tests/SettingsTests.cs b/Aigamo.ResXGenerator.Tests/SettingsTests.cs index ee1efb9..89c615c 100644 --- a/Aigamo.ResXGenerator.Tests/SettingsTests.cs +++ b/Aigamo.ResXGenerator.Tests/SettingsTests.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Aigamo.ResXGenerator.Tools; using FluentAssertions; using Microsoft.CodeAnalysis; @@ -9,393 +9,447 @@ namespace Aigamo.ResXGenerator.Tests; public class SettingsTests { - private static readonly GlobalOptions s_globalOptions = GlobalOptions.Select( - provider: new AnalyzerConfigOptionsProviderStub( - globalOptions: new AnalyzerConfigOptionsStub - { - RootNamespace = "namespace1", - MSBuildProjectFullPath = "project1.csproj", - MSBuildProjectName = "project1", - }, - fileOptions: null! - ), - token: default - ); + private static readonly GlobalOptions s_globalOptions = GlobalOptions.Select( + provider: new AnalyzerConfigOptionsProviderStub( + globalOptions: new AnalyzerConfigOptionsStub + { + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", + MSBuildProjectName = "project1", + }, + fileOptions: null! + ), + token: default + ); - [Fact] - public void GlobalDefaults() - { - var globalOptions = s_globalOptions; - globalOptions.ProjectName.Should().Be("project1"); - globalOptions.RootNamespace.Should().Be("namespace1"); - globalOptions.ProjectFullPath.Should().Be("project1.csproj"); - globalOptions.InnerClassName.Should().BeNullOrEmpty(); - globalOptions.ClassNamePostfix.Should().BeNullOrEmpty(); - globalOptions.InnerClassInstanceName.Should().BeNullOrEmpty(); - globalOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated); - globalOptions.NullForgivingOperators.Should().Be(false); - globalOptions.StaticClass.Should().Be(true); - globalOptions.StaticMembers.Should().Be(true); - globalOptions.PublicClass.Should().Be(false); - globalOptions.PartialClass.Should().Be(false); - globalOptions.IsValid.Should().Be(true); - } + [Fact] + public void GlobalDefaults() + { + var globalOptions = s_globalOptions; + globalOptions.ProjectName.Should().Be("project1"); + globalOptions.RootNamespace.Should().Be("namespace1"); + globalOptions.ProjectFullPath.Should().Be("project1.csproj"); + globalOptions.InnerClassName.Should().BeNullOrEmpty(); + globalOptions.ClassNamePostfix.Should().BeNullOrEmpty(); + globalOptions.InnerClassInstanceName.Should().BeNullOrEmpty(); + globalOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated); + globalOptions.NullForgivingOperators.Should().Be(false); + globalOptions.StaticClass.Should().Be(true); + globalOptions.StaticMembers.Should().Be(true); + globalOptions.PublicClass.Should().Be(false); + globalOptions.PartialClass.Should().Be(false); + globalOptions.IsValid.Should().Be(true); + globalOptions.GenerateCode.Should().Be(false); + globalOptions.GenerationType.Should().Be(GenerationType.ResourceManager); + } - [Fact] - public void GlobalSettings_CanReadAll() - { - var globalOptions = GlobalOptions.Select( - provider: new AnalyzerConfigOptionsProviderStub( - globalOptions: new AnalyzerConfigOptionsStub - { - RootNamespace = "namespace1", - MSBuildProjectFullPath = "project1.csproj", - MSBuildProjectName = "project1", - ResXGenerator_InnerClassName = "test1", - ResXGenerator_InnerClassInstanceName = "test2", - ResXGenerator_ClassNamePostfix= "test3", - ResXGenerator_InnerClassVisibility = "public", - ResXGenerator_NullForgivingOperators = "true", - ResXGenerator_StaticClass = "false", - ResXGenerator_StaticMembers = "false", - ResXGenerator_GenerateCode = "true", - ResXGenerator_PublicClass = "true", - ResXGenerator_PartialClass = "true", - }, - fileOptions: null! - ), - token: default - ); - globalOptions.RootNamespace.Should().Be("namespace1"); - globalOptions.ProjectFullPath.Should().Be("project1.csproj"); - globalOptions.ProjectName.Should().Be("project1"); - globalOptions.InnerClassName.Should().Be("test1"); - globalOptions.InnerClassInstanceName.Should().Be("test2"); - globalOptions.ClassNamePostfix.Should().Be("test3"); - globalOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public); - globalOptions.NullForgivingOperators.Should().Be(true); - globalOptions.StaticClass.Should().Be(false); - globalOptions.GenerateCode.Should().Be(true); - globalOptions.StaticMembers.Should().Be(false); - globalOptions.PublicClass.Should().Be(true); - globalOptions.PartialClass.Should().Be(true); - globalOptions.IsValid.Should().Be(true); - } + [Fact] + public void GlobalSettings_CanReadAll() + { + var globalOptions = GlobalOptions.Select( + provider: new AnalyzerConfigOptionsProviderStub( + globalOptions: new AnalyzerConfigOptionsStub + { + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", + MSBuildProjectName = "project1", + ResXGenerator_InnerClassName = "test1", + ResXGenerator_InnerClassInstanceName = "test2", + ResXGenerator_ClassNamePostfix = "test3", + ResXGenerator_InnerClassVisibility = "public", + ResXGenerator_NullForgivingOperators = "true", + ResXGenerator_StaticClass = "false", + ResXGenerator_StaticMembers = "false", + ResXGenerator_GenerateCode = "true", + ResXGenerator_PublicClass = "true", + ResXGenerator_PartialClass = "true", + ResXGenerator_GenerationType = "ResourceManager" + }, + fileOptions: null! + ), + token: default + ); + globalOptions.RootNamespace.Should().Be("namespace1"); + globalOptions.ProjectFullPath.Should().Be("project1.csproj"); + globalOptions.ProjectName.Should().Be("project1"); + globalOptions.InnerClassName.Should().Be("test1"); + globalOptions.InnerClassInstanceName.Should().Be("test2"); + globalOptions.ClassNamePostfix.Should().Be("test3"); + globalOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public); + globalOptions.NullForgivingOperators.Should().Be(true); + globalOptions.StaticClass.Should().Be(false); + globalOptions.GenerateCode.Should().Be(true); + globalOptions.StaticMembers.Should().Be(false); + globalOptions.PublicClass.Should().Be(true); + globalOptions.PartialClass.Should().Be(true); + globalOptions.IsValid.Should().Be(true); + globalOptions.GenerationType.Should().Be(GenerationType.ResourceManager); + } - [Fact] - public void FileDefaults() - { - var fileOptions = GenFileOptions.Select( - file: new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), - subFiles: Array.Empty() - ), - options: new AnalyzerConfigOptionsProviderStub( - globalOptions: null!, - fileOptions: new AnalyzerConfigOptionsStub() - ), - globalOptions: s_globalOptions - ); - fileOptions.InnerClassName.Should().BeNullOrEmpty(); - fileOptions.InnerClassInstanceName.Should().BeNullOrEmpty(); - fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated); - fileOptions.NullForgivingOperators.Should().Be(false); - fileOptions.StaticClass.Should().Be(true); - fileOptions.StaticMembers.Should().Be(true); - fileOptions.PublicClass.Should().Be(false); - fileOptions.PartialClass.Should().Be(false); - fileOptions.GenerateCode.Should().Be(false); - fileOptions.LocalNamespace.Should().Be("namespace1"); - fileOptions.CustomToolNamespace.Should().BeNullOrEmpty(); - fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx"); - fileOptions.ClassName.Should().Be("Path1"); - fileOptions.SkipFile.Should().Be(false); - fileOptions.IsValid.Should().Be(true); - } + [Fact] + public void FileDefaults() + { + var fileOptions = GenFileOptions.Select( + file: new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), + subFiles: [] + ), + options: new AnalyzerConfigOptionsProviderStub( + globalOptions: null!, + fileOptions: new AnalyzerConfigOptionsStub() + ), + globalOptions: s_globalOptions + ); + fileOptions.InnerClassName.Should().BeNullOrEmpty(); + fileOptions.InnerClassInstanceName.Should().BeNullOrEmpty(); + fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated); + fileOptions.NullForgivingOperators.Should().Be(false); + fileOptions.StaticClass.Should().Be(true); + fileOptions.StaticMembers.Should().Be(true); + fileOptions.PublicClass.Should().Be(false); + fileOptions.PartialClass.Should().Be(false); + fileOptions.GenerateCode.Should().Be(false); + fileOptions.LocalNamespace.Should().Be("namespace1"); + fileOptions.CustomToolNamespace.Should().BeNullOrEmpty(); + fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx"); + fileOptions.ClassName.Should().Be("Path1"); + fileOptions.SkipFile.Should().Be(false); + fileOptions.IsValid.Should().Be(true); + fileOptions.GenerationType.Should().Be(GenerationType.ResourceManager); + } - [Theory] - [InlineData("project1.csproj", "Path1.resx", null, "project1", "project1.Path1")] - [InlineData("project1.csproj", "Path1.resx", "", "project1", "project1.Path1")] - [InlineData("project1.csproj", "Path1.resx", "rootNamespace", "rootNamespace", "rootNamespace.Path1")] - [InlineData(@"ProjectFolder/project1.csproj", @"ProjectFolder/SubFolder/Path1.resx", "rootNamespace", "rootNamespace.SubFolder", "rootNamespace.SubFolder.Path1")] - [InlineData(@"ProjectFolder/project1.csproj", @"ProjectFolder/SubFolder With Space/Path1.resx", "rootNamespace", "rootNamespace.SubFolder_With_Space", "rootNamespace.SubFolder_With_Space.Path1")] - [InlineData(@"ProjectFolder/project1.csproj", @"ProjectFolder/SubFolder/Path1.resx", null, "SubFolder", "SubFolder.Path1")] - [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/Path1.resx", null, "_8_project", "_8_project.Path1")] - [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/Path1.resx", "", "_8_project", "_8_project.Path1")] - [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/SubFolder/Path1.resx", null, "SubFolder", "SubFolder.Path1")] - [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/SubFolder/Path1.resx", "", "SubFolder", "SubFolder.Path1")] - public void FileSettings_RespectsEmptyRootNamespace( - string msBuildProjectFullPath, - string mainFile, - string rootNamespace, - string expectedLocalNamespace, - string expectedEmbeddedFilename - ) - { - var fileOptions = GenFileOptions.Select( - file: new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(mainFile), Guid.NewGuid()), - subFiles: Array.Empty() - ), - options: new AnalyzerConfigOptionsProviderStub( - globalOptions: null!, - fileOptions: new AnalyzerConfigOptionsStub() - ), - globalOptions: GlobalOptions.Select( - provider: new AnalyzerConfigOptionsProviderStub( - globalOptions: new AnalyzerConfigOptionsStub - { - MSBuildProjectName = Path.GetFileNameWithoutExtension(msBuildProjectFullPath), - RootNamespace = rootNamespace, - MSBuildProjectFullPath = msBuildProjectFullPath - }, - fileOptions: null! - ), - token: default - ) - ); - fileOptions.InnerClassName.Should().BeNullOrEmpty(); - fileOptions.InnerClassInstanceName.Should().BeNullOrEmpty(); - fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated); - fileOptions.NullForgivingOperators.Should().Be(false); - fileOptions.StaticClass.Should().Be(true); - fileOptions.StaticMembers.Should().Be(true); - fileOptions.PublicClass.Should().Be(false); - fileOptions.PartialClass.Should().Be(false); - fileOptions.GenerateCode.Should().Be(false); - fileOptions.LocalNamespace.Should().Be(expectedLocalNamespace); - fileOptions.CustomToolNamespace.Should().BeNullOrEmpty(); - fileOptions.GroupedFile.MainFile.File.Path.Should().Be(mainFile); - fileOptions.EmbeddedFilename.Should().Be(expectedEmbeddedFilename); - fileOptions.ClassName.Should().Be("Path1"); - fileOptions.IsValid.Should().Be(true); - } + [Theory] + [InlineData("project1.csproj", "Path1.resx", null, "project1", "project1.Path1")] + [InlineData("project1.csproj", "Path1.resx", "", "project1", "project1.Path1")] + [InlineData("project1.csproj", "Path1.resx", "rootNamespace", "rootNamespace", "rootNamespace.Path1")] + [InlineData(@"ProjectFolder/project1.csproj", @"ProjectFolder/SubFolder/Path1.resx", "rootNamespace", "rootNamespace.SubFolder", "rootNamespace.SubFolder.Path1")] + [InlineData(@"ProjectFolder/project1.csproj", @"ProjectFolder/SubFolder With Space/Path1.resx", "rootNamespace", "rootNamespace.SubFolder_With_Space", "rootNamespace.SubFolder_With_Space.Path1")] + [InlineData(@"ProjectFolder/project1.csproj", @"ProjectFolder/SubFolder/Path1.resx", null, "SubFolder", "SubFolder.Path1")] + [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/Path1.resx", null, "_8_project", "_8_project.Path1")] + [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/Path1.resx", "", "_8_project", "_8_project.Path1")] + [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/SubFolder/Path1.resx", null, "SubFolder", "SubFolder.Path1")] + [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/SubFolder/Path1.resx", "", "SubFolder", "SubFolder.Path1")] + public void FileSettings_RespectsEmptyRootNamespace( + string msBuildProjectFullPath, + string mainFile, + string rootNamespace, + string expectedLocalNamespace, + string expectedEmbeddedFilename + ) + { + var fileOptions = GenFileOptions.Select( + file: new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub(mainFile), Guid.NewGuid()), + subFiles: [] + ), + options: new AnalyzerConfigOptionsProviderStub( + globalOptions: null!, + fileOptions: new AnalyzerConfigOptionsStub() + ), + globalOptions: GlobalOptions.Select( + provider: new AnalyzerConfigOptionsProviderStub( + globalOptions: new AnalyzerConfigOptionsStub + { + MSBuildProjectName = Path.GetFileNameWithoutExtension(msBuildProjectFullPath), + RootNamespace = rootNamespace, + MSBuildProjectFullPath = msBuildProjectFullPath + }, + fileOptions: null! + ), + token: default + ) + ); + fileOptions.InnerClassName.Should().BeNullOrEmpty(); + fileOptions.InnerClassInstanceName.Should().BeNullOrEmpty(); + fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated); + fileOptions.NullForgivingOperators.Should().Be(false); + fileOptions.StaticClass.Should().Be(true); + fileOptions.StaticMembers.Should().Be(true); + fileOptions.PublicClass.Should().Be(false); + fileOptions.PartialClass.Should().Be(false); + fileOptions.GenerateCode.Should().Be(false); + fileOptions.LocalNamespace.Should().Be(expectedLocalNamespace); + fileOptions.CustomToolNamespace.Should().BeNullOrEmpty(); + fileOptions.GroupedFile.MainFile.File.Path.Should().Be(mainFile); + fileOptions.EmbeddedFilename.Should().Be(expectedEmbeddedFilename); + fileOptions.ClassName.Should().Be("Path1"); + fileOptions.IsValid.Should().Be(true); + fileOptions.GenerationType.Should().Be(GenerationType.ResourceManager); + } - [Fact] - public void File_PostFix() - { - var fileOptions = GenFileOptions.Select( - file: new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), - subFiles: Array.Empty() - ), - options: new AnalyzerConfigOptionsProviderStub( - globalOptions: null!, - fileOptions: new AnalyzerConfigOptionsStub { ClassNamePostfix = "test1" } - ), - globalOptions: s_globalOptions - ); - fileOptions.ClassName.Should().Be("Path1test1"); - fileOptions.IsValid.Should().Be(true); - } + [Fact] + public void File_PostFix() + { + var fileOptions = GenFileOptions.Select( + file: new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), + subFiles: [] + ), + options: new AnalyzerConfigOptionsProviderStub( + globalOptions: null!, + fileOptions: new AnalyzerConfigOptionsStub { ClassNamePostfix = "test1" } + ), + globalOptions: s_globalOptions + ); + fileOptions.ClassName.Should().Be("Path1test1"); + fileOptions.IsValid.Should().Be(true); + } - [Fact] - public void FileSettings_CanReadAll() - { - var fileOptions = GenFileOptions.Select( - file: new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), - subFiles: Array.Empty() - ), - options: new AnalyzerConfigOptionsProviderStub( - globalOptions: null!, - fileOptions: new AnalyzerConfigOptionsStub - { - RootNamespace = "namespace1", MSBuildProjectFullPath = "project1.csproj", - CustomToolNamespace = "ns1", - InnerClassName = "test1", - InnerClassInstanceName = "test2", - InnerClassVisibility = "public", - NullForgivingOperators = "true", - StaticClass = "false", - StaticMembers = "false", - PublicClass = "true", - PartialClass = "true", - GenerateCode = "true", - } - ), - globalOptions: s_globalOptions - ); - fileOptions.InnerClassName.Should().Be("test1"); - fileOptions.InnerClassInstanceName.Should().Be("test2"); - fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public); - fileOptions.NullForgivingOperators.Should().Be(false); - fileOptions.StaticClass.Should().Be(false); - fileOptions.StaticMembers.Should().Be(false); - fileOptions.PublicClass.Should().Be(true); - fileOptions.PartialClass.Should().Be(true); - fileOptions.IsValid.Should().Be(true); - fileOptions.GenerateCode.Should().Be(true); - fileOptions.LocalNamespace.Should().Be("namespace1"); - fileOptions.CustomToolNamespace.Should().Be("ns1"); - fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx"); - fileOptions.ClassName.Should().Be("Path1"); - } + [Fact] + public void FileSettings_CanReadAll() + { + var fileOptions = GenFileOptions.Select( + file: new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), + subFiles: [] + ), + options: new AnalyzerConfigOptionsProviderStub( + globalOptions: null!, + fileOptions: new AnalyzerConfigOptionsStub + { + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", + CustomToolNamespace = "ns1", + InnerClassName = "test1", + InnerClassInstanceName = "test2", + InnerClassVisibility = "public", + NullForgivingOperators = "true", + StaticClass = "false", + StaticMembers = "false", + PublicClass = "true", + PartialClass = "true", + GenerateCode = "true", + GenerationType = "ResourceManager" + } + ), + globalOptions: s_globalOptions + ); + fileOptions.InnerClassName.Should().Be("test1"); + fileOptions.InnerClassInstanceName.Should().Be("test2"); + fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public); + fileOptions.NullForgivingOperators.Should().Be(false); + fileOptions.StaticClass.Should().Be(false); + fileOptions.StaticMembers.Should().Be(false); + fileOptions.PublicClass.Should().Be(true); + fileOptions.PartialClass.Should().Be(true); + fileOptions.IsValid.Should().Be(true); + fileOptions.GenerateCode.Should().Be(true); + fileOptions.LocalNamespace.Should().Be("namespace1"); + fileOptions.CustomToolNamespace.Should().Be("ns1"); + fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx"); + fileOptions.ClassName.Should().Be("Path1"); + fileOptions.GenerationType.Should().Be(GenerationType.ResourceManager); + } - [Fact] - public void FileSettings_RespectsGlobalDefaults() - { - var globalOptions = GlobalOptions.Select( - provider: new AnalyzerConfigOptionsProviderStub( - globalOptions: new AnalyzerConfigOptionsStub - { - RootNamespace = "namespace1", - MSBuildProjectFullPath = "project1.csproj", - MSBuildProjectName = "project1", - ResXGenerator_InnerClassName = "test1", - ResXGenerator_InnerClassInstanceName = "test2", - ResXGenerator_ClassNamePostfix= "test3", - ResXGenerator_InnerClassVisibility = "public", - ResXGenerator_NullForgivingOperators = "true", - ResXGenerator_StaticClass = "false", - ResXGenerator_StaticMembers = "false", - ResXGenerator_PublicClass = "true", - ResXGenerator_PartialClass = "true", - }, - fileOptions: null! - ), - token: default - ); - var fileOptions = GenFileOptions.Select( - file: new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), - subFiles: Array.Empty() - ), - options: new AnalyzerConfigOptionsProviderStub( - globalOptions: null!, - fileOptions: new AnalyzerConfigOptionsStub() - ), - globalOptions: globalOptions - ); - fileOptions.InnerClassName.Should().Be("test1"); - fileOptions.InnerClassInstanceName.Should().Be("test2"); - fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public); - fileOptions.NullForgivingOperators.Should().Be(true); - fileOptions.StaticClass.Should().Be(false); - fileOptions.StaticMembers.Should().Be(false); - fileOptions.PublicClass.Should().Be(true); - fileOptions.PartialClass.Should().Be(true); - fileOptions.GenerateCode.Should().Be(false); - fileOptions.LocalNamespace.Should().Be("namespace1"); - fileOptions.CustomToolNamespace.Should().BeNullOrEmpty(); - fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx"); - fileOptions.ClassName.Should().Be("Path1test3"); - fileOptions.IsValid.Should().Be(true); - } + [Fact] + public void FileSettings_RespectsGlobalDefaults() + { + var globalOptions = GlobalOptions.Select( + provider: new AnalyzerConfigOptionsProviderStub( + globalOptions: new AnalyzerConfigOptionsStub + { + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", + MSBuildProjectName = "project1", + ResXGenerator_InnerClassName = "test1", + ResXGenerator_InnerClassInstanceName = "test2", + ResXGenerator_ClassNamePostfix = "test3", + ResXGenerator_InnerClassVisibility = "public", + ResXGenerator_NullForgivingOperators = "true", + ResXGenerator_StaticClass = "false", + ResXGenerator_StaticMembers = "false", + ResXGenerator_PublicClass = "true", + ResXGenerator_PartialClass = "true", + }, + fileOptions: null! + ), + token: default + ); + var fileOptions = GenFileOptions.Select( + file: new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), + subFiles: [] + ), + options: new AnalyzerConfigOptionsProviderStub( + globalOptions: null!, + fileOptions: new AnalyzerConfigOptionsStub() + ), + globalOptions: globalOptions + ); + fileOptions.InnerClassName.Should().Be("test1"); + fileOptions.InnerClassInstanceName.Should().Be("test2"); + fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public); + fileOptions.NullForgivingOperators.Should().Be(true); + fileOptions.StaticClass.Should().Be(false); + fileOptions.StaticMembers.Should().Be(false); + fileOptions.PublicClass.Should().Be(true); + fileOptions.PartialClass.Should().Be(true); + fileOptions.GenerateCode.Should().Be(false); + fileOptions.LocalNamespace.Should().Be("namespace1"); + fileOptions.CustomToolNamespace.Should().BeNullOrEmpty(); + fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx"); + fileOptions.ClassName.Should().Be("Path1test3"); + fileOptions.IsValid.Should().Be(true); + } - [Fact] - public void FileSettings_CanSkipIndividualFile() - { - var fileOptions = GenFileOptions.Select( - file: new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), - subFiles: Array.Empty() - ), - options: new AnalyzerConfigOptionsProviderStub( - globalOptions: null!, - fileOptions: new AnalyzerConfigOptionsStub - { - RootNamespace = "namespace1", - MSBuildProjectFullPath = "project1.csproj", - SkipFile = "true", - } - ), - globalOptions: s_globalOptions - ); + [Fact] + public void FileSettings_CanSkipIndividualFile() + { + var fileOptions = GenFileOptions.Select( + file: new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), + subFiles: [] + ), + options: new AnalyzerConfigOptionsProviderStub( + globalOptions: null!, + fileOptions: new AnalyzerConfigOptionsStub + { + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", + SkipFile = "true", + } + ), + globalOptions: s_globalOptions + ); - fileOptions.LocalNamespace.Should().Be("namespace1"); - fileOptions.SkipFile.Should().Be(true); - fileOptions.IsValid.Should().Be(true); - } + fileOptions.LocalNamespace.Should().Be("namespace1"); + fileOptions.SkipFile.Should().Be(true); + fileOptions.IsValid.Should().Be(true); + } + [Fact] + public void FileSettings_CanSwitchGenerationType() + { + var fileOptions = GenFileOptions.Select( + file: new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), + subFiles: [] + ), + options: new AnalyzerConfigOptionsProviderStub( + globalOptions: null!, + fileOptions: new AnalyzerConfigOptionsStub + { + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", + GenerationType = "StringLocalizer", + } + ), + globalOptions: s_globalOptions + ); + fileOptions.LocalNamespace.Should().Be("namespace1"); + fileOptions.GenerationType.Should().Be(GenerationType.StringLocalizer); + fileOptions.IsValid.Should().Be(true); + } - private class AnalyzerConfigOptionsStub : AnalyzerConfigOptions - { - // ReSharper disable InconsistentNaming - public string? MSBuildProjectFullPath { get; init; } - // ReSharper disable InconsistentNaming - public string? MSBuildProjectName { get; init; } - public string? RootNamespace { get; init; } - public string? ResXGenerator_ClassNamePostfix { get; init; } - public string? ResXGenerator_PublicClass { get; init; } - public string? ResXGenerator_NullForgivingOperators { get; init; } - public string? ResXGenerator_StaticClass { get; init; } - public string? ResXGenerator_StaticMembers { get; init; } - public string? ResXGenerator_PartialClass { get; init; } - public string? ResXGenerator_InnerClassVisibility { get; init; } - public string? ResXGenerator_InnerClassName { get; init; } - public string? ResXGenerator_InnerClassInstanceName { get; init; } - public string? ResXGenerator_GenerateCode { get; init; } - public string? CustomToolNamespace { get; init; } - public string? TargetPath { get; init; } - public string? ClassNamePostfix { get; init; } - public string? PublicClass { get; init; } - public string? NullForgivingOperators { get; init; } - public string? StaticClass { get; init; } - public string? StaticMembers { get; init; } - public string? PartialClass { get; init; } - public string? InnerClassVisibility { get; init; } - public string? InnerClassName { get; init; } - public string? InnerClassInstanceName { get; init; } - public string? GenerateCode { get; init; } - public string? SkipFile { get; init; } + [Fact] + public void GlobalOptions_CanSwitchGenerationType() + { + var globalOptions = GlobalOptions.Select( + provider: new AnalyzerConfigOptionsProviderStub( + globalOptions: new AnalyzerConfigOptionsStub + { + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", + MSBuildProjectName = "project1", + ResXGenerator_GenerationType = "StringLocalizer" + }, + fileOptions: null! + ), + token: default + ); - // ReSharper restore InconsistentNaming + globalOptions.RootNamespace.Should().Be("namespace1"); + globalOptions.GenerationType.Should().Be(GenerationType.StringLocalizer); + globalOptions.IsValid.Should().Be(true); + } - public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) - { - string? GetVal() => - key switch - { - "build_property.MSBuildProjectFullPath" => MSBuildProjectFullPath, - "build_property.MSBuildProjectName" => MSBuildProjectName, - "build_property.RootNamespace" => RootNamespace, - "build_property.ResXGenerator_GenerateCode" => ResXGenerator_GenerateCode, - "build_property.ResXGenerator_ClassNamePostfix" => ResXGenerator_ClassNamePostfix, - "build_property.ResXGenerator_PublicClass" => ResXGenerator_PublicClass, - "build_property.ResXGenerator_NullForgivingOperators" => ResXGenerator_NullForgivingOperators, - "build_property.ResXGenerator_StaticClass" => ResXGenerator_StaticClass, - "build_property.ResXGenerator_StaticMembers" => ResXGenerator_StaticMembers, - "build_property.ResXGenerator_PartialClass" => ResXGenerator_PartialClass, - "build_property.ResXGenerator_InnerClassVisibility" => ResXGenerator_InnerClassVisibility, - "build_property.ResXGenerator_InnerClassName" => ResXGenerator_InnerClassName, - "build_property.ResXGenerator_InnerClassInstanceName" => ResXGenerator_InnerClassInstanceName, - "build_metadata.EmbeddedResource.CustomToolNamespace" => CustomToolNamespace, - "build_metadata.EmbeddedResource.TargetPath" => TargetPath, - "build_metadata.EmbeddedResource.ClassNamePostfix" => ClassNamePostfix, - "build_metadata.EmbeddedResource.PublicClass" => PublicClass, - "build_metadata.EmbeddedResource.NullForgivingOperators" => NullForgivingOperators, - "build_metadata.EmbeddedResource.StaticClass" => StaticClass, - "build_metadata.EmbeddedResource.StaticMembers" => StaticMembers, - "build_metadata.EmbeddedResource.PartialClass" => PartialClass, - "build_metadata.EmbeddedResource.InnerClassVisibility" => InnerClassVisibility, - "build_metadata.EmbeddedResource.InnerClassName" => InnerClassName, - "build_metadata.EmbeddedResource.InnerClassInstanceName" => InnerClassInstanceName, - "build_metadata.EmbeddedResource.GenerateCode" => GenerateCode, - "build_metadata.EmbeddedResource.SkipFile" => SkipFile, - _ => null - }; - value = GetVal(); - return value is not null; - } - } + private class AnalyzerConfigOptionsStub : AnalyzerConfigOptions + { + // ReSharper disable InconsistentNaming + public string? MSBuildProjectFullPath { get; init; } + // ReSharper disable InconsistentNaming + public string? MSBuildProjectName { get; init; } + public string? RootNamespace { get; init; } + public string? ResXGenerator_ClassNamePostfix { get; init; } + public string? ResXGenerator_PublicClass { get; init; } + public string? ResXGenerator_NullForgivingOperators { get; init; } + public string? ResXGenerator_StaticClass { get; init; } + public string? ResXGenerator_StaticMembers { get; init; } + public string? ResXGenerator_PartialClass { get; init; } + public string? ResXGenerator_InnerClassVisibility { get; init; } + public string? ResXGenerator_InnerClassName { get; init; } + public string? ResXGenerator_InnerClassInstanceName { get; init; } + public string? ResXGenerator_GenerateCode { get; init; } + public string? ResXGenerator_GenerationType { get; init; } + public string? CustomToolNamespace { get; init; } + public string? TargetPath { get; init; } + public string? ClassNamePostfix { get; init; } + public string? PublicClass { get; init; } + public string? NullForgivingOperators { get; init; } + public string? StaticClass { get; init; } + public string? StaticMembers { get; init; } + public string? PartialClass { get; init; } + public string? InnerClassVisibility { get; init; } + public string? InnerClassName { get; init; } + public string? InnerClassInstanceName { get; init; } + public string? GenerateCode { get; init; } + public string? GenerationType { get; init; } + public string? SkipFile { get; init; } - private class AnalyzerConfigOptionsProviderStub : AnalyzerConfigOptionsProvider - { - private readonly AnalyzerConfigOptions _fileOptions; + // ReSharper restore InconsistentNaming - public override AnalyzerConfigOptions GlobalOptions { get; } + public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) + { + string? GetVal() + { + return key switch + { + "build_property.MSBuildProjectFullPath" => MSBuildProjectFullPath, + "build_property.MSBuildProjectName" => MSBuildProjectName, + "build_property.RootNamespace" => RootNamespace, + "build_property.ResXGenerator_GenerateCode" => ResXGenerator_GenerateCode, + "build_property.ResXGenerator_ClassNamePostfix" => ResXGenerator_ClassNamePostfix, + "build_property.ResXGenerator_PublicClass" => ResXGenerator_PublicClass, + "build_property.ResXGenerator_NullForgivingOperators" => ResXGenerator_NullForgivingOperators, + "build_property.ResXGenerator_StaticClass" => ResXGenerator_StaticClass, + "build_property.ResXGenerator_StaticMembers" => ResXGenerator_StaticMembers, + "build_property.ResXGenerator_PartialClass" => ResXGenerator_PartialClass, + "build_property.ResXGenerator_InnerClassVisibility" => ResXGenerator_InnerClassVisibility, + "build_property.ResXGenerator_InnerClassName" => ResXGenerator_InnerClassName, + "build_property.ResXGenerator_InnerClassInstanceName" => ResXGenerator_InnerClassInstanceName, + "build_property.ResXGenerator_GenerationType" => ResXGenerator_GenerationType, + "build_metadata.EmbeddedResource.CustomToolNamespace" => CustomToolNamespace, + "build_metadata.EmbeddedResource.TargetPath" => TargetPath, + "build_metadata.EmbeddedResource.ClassNamePostfix" => ClassNamePostfix, + "build_metadata.EmbeddedResource.PublicClass" => PublicClass, + "build_metadata.EmbeddedResource.NullForgivingOperators" => NullForgivingOperators, + "build_metadata.EmbeddedResource.StaticClass" => StaticClass, + "build_metadata.EmbeddedResource.StaticMembers" => StaticMembers, + "build_metadata.EmbeddedResource.PartialClass" => PartialClass, + "build_metadata.EmbeddedResource.InnerClassVisibility" => InnerClassVisibility, + "build_metadata.EmbeddedResource.InnerClassName" => InnerClassName, + "build_metadata.EmbeddedResource.InnerClassInstanceName" => InnerClassInstanceName, + "build_metadata.EmbeddedResource.GenerateCode" => GenerateCode, + "build_metadata.EmbeddedResource.SkipFile" => SkipFile, + "build_metadata.EmbeddedResource.GenerationType" => GenerationType, + _ => null + }; + } - public AnalyzerConfigOptionsProviderStub(AnalyzerConfigOptions globalOptions, AnalyzerConfigOptions fileOptions) - { - _fileOptions = fileOptions; - GlobalOptions = globalOptions; - } + value = GetVal(); + return value is not null; + } + } - public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException(); + private class AnalyzerConfigOptionsProviderStub(AnalyzerConfigOptions globalOptions, AnalyzerConfigOptions fileOptions) : AnalyzerConfigOptionsProvider + { + private readonly AnalyzerConfigOptions _fileOptions = fileOptions; - public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => _fileOptions; + public override AnalyzerConfigOptions GlobalOptions { get; } = globalOptions; - } + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException(); + + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => _fileOptions; + } } diff --git a/Aigamo.ResXGenerator.Tests/UtilitiesTests.cs b/Aigamo.ResXGenerator.Tests/UtilitiesTests.cs index 80e5187..19b42fc 100644 --- a/Aigamo.ResXGenerator.Tests/UtilitiesTests.cs +++ b/Aigamo.ResXGenerator.Tests/UtilitiesTests.cs @@ -1,41 +1,43 @@ -using System.Diagnostics.CodeAnalysis; using FluentAssertions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; using Xunit; namespace Aigamo.ResXGenerator.Tests; public class UtilitiesTests { + [Theory] + [InlineData("Valid", "Valid")] + [InlineData("_Valid", "_Valid")] + [InlineData("Valid123", "Valid123")] + [InlineData("Valid_123", "Valid_123")] + [InlineData("Valid.123", "Valid.123")] + [InlineData("8Ns", "_8Ns")] + [InlineData("Ns+InvalidChar", "Ns_InvalidChar")] + [InlineData("Ns..Folder...Folder2", "Ns.Folder.Folder2")] + [InlineData("Ns.Folder.", "Ns.Folder")] + [InlineData(".Ns.Folder", "Ns.Folder")] + [InlineData("Folder with space", "Folder_with_space")] + [InlineData("folder with .. space", "folder_with_._space")] + public void SanitizeNamespace(string input, string expected) => Utilities.SanitizeNamespace(input).Should().Be(expected); + [Theory] + [InlineData("Valid", "Valid")] + [InlineData(".Valid", ".Valid")] + [InlineData("8Ns", "8Ns")] + [InlineData("..Ns", ".Ns")] + public void SanitizeNamespaceWithoutFirstCharRules(string input, string expected) => Utilities.SanitizeNamespace(input, false).Should().Be(expected); - [Theory] - [InlineData("Valid", "Valid")] - [InlineData("_Valid", "_Valid")] - [InlineData("Valid123", "Valid123")] - [InlineData("Valid_123", "Valid_123")] - [InlineData("Valid.123", "Valid.123")] - [InlineData("8Ns", "_8Ns")] - [InlineData("Ns+InvalidChar", "Ns_InvalidChar")] - [InlineData("Ns..Folder...Folder2", "Ns.Folder.Folder2")] - [InlineData("Ns.Folder.", "Ns.Folder")] - [InlineData(".Ns.Folder", "Ns.Folder")] - [InlineData("Folder with space", "Folder_with_space")] - [InlineData("folder with .. space", "folder_with_._space")] - public void SanitizeNamespace(string input, string expected) - { - Utilities.SanitizeNamespace(input).Should().Be(expected); - } - - [Theory] - [InlineData("Valid", "Valid")] - [InlineData(".Valid", ".Valid")] - [InlineData("8Ns", "8Ns")] - [InlineData("..Ns", ".Ns")] - public void SanitizeNamespaceWithoutFirstCharRules(string input, string expected) - { - Utilities.SanitizeNamespace(input, false).Should().Be(expected); - } + [Fact] + public void GetLocalNamespace_ShouldNotGenerateIllegalNamespace() + { + var ns = Utilities.GetLocalNamespace("resx", "asd.asd", "path", "name", "root"); + ns.Should().Be("root"); + } + [Fact] + public void ResxFileName_ShouldNotGenerateIllegalClassNames() + { + var ns = Utilities.GetClassNameFromPath("test.cshtml.resx"); + ns.Should().Be("test"); + } } diff --git a/Aigamo.ResXGenerator.sln b/Aigamo.ResXGenerator.sln index 120762c..a5441e8 100644 --- a/Aigamo.ResXGenerator.sln +++ b/Aigamo.ResXGenerator.sln @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoLocalization", "DemoLocalization\DemoLocalization.csproj", "{871D926F-6C45-4480-9640-26D899C4FA4A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {47BDEBDC-42BF-4A8E-9825-FC5ECC844542}.Debug|Any CPU.Build.0 = Debug|Any CPU {47BDEBDC-42BF-4A8E-9825-FC5ECC844542}.Release|Any CPU.ActiveCfg = Release|Any CPU {47BDEBDC-42BF-4A8E-9825-FC5ECC844542}.Release|Any CPU.Build.0 = Release|Any CPU + {871D926F-6C45-4480-9640-26D899C4FA4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {871D926F-6C45-4480-9640-26D899C4FA4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {871D926F-6C45-4480-9640-26D899C4FA4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {871D926F-6C45-4480-9640-26D899C4FA4A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj b/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj index 64b24e3..1ba97a6 100644 --- a/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj +++ b/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj @@ -18,7 +18,7 @@ $(NoWarn);NU5128 true enable - true + true diff --git a/Aigamo.ResXGenerator/Analyser.cs b/Aigamo.ResXGenerator/Analyser.cs new file mode 100644 index 0000000..737a4bf --- /dev/null +++ b/Aigamo.ResXGenerator/Analyser.cs @@ -0,0 +1,90 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Aigamo.ResXGenerator; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class Analyser : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + DuplicateWarning, + MemberSameAsClassWarning, + MemberWithStaticError, + LocalizerStaticError, + LocalizerPartialError, + LocalizationIncoherentNamespace, + FatalError + ); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + // We only use the DiagnosticDescriptor to report from the Source Generator + } + + public static readonly DiagnosticDescriptor DuplicateWarning = new( + id: "AigamoResXGenerator001", + title: "Duplicate member", + messageFormat: "Ignored added member '{0}'", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor MemberSameAsClassWarning = new( + id: "AigamoResXGenerator002", + title: "Member same name as class", + messageFormat: "Ignored member '{0}' has same name as class", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor MemberWithStaticError = new( + id: "AigamoResXGenerator003", + title: "Incompatible settings", + messageFormat: "Cannot have static members/class with an class instance", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor LocalizerStaticError = new( + id: "AigamoResXGenerator004", + title: "Incompatible settings", + messageFormat: "When using StringLocalizer , the static option not available. Parameter ignored.", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor LocalizerPartialError = new( + id: "AigamoResXGenerator005", + title: "Incompatible settings", + messageFormat: "When using StringLocalizer , the partial option not available. Parameter ignored.", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor LocalizationIncoherentNamespace = new( + id: "AigamoResXGenerator006", + title: "Incoherent namespace", + messageFormat: + "When using StringLocalizer, the namespace must be the same as the Resx file. Either remove the CustomToolNamespace or change the GenerationType to ResourceManager. Parameter ignored.", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static DiagnosticDescriptor FatalError => new( + id: "AigamoResXGenerator999", + title: "Fatal Error generated", + messageFormat: "An error occured on generation file {0} error {1}", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); +} diff --git a/Aigamo.ResXGenerator/AnalyzerReleases.Shipped.md b/Aigamo.ResXGenerator/AnalyzerReleases.Shipped.md index d567f14..a67d0ce 100644 --- a/Aigamo.ResXGenerator/AnalyzerReleases.Shipped.md +++ b/Aigamo.ResXGenerator/AnalyzerReleases.Shipped.md @@ -1,3 +1,9 @@ -; Shipped analyzer releases -; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md +## Release 3.1 +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +AigamoResXGenerator001 | ResXGenerator | Warning | StringBuilderGenerator +AigamoResXGenerator002 | ResXGenerator | Warning | StringBuilderGenerator +AigamoResXGenerator003 | ResXGenerator | Error | StringBuilderGenerator diff --git a/Aigamo.ResXGenerator/AnalyzerReleases.Unshipped.md b/Aigamo.ResXGenerator/AnalyzerReleases.Unshipped.md index fc65dc5..31179f0 100644 --- a/Aigamo.ResXGenerator/AnalyzerReleases.Unshipped.md +++ b/Aigamo.ResXGenerator/AnalyzerReleases.Unshipped.md @@ -1,9 +1,6 @@ -; Unshipped analyzer release -; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md - ### New Rules + Rule ID | Category | Severity | Notes ---------|----------|----------|------- -AigamoResXGenerator001 | ResXGenerator | Warning | StringBuilderGenerator -AigamoResXGenerator002 | ResXGenerator | Warning | StringBuilderGenerator -AigamoResXGenerator003 | ResXGenerator | Error | StringBuilderGenerator +--------|----------|----------|-------------------- +AigamoResXGenerator005|ResXGenerator|Warning|LocalizerGenerator +AigamoResXGenerator999|ResXGenerator|Error|SourceGenerator diff --git a/Aigamo.ResXGenerator/Extensions/BoolExtensions.cs b/Aigamo.ResXGenerator/Extensions/BoolExtensions.cs new file mode 100644 index 0000000..babcd5a --- /dev/null +++ b/Aigamo.ResXGenerator/Extensions/BoolExtensions.cs @@ -0,0 +1,5 @@ +namespace Aigamo.ResXGenerator.Extensions; + public static class BoolExtensions + { + public static T InterpolateCondition(this bool condition, T valueIfTrue, T valueIfFalse) => condition ? valueIfTrue : valueIfFalse; + } diff --git a/Aigamo.ResXGenerator/Extensions/EnumerableExtensions.cs b/Aigamo.ResXGenerator/Extensions/EnumerableExtensions.cs index c31644e..1058f68 100644 --- a/Aigamo.ResXGenerator/Extensions/EnumerableExtensions.cs +++ b/Aigamo.ResXGenerator/Extensions/EnumerableExtensions.cs @@ -1,13 +1,8 @@ namespace Aigamo.ResXGenerator.Extensions; public static class EnumerableExtensions { - public static void ForEach(this IEnumerable col, Action action) - { - foreach (var i in col) action(i); - } - - public static void ForEach(this IEnumerable> col, Action action) - { - foreach (var i in col) action(i.Item1, i.Item2, i.Item3); - } + public static void ForEach(this IEnumerable col, Action action) + { + foreach (var i in col) action(i); + } } diff --git a/Aigamo.ResXGenerator/Extensions/StringExtensions.cs b/Aigamo.ResXGenerator/Extensions/StringExtensions.cs index ef1fab5..e09f555 100644 --- a/Aigamo.ResXGenerator/Extensions/StringExtensions.cs +++ b/Aigamo.ResXGenerator/Extensions/StringExtensions.cs @@ -5,11 +5,18 @@ namespace Aigamo.ResXGenerator.Extensions; internal static class StringExtensions { - public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrEmpty(value); + public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrEmpty(value); - public static string ToXmlCommentSafe(this string input, string indent) - { - var lines = HttpUtility.HtmlEncode(input.Trim())?.Split(["\n\r", "\r", "\n"], StringSplitOptions.None) ?? []; - return string.Join($"{Environment.NewLine}{indent}/// ", lines); - } + public static string ToXmlCommentSafe(this string input, string indent) + { + var lines = HttpUtility.HtmlEncode(input.Trim()).GetCodeLines(); + return string.Join($"{Environment.NewLine}{indent}/// ", lines); + } + public static string Indent(this string input, int level = 1) + { + var indent = new string(' ', level * Constants.IndentCount); + return string.Join(Environment.NewLine, input.GetCodeLines().Select(l => indent + l)); + } + + public static IEnumerable GetCodeLines(this string input) => RegexDefinitions.NewLine.Split(input); } diff --git a/Aigamo.ResXGenerator/Generators/CodeGenerator.cs b/Aigamo.ResXGenerator/Generators/CodeGenerator.cs new file mode 100644 index 0000000..6b12759 --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/CodeGenerator.cs @@ -0,0 +1,83 @@ +using System.Collections.Immutable; +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; +using Microsoft.CodeAnalysis.CSharp; +#nullable disable + +namespace Aigamo.ResXGenerator.Generators; + +public sealed class CodeGenerator : GeneratorBase, IResXGenerator +{ + private StringBuilderGeneratorHelper Helper { get; set; } + + public override GeneratedOutput Generate(GenFileOptions options, CancellationToken cancellationToken = default) + { + Init(options); + + Helper = new StringBuilderGeneratorHelper(Options); + + Content = Options.GroupedFile.MainFile.File.GetText(cancellationToken); + if (Content is null) + { + GeneratedFileName = Options.GroupedFile.MainFile.File.Path; + Helper.Append("//ERROR reading file:"); + return Helper.GetOutput(GeneratedFileName, Validator); + } + + GeneratedFileName = $"{Options.LocalNamespace}.{Options.ClassName}.g.cs"; + + Helper.AppendHeader(options.CustomToolNamespace ?? options.LocalNamespace); + Helper.AppendCodeUsings(); + Helper.AppendClassHeader(Options); + Helper.AppendInnerClass(Options, Validator); + GenerateCode(cancellationToken); + Helper.AppendClassFooter(Options); + + return Helper.GetOutput(GeneratedFileName, Validator); + } + + private void GenerateCode(CancellationToken cancellationToken) + { + var combo = new CultureInfoCombo(Options.GroupedFile.SubFiles); + var definedLanguages = combo.GetDefinedLanguages(); + + var fallback = ReadResxFile(Content!); + var subfiles = definedLanguages.Select(lang => + { + var subcontent = lang.FileWithHash.File.GetText(cancellationToken); + return subcontent is null + ? null + : ReadResxFile(subcontent)? + .GroupBy(x => x.Key) + .ToImmutableDictionary(x => x.Key, x => x.First().Value); + }).ToList(); + + if (fallback is null || subfiles.Any(x => x is null)) + { + Helper.AppendFormat("//could not read {0} or one of its children", Options.GroupedFile.MainFile.File.Path); + return; + } + + fallback.ForEach(fbi => + { + cancellationToken.ThrowIfCancellationRequested(); + if (Helper.GenerateMember(fbi, Options, Validator) is not { valid: true }) return; + + Helper.Append(" => GetString_"); + Helper.AppendLanguages(definedLanguages); + Helper.Append("("); + Helper.Append(SymbolDisplay.FormatLiteral(fbi.Value, true)); + + subfiles.ForEach(xml => + { + Helper.Append(", "); + if (!xml!.TryGetValue(fbi.Key, out var langValue)) + langValue = fbi.Value; + Helper.Append(SymbolDisplay.FormatLiteral(langValue, true)); + }); + + Helper.AppendLine(");"); + }); + } +} diff --git a/Aigamo.ResXGenerator/Generators/ComboGenerator.cs b/Aigamo.ResXGenerator/Generators/ComboGenerator.cs index 978fc76..f8a1ff6 100644 --- a/Aigamo.ResXGenerator/Generators/ComboGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/ComboGenerator.cs @@ -1,82 +1,83 @@ using System.Globalization; using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Models; using Aigamo.ResXGenerator.Tools; +#nullable disable namespace Aigamo.ResXGenerator.Generators; -public sealed class ComboGenerator : StringBuilderGenerator, IComboGenerator +public sealed class ComboGenerator : GeneratorBase, IComboGenerator { - private const string OutputStringFilenameFormat = "Aigamo.ResXGenerator.{0}.g.cs"; - private static readonly Dictionary> s_allChildren = new(); + private const string OutputStringFilenameFormat = "Aigamo.ResXGenerator.{0}.g.cs"; + private static readonly Dictionary> s_allChildren = new(); + private StringBuilderGeneratorHelper Helper { get; set; } - /// - /// Build all CultureInfo children - /// - static ComboGenerator() - { - var all = CultureInfo.GetCultures(CultureTypes.AllCultures); + /// + /// Build all CultureInfo children + /// + static ComboGenerator() + { + var all = CultureInfo.GetCultures(CultureTypes.AllCultures); - all.ForEach(cultureInfo => - { - if (cultureInfo.LCID == 4096 || cultureInfo.IsNeutralCulture || cultureInfo.Name.IsNullOrEmpty()) - return; + all.ForEach(cultureInfo => + { + if (cultureInfo.LCID == 4096 || cultureInfo.IsNeutralCulture || cultureInfo.Name.IsNullOrEmpty()) + return; - var parent = cultureInfo.Parent; - if (!s_allChildren.TryGetValue(parent.LCID, out var v)) - s_allChildren[parent.LCID] = v = []; - v.Add(cultureInfo.LCID); - }); - } + var parent = cultureInfo.Parent; + if (!s_allChildren.TryGetValue(parent.LCID, out var v)) + s_allChildren[parent.LCID] = v = []; + v.Add(cultureInfo.LCID); + }); + } - public override GeneratedOutput Generate(CultureInfoCombo options, CancellationToken cancellationToken = default) - { - var definedLanguages = options.GetDefinedLanguages(); - var builder = GetBuilder("Aigamo.ResXGenerator"); + public override GeneratedOutput Generate(CultureInfoCombo options, CancellationToken cancellationToken = default) + { + Init(options); + Helper = new StringBuilderGeneratorHelper(); - builder.AppendLine("internal static partial class Helpers"); - builder.AppendLine("{"); + var definedLanguages = Options.GetDefinedLanguages(); - builder.Append(" public static string GetString_"); - var functionNamePostFix = FunctionNamePostFix(definedLanguages); - builder.Append(functionNamePostFix); - builder.Append("(string fallback"); - definedLanguages.ForEach((name, _, _) => - { - builder.Append(", "); - builder.Append("string "); - builder.Append(name); - }); + Helper.AppendHeader("Aigamo.ResXGenerator"); - builder.Append(") => "); - builder.Append(Constants.SystemGlobalization); - builder.AppendLine(".CultureInfo.CurrentUICulture.LCID switch"); - builder.AppendLine(" {"); - var already = new HashSet(); - definedLanguages.ForEach((name, lcid, _) => - { - var findParents = FindParents(lcid).Except(already).ToList(); - findParents - .Select(parent => - { - already.Add(parent); - return $" {parent} => {name.Replace('-', '_')},"; - }) - .ForEach(l => builder.AppendLine(l)); - }); + Helper.AppendLine("internal static partial class Helpers"); + Helper.AppendLine("{"); - builder.AppendLine(" _ => fallback"); - builder.AppendLine(" };"); - builder.AppendLine("}"); + Helper.Append(" public static string GetString_"); + var functionNamePostFix = Helper.AppendLanguages(definedLanguages); + Helper.Append("(string fallback"); + definedLanguages.ForEach(ci => + { + Helper.Append(", "); + Helper.Append("string "); + Helper.Append(ci.Name); + }); - return new GeneratedOutput(string.Format(OutputStringFilenameFormat, functionNamePostFix), builder.ToString()); - } + GeneratedFileName = string.Format(OutputStringFilenameFormat, functionNamePostFix); - public string GeneratedFileName(CultureInfoCombo combo) - { - var definedLanguages = combo.GetDefinedLanguages(); - var functionNamePostFix = FunctionNamePostFix(definedLanguages); - return string.Format(OutputStringFilenameFormat, functionNamePostFix) ; - } + Helper.Append(") => "); + Helper.Append(Constants.SystemGlobalization); + Helper.AppendLine(".CultureInfo.CurrentUICulture.LCID switch"); + Helper.AppendLine(" {"); + var already = new HashSet(); + definedLanguages.ForEach(ci => + { + var findParents = FindParents(ci.LCID).Except(already).ToList(); + findParents + .Select(parent => + { + already.Add(parent); + return $" {parent} => {ci.Name.Replace('-', '_')},"; + }) + .ForEach(l => Helper.AppendLine(l)); + }); - private static IEnumerable FindParents(int toFind) => s_allChildren.TryGetValue(toFind, out var v) ? v.Prepend(toFind) : [toFind]; + Helper.AppendLine(" _ => fallback"); + Helper.AppendLine(" };"); + Helper.AppendLine("}"); + + return Helper.GetOutput(GeneratedFileName, Validator); + } + + private static IEnumerable FindParents(int toFind) => s_allChildren.TryGetValue(toFind, out var v) ? v.Prepend(toFind) : [toFind]; } diff --git a/Aigamo.ResXGenerator/Generators/GeneratedOutput.cs b/Aigamo.ResXGenerator/Generators/GeneratedOutput.cs deleted file mode 100644 index 971e8ac..0000000 --- a/Aigamo.ResXGenerator/Generators/GeneratedOutput.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace Aigamo.ResXGenerator.Generators; -public class GeneratedOutput(string fileName, string sourceCode, IEnumerable errorsAndWarnings) -{ - public GeneratedOutput(string fileName, string sourceCode) : this(fileName, sourceCode, []) - { - - } - - public string FileName { get; internal set; } = fileName; - public string SourceCode { get; internal set; } = sourceCode; - public IEnumerable ErrorsAndWarnings { get; internal set; } = errorsAndWarnings; -} diff --git a/Aigamo.ResXGenerator/Generators/GeneratorBase.cs b/Aigamo.ResXGenerator/Generators/GeneratorBase.cs new file mode 100644 index 0000000..905ffb5 --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/GeneratorBase.cs @@ -0,0 +1,40 @@ +using System.Xml.Linq; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; +using Microsoft.CodeAnalysis.Text; +#nullable disable + +namespace Aigamo.ResXGenerator.Generators; + +public abstract class GeneratorBase : IGenerator +{ + protected SourceText Content { get; set; } + public string GeneratedFileName { get; protected set; } + protected T Options { get; private set; } + protected IntegrityValidator Validator { get; private set; } + + protected void Init(T options) + { + Options = options; + Validator = new IntegrityValidator(); + Content = SourceText.From(string.Empty); + GeneratedFileName = string.Empty; + } + + public abstract GeneratedOutput Generate(T options, CancellationToken cancellationToken = default); + + protected static IEnumerable ReadResxFile(SourceText content) + { + using var reader = new StringReader(content.ToString()); + + if (XDocument.Load(reader, LoadOptions.SetLineInfo).Root is { } element) + return element + .Descendants() + .Where(static data => data.Name == "data") + .Select(static data => new FallBackItem(data.Attribute("name")!.Value, data.Descendants("value").First().Value, data.Attribute("name")!)); + + return []; + } + + protected GeneratedOutput GetOutput(string sourceCode) => new(GeneratedFileName, sourceCode, Validator.ErrorsAndWarnings); +} diff --git a/Aigamo.ResXGenerator/Generators/LocalizerGenerator.cs b/Aigamo.ResXGenerator/Generators/LocalizerGenerator.cs new file mode 100644 index 0000000..068b190 --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/LocalizerGenerator.cs @@ -0,0 +1,63 @@ +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; + +namespace Aigamo.ResXGenerator.Generators; + +public sealed class LocalizerGenerator : GeneratorBase, IResXGenerator +{ + public override GeneratedOutput Generate(GenFileOptions options, CancellationToken cancellationToken = default) + { + Init(options); + var generatedFileName = $"{options.LocalNamespace}.{options.ClassName}.g.cs"; + + Content = options.GroupedFile.MainFile.File.GetText(cancellationToken); + if (Content is null) return new GeneratedOutput(options.GroupedFile.MainFile.File.Path, "//ERROR reading file:", []); + + Validator.ValidateInconsistentNameSpace(Options); + Validator.ValidateLocalizationModifiers(Options); + + var sourceCode = GenerateResourceManager(); + + return new GeneratedOutput(generatedFileName, sourceCode, Validator.ErrorsAndWarnings); + } + + //Note with StringLocalization injection work with singleton or transient + //No static class where allowed + private string GenerateResourceManager() + { + var fallback = ReadResxFile(Content)?.ToList(); + + return fallback is null ? "//could not read {0} or one of its children" : + $$""" + {{Constants.AutoGeneratedHeader}} + {{Options.NullForgivingOperators.InterpolateCondition("#nullable disable", "#nullable enable")}} + using Microsoft.Extensions.Localization; + using System.Text; + + namespace {{Options.LocalNamespace}}; + + public interface I{{Options.ClassName}} + { + {{string.Join(Environment.NewLine, fallback.Select(GenerateInterfaceMembers)).Indent()}} + } + + {{Options.PublicClass.InterpolateCondition("public", "internal")}} class {{Options.ClassName}}(IStringLocalizer<{{Options.ClassName}}> stringLocalizer) : I{{Options.ClassName}} + { + {{string.Join(Environment.NewLine, fallback.Select(GenerateMembers)).Indent()}} + } + """; + } + + private string GenerateMembers(FallBackItem fallbackItem) => !Validator.ValidateMember(fallbackItem, Options) ? + $"// Skipped invalid member name: {fallbackItem.Key}" : + $"public string {fallbackItem.Key} => stringLocalizer[\"{fallbackItem.Key}\"];"; + + private static string GenerateInterfaceMembers(FallBackItem fallbackItem) => + $$""" + /// + /// Looks up a localized string similar to {{fallbackItem.Value.ToXmlCommentSafe(string.Empty)}}. + /// + string {{fallbackItem.Key}} {get;} + """; +} diff --git a/Aigamo.ResXGenerator/Generators/LocalizerGlobalRegisterGenerator.cs b/Aigamo.ResXGenerator/Generators/LocalizerGlobalRegisterGenerator.cs new file mode 100644 index 0000000..c47d85b --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/LocalizerGlobalRegisterGenerator.cs @@ -0,0 +1,41 @@ +using System.Collections.Immutable; +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; + +namespace Aigamo.ResXGenerator.Generators; + +public sealed class LocalizerGlobalRegisterGenerator : GeneratorBase>, IGlobalRegisterGenerator +{ + public override GeneratedOutput Generate(ImmutableArray options, CancellationToken cancellationToken = default) + { + Init(options); + + GeneratedFileName = "Aigamo.ResXGenerator.Registers.g.cs"; + + var sourceCode = $$""" + {{Constants.AutoGeneratedHeader}} + {{Options.All(f => f.NullForgivingOperator).InterpolateCondition("#nullable disable", "#nullable enable")}} + using Microsoft.Extensions.DependencyInjection; + {{string.Join(Environment.NewLine, Options.Select(GenerateUsing))}} + + namespace ResXGenerator.Registration; + + public static class ResXGeneratorRegistrationExtension + { + public static IServiceCollection UsingResXGenerator(this IServiceCollection services) + { + {{string.Join(Environment.NewLine, Options.Select(GenerateRegisterCall)).Indent(2)}} + + return services; + } + } + """; + + return GetOutput(sourceCode); + } + + private static string GenerateRegisterCall(GenFilesNamespace nsur) => $"services.{nsur.NameOfUsingMethodRegistration}();"; + + private static string GenerateUsing(GenFilesNamespace nsur) => $"using {nsur.Namespace};"; +} diff --git a/Aigamo.ResXGenerator/Generators/LocalizerRegisterGenerator.cs b/Aigamo.ResXGenerator/Generators/LocalizerRegisterGenerator.cs new file mode 100644 index 0000000..8264bce --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/LocalizerRegisterGenerator.cs @@ -0,0 +1,43 @@ +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; + +namespace Aigamo.ResXGenerator.Generators; + +public sealed class LocalizerRegisterGenerator : GeneratorBase, ILocalRegisterGenerator +{ + public override GeneratedOutput Generate(GenFilesNamespace options, CancellationToken cancellationToken = default) + { + Init(options); + + var generatedFileName = $"{options.Namespace}.Registers.g.cs"; + + var sourceCode = GenerateResourceRegistration(options); + + return new GeneratedOutput(generatedFileName, sourceCode, Validator.ErrorsAndWarnings); + } + + private string GenerateResourceRegistration(GenFilesNamespace options) + { + var items = options.Files.Select(f => f.ClassName).ToList(); + + return $$""" + {{Constants.AutoGeneratedHeader}} + {{Options.NullForgivingOperator.InterpolateCondition("#nullable disable", "#nullable enable")}} + using Microsoft.Extensions.DependencyInjection; + + namespace {{options.Namespace}}; + + public static class {{options.SafeNamespaceName}}RegistrationExtensions + { + public static IServiceCollection {{options.NameOfUsingMethodRegistration}}(this IServiceCollection services) + { + {{string.Join(Environment.NewLine, items.Select(GenerateRegistrationCalls)).Indent(2)}} + return services; + } + } + """; + } + + private static string GenerateRegistrationCalls(string className) => $"services.AddSingleton();"; +} diff --git a/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs b/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs index 466febe..7b51940 100644 --- a/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs @@ -1,234 +1,75 @@ -using System.Collections.Immutable; -using System.Text; -using System.Xml; -using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Models; using Aigamo.ResXGenerator.Tools; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; +#nullable disable namespace Aigamo.ResXGenerator.Generators; -public sealed class ResourceManagerGenerator : StringBuilderGenerator, IResXGenerator +public sealed class ResourceManagerGenerator : GeneratorBase, IResXGenerator { - public override GeneratedOutput Generate(GenFileOptions options, CancellationToken cancellationToken = default) - { - var errorsAndWarnings = new List(); - var generatedFileName = $"{options.LocalNamespace}.{options.ClassName}.g.cs"; - - var content = options.GroupedFile.MainFile.File.GetText(cancellationToken); - if (content is null) return new GeneratedOutput (options.GroupedFile.MainFile.File.Path, "//ERROR reading file:", errorsAndWarnings); - - // HACK: netstandard2.0 doesn't support improved interpolated strings? - var builder = GetBuilder(options.CustomToolNamespace ?? options.LocalNamespace); - - if (options.GenerateCode) - AppendCodeUsings(builder); - else - AppendResourceManagerUsings(builder); - - builder.Append(options.PublicClass ? "public" : "internal"); - builder.Append(options.StaticClass ? " static" : string.Empty); - builder.Append(options.PartialClass ? " partial class " : " class "); - builder.AppendLine(options.ClassName); - builder.AppendLine("{"); - - var indent = " "; - string containerClassName = options.ClassName; - - if (options.InnerClassVisibility != InnerClassVisibility.NotGenerated) - { - containerClassName = string.IsNullOrEmpty(options.InnerClassName) ? "Resources" : options.InnerClassName; - if (!string.IsNullOrEmpty(options.InnerClassInstanceName)) - { - if (options.StaticClass || options.StaticMembers) - { - errorsAndWarnings.Add(Diagnostic.Create( - descriptor: Rules.MemberWithStaticError, - location: Location.Create( - filePath: options.GroupedFile.MainFile.File.Path, - textSpan: new TextSpan(), - lineSpan: new LinePositionSpan() - ) - )); - } - - builder.Append(indent); - builder.Append("public "); - builder.Append(containerClassName); - builder.Append(" "); - builder.Append(options.InnerClassInstanceName); - builder.AppendLine(" { get; } = new();"); - builder.AppendLine(); - } - - builder.Append(indent); - builder.Append(GetInnerClassVisibility(options)); - builder.Append(options.StaticClass ? " static" : string.Empty); - builder.Append(options.PartialClass ? " partial class " : " class "); - - builder.AppendLine(containerClassName); - builder.Append(indent); - builder.AppendLine("{"); - - indent += " "; - } - - if (options.GenerateCode) - GenerateCode(options, content, indent, containerClassName, builder, errorsAndWarnings, cancellationToken); - else - GenerateResourceManager(options, content, indent, containerClassName, builder, errorsAndWarnings, cancellationToken); - - if (options.InnerClassVisibility != InnerClassVisibility.NotGenerated) - { - builder.AppendLine(" }"); - } - - builder.AppendLine("}"); - - return new GeneratedOutput(generatedFileName, builder.ToString(), errorsAndWarnings); - } - - private static void GenerateCode( - GenFileOptions options, - SourceText content, - string indent, - string containerClassName, - StringBuilder builder, - List errorsAndWarnings, - CancellationToken cancellationToken) - { - var combo = new CultureInfoCombo(options.GroupedFile.SubFiles); - var definedLanguages = combo.GetDefinedLanguages(); - - var fallback = ReadResxFile(content); - var subfiles = definedLanguages.Select(lang => - { - var subcontent = lang.FileWithHash.File.GetText(cancellationToken); - return subcontent is null - ? null - : ReadResxFile(subcontent)? - .GroupBy(x => x.key) - .ToImmutableDictionary(x => x.Key, x => x.First().value); - }).ToList(); - if (fallback is null || subfiles.Any(x => x is null)) - { - builder.AppendFormat("//could not read {0} or one of its children", options.GroupedFile.MainFile.File.Path); - return; - } - - var alreadyAddedMembers = new HashSet(); - fallback.ForEach((key, value, line) => - { - cancellationToken.ThrowIfCancellationRequested(); - if ( - !GenerateMember( - indent, - builder, - options, - key, - value, - line, - alreadyAddedMembers, - errorsAndWarnings, - containerClassName, - out _ - ) - ) return; - - builder.Append(" => GetString_"); - builder.Append(FunctionNamePostFix(definedLanguages)); - builder.Append("("); - builder.Append(SymbolDisplay.FormatLiteral(value, true)); - - subfiles.ForEach(xml => - { - builder.Append(", "); - if (!xml!.TryGetValue(key, out var langValue)) - langValue = value; - builder.Append(SymbolDisplay.FormatLiteral(langValue, true)); - }); - - builder.AppendLine(");"); - }); - } - - private static void GenerateResourceManager( - GenFileOptions options, - SourceText content, - string indent, - string containerClassName, - StringBuilder builder, - List errorsAndWarnings, - CancellationToken cancellationToken) - { - GenerateResourceManagerMembers(builder, indent, containerClassName, options); - - var members = ReadResxFile(content); - if (members is null) - { - return; - } - - var alreadyAddedMembers = new HashSet { Constants.CultureInfoVariable }; - members.ForEach((key, value, line) => - { - cancellationToken.ThrowIfCancellationRequested(); - CreateMember( - indent, - builder, - options, - key, - value, - line, - alreadyAddedMembers, - errorsAndWarnings, - containerClassName - ); - }); - } - - private static void CreateMember( - string indent, - StringBuilder builder, - GenFileOptions options, - string name, - string value, - IXmlLineInfo line, - HashSet alreadyAddedMembers, - List errorsAndWarnings, - string containerClassName - ) - { - if (!GenerateMember(indent, builder, options, name, value, line, alreadyAddedMembers, errorsAndWarnings, containerClassName, out var resourceAccessByName)) - { - return; - } - - if (resourceAccessByName) - { - builder.Append(" => ResourceManager.GetString(nameof("); - builder.Append(name); - builder.Append("), "); - } - else - { - builder.Append(@" => ResourceManager.GetString("""); - builder.Append(name.Replace(@"""", @"\""")); - builder.Append(@""", "); - } - - builder.Append(Constants.CultureInfoVariable); - builder.Append(")"); - builder.Append(options.NullForgivingOperators ? "!" : null); - builder.AppendLine(";"); - } - - private static string GetInnerClassVisibility(GenFileOptions options) - { - if (options.InnerClassVisibility == InnerClassVisibility.SameAsOuter) - return options.PublicClass ? "public" : "internal"; - - return options.InnerClassVisibility.ToString().ToLowerInvariant(); - } + private StringBuilderGeneratorHelper Helper { get; set; } + + public override GeneratedOutput Generate(GenFileOptions options, CancellationToken cancellationToken = default) + { + Init(options); + + Helper = new StringBuilderGeneratorHelper(options); + + Content = Options.GroupedFile.MainFile.File.GetText(cancellationToken); + if (Content is null) + { + GeneratedFileName = Options.GroupedFile.MainFile.File.Path; + Helper.Append("//ERROR reading file:"); + return Helper.GetOutput(GeneratedFileName, Validator); + } + + GeneratedFileName = $"{Options.LocalNamespace}.{Options.ClassName}.g.cs"; + + Helper.AppendHeader(Options.CustomToolNamespace ?? Options.LocalNamespace); + Helper.AppendResourceManagerUsings(); + Helper.AppendClassHeader(options); + Helper.AppendInnerClass(options, Validator); + GenerateResourceManager(cancellationToken); + Helper.AppendClassFooter(options); + + return Helper.GetOutput(GeneratedFileName, Validator); + } + + private void GenerateResourceManager(CancellationToken cancellationToken) + { + Helper.GenerateResourceManagerMembers(Options); + + var members = ReadResxFile(Content!); + + members?.ForEach(fbi => + { + cancellationToken.ThrowIfCancellationRequested(); + CreateMember(fbi); + }); + } + + private void CreateMember(FallBackItem fallbackItem) + { + if (Helper.GenerateMember(fallbackItem, Options, Validator) is not { valid: true } output) return; + + var (_, resourceAccessByName) = output; + + if (resourceAccessByName) + { + Helper.Append(" => ResourceManager.GetString(nameof("); + Helper.Append(fallbackItem.Key); + Helper.Append("), "); + } + else + { + Helper.Append(@" => ResourceManager.GetString("""); + Helper.Append(fallbackItem.Key.Replace(@"""", @"\""")); + Helper.Append(@""", "); + } + + Helper.Append(Constants.CultureInfoVariable); + Helper.Append(")"); + Helper.Append(Options.NullForgivingOperators ? "!" : string.Empty); + Helper.AppendLine(";"); + } } diff --git a/Aigamo.ResXGenerator/Generators/StringBuilderGenerator.cs b/Aigamo.ResXGenerator/Generators/StringBuilderGenerator.cs deleted file mode 100644 index 3e8553a..0000000 --- a/Aigamo.ResXGenerator/Generators/StringBuilderGenerator.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System.Globalization; -using System.Resources; -using System.Text; -using System.Text.RegularExpressions; -using System.Xml; -using System.Xml.Linq; -using Aigamo.ResXGenerator.Extensions; -using Aigamo.ResXGenerator.Tools; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; - -namespace Aigamo.ResXGenerator.Generators; - -public abstract class StringBuilderGenerator : IGenerator -{ - private static Location GetMemberLocation(GenFileOptions fileOptions, IXmlLineInfo line, string memberName) => - Location.Create( - filePath: fileOptions.GroupedFile.MainFile.File.Path, - textSpan: new TextSpan(), - lineSpan: new LinePositionSpan( - start: new LinePosition(line.LineNumber - 1, line.LinePosition - 1), - end: new LinePosition(line.LineNumber - 1, line.LinePosition - 1 + memberName.Length) - ) - ); - - public abstract GeneratedOutput Generate(T options, CancellationToken cancellationToken = default); - - protected static StringBuilder GetBuilder(string withNamespace) - { - var builder = new StringBuilder(); - - builder.AppendLine(Constants.AutoGeneratedHeader); - builder.AppendLine("#nullable enable"); - - builder.Append("namespace "); - builder.Append(withNamespace); - builder.AppendLine(";"); - - return builder; - } - - protected static bool GenerateMember( - string indent, - StringBuilder builder, - GenFileOptions options, - string name, - string neutralValue, - IXmlLineInfo line, - HashSet alreadyAddedMembers, - List errorsAndWarnings, - string containerClassName, - out bool resourceAccessByName - ) //Check - { - string memberName; - - if (RegexDefinitions.ValidMemberNamePattern.IsMatch(name)) - { - memberName = name; - resourceAccessByName = true; - } - else - { - memberName = RegexDefinitions.InvalidMemberNameSymbols.Replace(name, "_"); - resourceAccessByName = false; - } - - if (!alreadyAddedMembers.Add(memberName)) - { - errorsAndWarnings.Add(Diagnostic.Create( - descriptor: Rules.DuplicateWarning, - location: GetMemberLocation(options, line, memberName), memberName - )); - return false; - } - - if (memberName == containerClassName) - { - errorsAndWarnings.Add(Diagnostic.Create( - descriptor: Rules.MemberSameAsClassWarning, - location: GetMemberLocation(options, line, memberName), memberName - )); - return false; - } - - builder.AppendLine(); - - builder.Append(indent); - builder.AppendLine("/// "); - - builder.Append(indent); - builder.Append("/// Looks up a localized string similar to "); - builder.Append(neutralValue.ToXmlCommentSafe(indent)); - builder.AppendLine("."); - - builder.Append(indent); - builder.AppendLine("/// "); - - builder.Append(indent); - builder.Append("public "); - builder.Append(options.StaticMembers ? "static " : string.Empty); - builder.Append("string"); - builder.Append(options.NullForgivingOperators ? null : "?"); - builder.Append(" "); - builder.Append(memberName); - return true; - } - - protected static IEnumerable<(string key, string value, IXmlLineInfo line)>? ReadResxFile(SourceText content) - { - using var reader = new StringReader(content.ToString()); - - if (XDocument.Load(reader, LoadOptions.SetLineInfo).Root is { } element) - return element - .Descendants() - .Where(static data => data.Name == "data") - .Select(static data => ( - key: data.Attribute("name")!.Value, - value: data.Descendants("value").First().Value, - line: (IXmlLineInfo)data.Attribute("name")! - )); - - return null; - } - protected static void AppendResourceManagerUsings(StringBuilder builder) - { - builder.Append("using "); - builder.Append(Constants.SystemGlobalization); - builder.AppendLine(";"); - - builder.Append("using "); - builder.Append(Constants.SystemResources); - builder.AppendLine(";"); - - builder.AppendLine(); - } - - protected static void AppendCodeUsings(StringBuilder builder) - { - builder.AppendLine("using static Aigamo.ResXGenerator.Helpers;"); - builder.AppendLine(); - } - - protected static void GenerateResourceManagerMembers( - StringBuilder builder, - string indent, - string containerClassName, - GenFileOptions options - ) - { - builder.Append(indent); - builder.Append("private static "); - builder.Append(nameof(ResourceManager)); - builder.Append("? "); - builder.Append(Constants.SResourceManagerVariable); - builder.AppendLine(";"); - - builder.Append(indent); - builder.Append("public static "); - builder.Append(nameof(ResourceManager)); - builder.Append(" "); - builder.Append(Constants.ResourceManagerVariable); - builder.Append(" => "); - builder.Append(Constants.SResourceManagerVariable); - builder.Append(" ??= new "); - builder.Append(nameof(ResourceManager)); - builder.Append("(\""); - builder.Append(options.EmbeddedFilename); - builder.Append("\", typeof("); - builder.Append(containerClassName); - builder.AppendLine(").Assembly);"); - - builder.Append(indent); - builder.Append("public "); - builder.Append(options.StaticMembers ? "static " : string.Empty); - builder.Append(nameof(CultureInfo)); - builder.Append("? "); - builder.Append(Constants.CultureInfoVariable); - builder.AppendLine(" { get; set; }"); - } - - protected static string FunctionNamePostFix(IReadOnlyList<(string Name, int LCID, AdditionalTextWithHash FileWithHash)>? definedLanguages) => string.Join("_", definedLanguages?.Select(x => x.LCID) ?? []); -} diff --git a/Aigamo.ResXGenerator/Models/ComboItem.cs b/Aigamo.ResXGenerator/Models/ComboItem.cs new file mode 100644 index 0000000..35772d8 --- /dev/null +++ b/Aigamo.ResXGenerator/Models/ComboItem.cs @@ -0,0 +1,5 @@ +using Aigamo.ResXGenerator.Tools; + +namespace Aigamo.ResXGenerator.Models; + +public record ComboItem(string Name, int LCID, AdditionalTextWithHash FileWithHash); diff --git a/Aigamo.ResXGenerator/Models/FallBackItem.cs b/Aigamo.ResXGenerator/Models/FallBackItem.cs new file mode 100644 index 0000000..69dd62b --- /dev/null +++ b/Aigamo.ResXGenerator/Models/FallBackItem.cs @@ -0,0 +1,5 @@ +using System.Xml; + +namespace Aigamo.ResXGenerator.Models; + +public record FallBackItem(string Key, string Value, IXmlLineInfo Line); diff --git a/Aigamo.ResXGenerator/Models/GenFilesNamespace.cs b/Aigamo.ResXGenerator/Models/GenFilesNamespace.cs new file mode 100644 index 0000000..87aef2e --- /dev/null +++ b/Aigamo.ResXGenerator/Models/GenFilesNamespace.cs @@ -0,0 +1,13 @@ +using System.Collections.Immutable; +using Aigamo.ResXGenerator.Tools; + +namespace Aigamo.ResXGenerator.Models; + +public record GenFilesNamespace(string Namespace, ImmutableArray Files) +{ + public string SafeNamespaceName { get; } = Namespace.NamespaceNameCompliant(); + + public bool NullForgivingOperator => Files.All(f => f.NullForgivingOperators); + + public string NameOfUsingMethodRegistration => $"Using{SafeNamespaceName}ResX"; +} diff --git a/Aigamo.ResXGenerator/Models/GeneratedOutput.cs b/Aigamo.ResXGenerator/Models/GeneratedOutput.cs new file mode 100644 index 0000000..e7cc7d4 --- /dev/null +++ b/Aigamo.ResXGenerator/Models/GeneratedOutput.cs @@ -0,0 +1,5 @@ +using Microsoft.CodeAnalysis; + +namespace Aigamo.ResXGenerator.Models; + +public record GeneratedOutput(string FileName, string SourceCode, IEnumerable ErrorsAndWarnings); diff --git a/Aigamo.ResXGenerator/Properties/launchSettings.json b/Aigamo.ResXGenerator/Properties/launchSettings.json index aacbe6d..de696d6 100644 --- a/Aigamo.ResXGenerator/Properties/launchSettings.json +++ b/Aigamo.ResXGenerator/Properties/launchSettings.json @@ -1,8 +1,11 @@ { - "profiles": { - "Debug": { - "commandName": "DebugRoslynComponent", - "targetProject": "..\\Aigamo.ResXGenerator.Tests\\Aigamo.ResXGenerator.Tests.csproj" - } - } -} + "profiles": { + "Debug": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\Aigamo.ResXGenerator.Tests\\Aigamo.ResXGenerator.Tests.csproj" + }, + "Profil 1": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/Aigamo.ResXGenerator/Rules.cs b/Aigamo.ResXGenerator/Rules.cs deleted file mode 100644 index 8c5c941..0000000 --- a/Aigamo.ResXGenerator/Rules.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace Aigamo.ResXGenerator; - -internal static class Rules -{ - public static readonly DiagnosticDescriptor DuplicateWarning = new( - id: "AigamoResXGenerator001", - title: "Duplicate member", - messageFormat: "Ignored added member '{0}'", - category: "ResXGenerator", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); - - public static readonly DiagnosticDescriptor MemberSameAsClassWarning = new( - id: "AigamoResXGenerator002", - title: "Member same name as class", - messageFormat: "Ignored member '{0}' has same name as class", - category: "ResXGenerator", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); - - public static readonly DiagnosticDescriptor MemberWithStaticError = new( - id: "AigamoResXGenerator003", - title: "Incompatible settings", - messageFormat: "Cannot have static members/class with an class instance", - category: "ResXGenerator", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true - ); - - public static DiagnosticDescriptor FatalError(string resourceDetail, Exception exception) => new( - id: "AigamoResXGenerator999", - title: "Fatal Error generated", - messageFormat: $"An error occured on generation file {resourceDetail} error {exception.Message}", - category: "ResXGenerator", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true - ); -} diff --git a/Aigamo.ResXGenerator/SourceGenerator.cs b/Aigamo.ResXGenerator/SourceGenerator.cs index 2765d77..4ba55a1 100644 --- a/Aigamo.ResXGenerator/SourceGenerator.cs +++ b/Aigamo.ResXGenerator/SourceGenerator.cs @@ -1,5 +1,7 @@ -using Aigamo.ResXGenerator.Extensions; +using System.Collections.Immutable; +using Aigamo.ResXGenerator.Extensions; using Aigamo.ResXGenerator.Generators; +using Aigamo.ResXGenerator.Models; using Aigamo.ResXGenerator.Tools; using Microsoft.CodeAnalysis; @@ -8,66 +10,173 @@ namespace Aigamo.ResXGenerator; [Generator] public class SourceGenerator : IIncrementalGenerator { - private IComboGenerator ComboGenerator { get; } = new ComboGenerator(); - private IResXGenerator ResXGenerator { get; } = new ResourceManagerGenerator(); - - public void Initialize(IncrementalGeneratorInitializationContext context) - { - var globalOptions = context.AnalyzerConfigOptionsProvider.Select(GlobalOptions.Select); - - // Note: Each Resx file will get a hash (random guid) so we can easily differentiate in the pipeline when the file changed or just some options - var allResxFiles = context.AdditionalTextsProvider.Where(static af => af.Path.EndsWith(".resx")) - .Select(static (f, _) => new AdditionalTextWithHash(f, Guid.NewGuid())); - - var monitor = allResxFiles.Collect().SelectMany(static (x, _) => GroupResxFiles.Group(x)); - - var inputs = monitor - .Combine(globalOptions) - .Combine(context.AnalyzerConfigOptionsProvider) - .Select(static (x, _) => GenFileOptions.Select( - file: x.Left.Left, - options: x.Right, - globalOptions: x.Left.Right - )) - .Where(static x => x is { IsValid: true, SkipFile: false }); - - GenerateResXFiles(context, inputs); - GenerateResxCombos(context, monitor); - } - - private void GenerateResxCombos(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider monitor) - { - var detectAllCombosOfResx = monitor.Collect().SelectMany((x, _) => GroupResxFiles.DetectChildCombos(x)); - context.RegisterSourceOutput(detectAllCombosOfResx, (ctx, combo) => - { - try - { - var output = ComboGenerator.Generate(combo, ctx.CancellationToken); - output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); - ctx.AddSource(output.FileName, output.SourceCode); - } - catch (Exception e) - { - ctx.ReportDiagnostic(Diagnostic.Create(Rules.FatalError(ComboGenerator.GeneratedFileName(combo), e), Location.None)); - } - }); - } - - private void GenerateResXFiles(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) - { - context.RegisterSourceOutput(inputs, (ctx, file) => - { - try - { - var output = ResXGenerator.Generate(file, ctx.CancellationToken); - output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); - ctx.AddSource(output.FileName, output.SourceCode); - } - catch (Exception e) - { - ctx.ReportDiagnostic(Diagnostic.Create(Rules.FatalError(file.ClassName, e), Location.None)); - } - - }); - } + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var globalOptions = context.AnalyzerConfigOptionsProvider.Select(GlobalOptions.Select); + + // Note: Each Resx file will get a hash (random guid) so we can easily differentiate in the pipeline when the file changed or just some options + var allResxFiles = context.AdditionalTextsProvider.Where(static af => af.Path.EndsWith(".resx")) + .Select(static (f, _) => new AdditionalTextWithHash(f, Guid.NewGuid())); + + var monitor = allResxFiles + .Collect() + .SelectMany(static (x, _) => GroupResxFiles.Group(x)) + .Combine(globalOptions) + .Combine(context.AnalyzerConfigOptionsProvider) + .Select(static (x, _) => GenFileOptions.Select(x.Left.Left, x.Right, x.Left.Right)) + .Where(static x => x is { IsValid: true, SkipFile: false }); + + //-------------------------------- + // Classic ResxManager + //-------------------------------- + var inputsResXManager = monitor + .Where(static x => x is { GenerationType: GenerationType.ResourceManager, GenerateCode: false }); + GenerateResXFiles(context, inputsResXManager); + + //-------------------------- + //Code generation for strongly typed access to resources + //-------------------------- + var inputsResXCodeGen = monitor + .Where(static x => x is { GenerationType: GenerationType.CodeGeneration } or { GenerationType: GenerationType.ResourceManager, GenerateCode: true }); + GenerateCodeFiles(context, inputsResXCodeGen); + + //-------------------------- + //IStringLocalizer service injection + //Need nugets + // Microsoft.Extensions.DependencyInjection + // Microsoft.Extensions.Localization + // Microsoft.Extensions.Logging + //-------------------------- + var inputResXLocalizer = monitor + .Where(static x => x is { GenerationType: GenerationType.StringLocalizer }) + .Collect() + .SelectMany(static (x, _) => x.GroupBy(f => f.LocalNamespace)) + .Select(static (x, _) => new GenFilesNamespace(x.Key, x.ToImmutableArray())); + GenerateLocalizerRegister(context, inputResXLocalizer); // Generate one localizer per namespace+class combo + } + + private static void GenerateResXFiles(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) + { + var generator = new ResourceManagerGenerator(); + context.RegisterSourceOutput(inputs, (ctx, file) => + { + try + { + var output = generator.Generate(file, ctx.CancellationToken); + output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); + ctx.AddSource(output.FileName, output.SourceCode); + } + catch (Exception e) + { + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"ResXManager({file.ClassName})", e)); + } + }); + } + + private static void GenerateCodeFiles(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) + { + var generator = new CodeGenerator(); + context.RegisterSourceOutput(inputs, (ctx, file) => + { + try + { + var output = generator.Generate(file, ctx.CancellationToken); + output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); + ctx.AddSource(output.FileName, output.SourceCode); + } + catch (Exception e) + { + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"ResXManager({file.ClassName})", e)); + } + }); + + var inputsCombos = inputs.Select(static (x, _) => x.GroupedFile); + GenerateResXCombos(context, inputsCombos); + } + + private static void GenerateResXCombos(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider monitor) + { + var generator = new ComboGenerator(); + var detectAllCombosOfResx = monitor + .Collect() + .SelectMany((x, _) => GroupResxFiles.DetectChildCombos(x)); + context.RegisterSourceOutput(detectAllCombosOfResx, (ctx, combo) => + { + try + { + var output = generator.Generate(combo, ctx.CancellationToken); + output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); + ctx.AddSource(output.FileName, output.SourceCode); + } + catch (Exception e) + { + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, generator.GeneratedFileName, e)); + } + }); + } + + private static void GenerateLocalizerRegister(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) + { + var generator = new LocalizerRegisterGenerator(); + + context.RegisterSourceOutput(inputs, (ctx, ns) => + { + try + { + var output = generator.Generate(ns, ctx.CancellationToken); + output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); + ctx.AddSource(output.FileName, output.SourceCode); + } + catch (Exception e) + { + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, "Registration of localizers", e)); + } + }); + + GenerateLocalizerResXClasses(context, inputs); + GenerateLocalizerGlobalRegister(context, inputs); + } + + private static void GenerateLocalizerResXClasses(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) + { + var generator = new LocalizerGenerator(); + //Debugger.Launch(); + + var resxFiles = inputs + .SelectMany(static (x, _) => x.Files.GroupBy(f => f.ClassName)) + .Select(static (x, _) => x.First()); + + context.RegisterSourceOutput(resxFiles, (ctx, file) => + { + try + { + var output = generator.Generate(file, ctx.CancellationToken); + output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); + ctx.AddSource(output.FileName, output.SourceCode); + } + catch (Exception e) + { + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"Error while generating class for {file.ClassName}", e)); + } + }); + } + + private static void GenerateLocalizerGlobalRegister(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) + { + var globalGenerator = new LocalizerGlobalRegisterGenerator(); + var global = inputs.Collect(); + + context.RegisterSourceOutput(global, (ctx, gns) => + { + try + { + var output = globalGenerator.Generate(gns, ctx.CancellationToken); + ctx.AddSource(output.FileName, output.SourceCode); + } + catch (Exception e) + { + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, " Global localizer registration", e)); + } + }); + } } diff --git a/Aigamo.ResXGenerator/Tools/AdditionalTextWithHash.cs b/Aigamo.ResXGenerator/Tools/AdditionalTextWithHash.cs index a7e77b9..5372587 100644 --- a/Aigamo.ResXGenerator/Tools/AdditionalTextWithHash.cs +++ b/Aigamo.ResXGenerator/Tools/AdditionalTextWithHash.cs @@ -4,21 +4,15 @@ namespace Aigamo.ResXGenerator.Tools; public readonly record struct AdditionalTextWithHash(AdditionalText File, Guid Hash) { - public bool Equals(AdditionalTextWithHash other) - { - return File.Path.Equals(other.File.Path) && Hash.Equals(other.Hash); - } + public bool Equals(AdditionalTextWithHash other) => File.Path.Equals(other.File.Path) && Hash.Equals(other.Hash); - public override int GetHashCode() - { - unchecked - { - return File.GetHashCode() * 397 ^ Hash.GetHashCode(); - } - } + public override int GetHashCode() + { + unchecked + { + return File.GetHashCode() * 397 ^ Hash.GetHashCode(); + } + } - public override string ToString() - { - return $"{nameof(File)}: {File?.Path}, {nameof(Hash)}: {Hash}"; - } + public override string ToString() => $"{nameof(File)}: {File?.Path}, {nameof(Hash)}: {Hash}"; } diff --git a/Aigamo.ResXGenerator/Tools/Constants.cs b/Aigamo.ResXGenerator/Tools/Constants.cs index a3dc016..e278e45 100644 --- a/Aigamo.ResXGenerator/Tools/Constants.cs +++ b/Aigamo.ResXGenerator/Tools/Constants.cs @@ -19,4 +19,5 @@ internal static class Constants public const string SResourceManagerVariable = "s_resourceManager"; public const string ResourceManagerVariable = "ResourceManager"; public const string CultureInfoVariable = "CultureInfo"; + public const int IndentCount = 4; } diff --git a/Aigamo.ResXGenerator/Tools/CultureInfoCombo.cs b/Aigamo.ResXGenerator/Tools/CultureInfoCombo.cs index 0a2a148..b20c054 100644 --- a/Aigamo.ResXGenerator/Tools/CultureInfoCombo.cs +++ b/Aigamo.ResXGenerator/Tools/CultureInfoCombo.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Aigamo.ResXGenerator.Models; using Aigamo.ResXGenerator.Tools; namespace Aigamo.ResXGenerator; @@ -20,9 +21,9 @@ public CultureInfoCombo(IReadOnlyList? files) public IReadOnlyList<(string Iso, AdditionalTextWithHash File)> CultureInfos { get;} - public IReadOnlyList<(string Name, int LCID, AdditionalTextWithHash FileWithHash)> GetDefinedLanguages() => CultureInfos? + public IReadOnlyList GetDefinedLanguages() => CultureInfos? .Select(x => (x.File, new CultureInfo(x.Iso))) - .Select(x => (Name: x.Item2.Name.Replace('-', '_'), x.Item2.LCID, x.File)) + .Select(x => new ComboItem(x.Item2.Name.Replace('-', '_'), x.Item2.LCID, x.File)) .ToList() ?? []; public bool Equals(CultureInfoCombo other) diff --git a/Aigamo.ResXGenerator/Tools/GenFileOptions.cs b/Aigamo.ResXGenerator/Tools/GenFileOptions.cs index 34444e5..8dced47 100644 --- a/Aigamo.ResXGenerator/Tools/GenFileOptions.cs +++ b/Aigamo.ResXGenerator/Tools/GenFileOptions.cs @@ -17,6 +17,7 @@ public readonly record struct GenFileOptions public string? CustomToolNamespace { get; init; } public string LocalNamespace { get; init; } public bool GenerateCode { get; init; } + public GenerationType GenerationType { get; init; } public string EmbeddedFilename { get; init; } public bool SkipFile { get; init; } public bool IsValid { get; init; } @@ -131,6 +132,16 @@ GlobalOptions globalOptions GenerateCode = genCodeSwitch.Equals("true", StringComparison.OrdinalIgnoreCase); } + GenerationType = globalOptions.GenerationType; + if ( + options.TryGetValue("build_metadata.EmbeddedResource.GenerationType", out var generationTypeSwitch) && + Enum.TryParse(generationTypeSwitch, true, out GenerationType g) && + g != GenerationType.SameAsOuter + ) + { + GenerationType = g; + } + if ( options.TryGetValue("build_metadata.EmbeddedResource.SkipFile", out var skipFile) && skipFile is { Length: > 0 } diff --git a/Aigamo.ResXGenerator/Tools/GenerationType.cs b/Aigamo.ResXGenerator/Tools/GenerationType.cs new file mode 100644 index 0000000..0ea787f --- /dev/null +++ b/Aigamo.ResXGenerator/Tools/GenerationType.cs @@ -0,0 +1,9 @@ +namespace Aigamo.ResXGenerator.Tools; + +public enum GenerationType +{ + ResourceManager, + CodeGeneration, + StringLocalizer, + SameAsOuter +} diff --git a/Aigamo.ResXGenerator/Tools/GlobalOptions.cs b/Aigamo.ResXGenerator/Tools/GlobalOptions.cs index 0e560f8..ee8c300 100644 --- a/Aigamo.ResXGenerator/Tools/GlobalOptions.cs +++ b/Aigamo.ResXGenerator/Tools/GlobalOptions.cs @@ -18,6 +18,7 @@ public sealed record GlobalOptions // this must be a record or implement IEquata public bool PublicClass { get; } public string ClassNamePostfix { get; } public bool GenerateCode { get; } + public GenerationType GenerationType { get; } public bool IsValid { get; } public GlobalOptions(AnalyzerConfigOptions options) @@ -99,14 +100,18 @@ public GlobalOptions(AnalyzerConfigOptions options) InnerClassInstanceName = innerClassInstanceNameSwitch; } - GenerateCode = false; - if ( + GenerateCode = options.TryGetValue("build_property.ResXGenerator_GenerateCode", out var genCodeSwitch) && genCodeSwitch is { Length: > 0 } && - genCodeSwitch.Equals("true", StringComparison.OrdinalIgnoreCase) + genCodeSwitch.Equals("true", StringComparison.OrdinalIgnoreCase); + + GenerationType = GenerationType.ResourceManager; + if ( + options.TryGetValue("build_property.ResXGenerator_GenerationType", out var generationTypeSwitch) && + Enum.TryParse(generationTypeSwitch, true, out GenerationType g) ) { - GenerateCode = true; + GenerationType = g; } } diff --git a/Aigamo.ResXGenerator/Tools/IGenerator.cs b/Aigamo.ResXGenerator/Tools/IGenerator.cs index 202a99e..c29430c 100644 --- a/Aigamo.ResXGenerator/Tools/IGenerator.cs +++ b/Aigamo.ResXGenerator/Tools/IGenerator.cs @@ -1,4 +1,5 @@ -using Aigamo.ResXGenerator.Generators; +using System.Collections.Immutable; +using Aigamo.ResXGenerator.Models; namespace Aigamo.ResXGenerator.Tools; @@ -8,7 +9,6 @@ public interface IGenerator } public interface IResXGenerator : IGenerator; -public interface IComboGenerator : IGenerator -{ - string GeneratedFileName(CultureInfoCombo combo); -} +public interface IComboGenerator : IGenerator; +public interface ILocalRegisterGenerator : IGenerator; +public interface IGlobalRegisterGenerator : IGenerator>; diff --git a/Aigamo.ResXGenerator/Tools/IntegrityValidator.cs b/Aigamo.ResXGenerator/Tools/IntegrityValidator.cs new file mode 100644 index 0000000..8cc01db --- /dev/null +++ b/Aigamo.ResXGenerator/Tools/IntegrityValidator.cs @@ -0,0 +1,115 @@ +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Aigamo.ResXGenerator.Tools; + +public class IntegrityValidator +{ + private readonly HashSet _alreadyAddedMembers = []; + public List ErrorsAndWarnings { get; } = []; + + public bool ValidateMember(FallBackItem fallBackItem, GenFileOptions options) => ValidateMember(fallBackItem, options, options.ClassName); + + public bool ValidateMember(FallBackItem fallBackItem, GenFileOptions options, string className) + { + var valid = true; + + if (!_alreadyAddedMembers.Add(fallBackItem.Key)) + { + ErrorsAndWarnings.Add(Diagnostic.Create( + descriptor: Analyser.DuplicateWarning, + location: Utilities.LocateMember(fallBackItem, options), + fallBackItem.Key + )); + + valid = false; + } + + if (fallBackItem.Key != className) return valid; + + ErrorsAndWarnings.Add(Diagnostic.Create( + descriptor: Analyser.MemberSameAsClassWarning, + location: Utilities.LocateMember(fallBackItem, options), + fallBackItem.Key + )); + + valid = false; + + return valid; + } + + /// + /// Cannot have static members/class with a class instance + /// + /// + /// + public bool ValidateInconsistentModificator(GenFileOptions options) + { + if (options.StaticClass == options.StaticMembers) return true; + + ErrorsAndWarnings.Add(Diagnostic.Create( + descriptor: Analyser.MemberWithStaticError, + location: Location.Create( + filePath: options.GroupedFile.MainFile.File.Path, + textSpan: new TextSpan(), + lineSpan: new LinePositionSpan() + ) + )); + + return false; + } + + public bool ValidateLocalizationModifiers(GenFileOptions options) + { + var valid = true; + if (options.GenerationType != GenerationType.StringLocalizer) return valid; + if (options.StaticClass || options.StaticMembers) + { + ErrorsAndWarnings.Add(Diagnostic.Create( + descriptor: Analyser.LocalizerStaticError, + location: Location.Create( + filePath: options.GroupedFile.MainFile.File.Path, + textSpan: new TextSpan(), + lineSpan: new LinePositionSpan() + ) + )); + valid = false; + } + + if (options.PartialClass) + { + ErrorsAndWarnings.Add(Diagnostic.Create( + descriptor: Analyser.LocalizerPartialError, + location: Location.Create( + filePath: options.GroupedFile.MainFile.File.Path, + textSpan: new TextSpan(), + lineSpan: new LinePositionSpan()))); + + valid = false; + } + + return valid; + } + + public bool ValidateInconsistentNameSpace(GenFileOptions options) + { + if (options.GenerationType != GenerationType.StringLocalizer || + options.CustomToolNamespace.IsNullOrEmpty() || + options.CustomToolNamespace == options.LocalNamespace) + return true; + + ErrorsAndWarnings.Add(Diagnostic.Create( + descriptor: Analyser.LocalizationIncoherentNamespace, + location: Location.Create( + filePath: options.GroupedFile.MainFile.File.Path, + textSpan: new TextSpan(), + lineSpan: new LinePositionSpan() + ) + )); + + return false; + + } +} diff --git a/Aigamo.ResXGenerator/Tools/RegexDefinitions.cs b/Aigamo.ResXGenerator/Tools/RegexDefinitions.cs index 43a8db5..95b6e9a 100644 --- a/Aigamo.ResXGenerator/Tools/RegexDefinitions.cs +++ b/Aigamo.ResXGenerator/Tools/RegexDefinitions.cs @@ -1,15 +1,21 @@ using System.Text.RegularExpressions; namespace Aigamo.ResXGenerator.Tools; + internal static class RegexDefinitions { - public static readonly Regex ValidMemberNamePattern = new( - pattern: @"^[\p{L}\p{Nl}_][\p{Cf}\p{L}\p{Mc}\p{Mn}\p{Nd}\p{Nl}\p{Pc}]*$", - options: RegexOptions.Compiled | RegexOptions.CultureInvariant - ); - - public static readonly Regex InvalidMemberNameSymbols = new( - pattern: @"[^\p{Cf}\p{L}\p{Mc}\p{Mn}\p{Nd}\p{Nl}\p{Pc}]", - options: RegexOptions.Compiled | RegexOptions.CultureInvariant - ); + public static readonly Regex ValidMemberNamePattern = new( + pattern: @"^[\p{L}\p{Nl}_][\p{Cf}\p{L}\p{Mc}\p{Mn}\p{Nd}\p{Nl}\p{Pc}]*$", + options: RegexOptions.Compiled | RegexOptions.CultureInvariant + ); + + public static readonly Regex InvalidMemberNameSymbols = new( + pattern: @"[^\p{Cf}\p{L}\p{Mc}\p{Mn}\p{Nd}\p{Nl}\p{Pc}]", + options: RegexOptions.Compiled | RegexOptions.CultureInvariant + ); + + public static readonly Regex NewLine = new( + pattern: @"\r\n|\n\r|\n|\r", + options: RegexOptions.Compiled | RegexOptions.CultureInvariant + ); } diff --git a/Aigamo.ResXGenerator/Tools/StringBuilderGeneratorHelper.cs b/Aigamo.ResXGenerator/Tools/StringBuilderGeneratorHelper.cs new file mode 100644 index 0000000..e1a3555 --- /dev/null +++ b/Aigamo.ResXGenerator/Tools/StringBuilderGeneratorHelper.cs @@ -0,0 +1,190 @@ +using System.Globalization; +using System.Resources; +using System.Text; +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Models; + +namespace Aigamo.ResXGenerator.Tools; + +public class StringBuilderGeneratorHelper +{ + private string ContainerClassName { get; set; } + private string Indent { get; set; } = " "; + private StringBuilder Builder { get; } = new(); + + public static string FunctionNamePostFix(IReadOnlyList? definedLanguages) => string.Join("_", definedLanguages?.Select(x => x.LCID) ?? []); + + public GeneratedOutput GetOutput(string fileName, IntegrityValidator validator) => new(fileName, Builder.ToString(), validator.ErrorsAndWarnings); + + public StringBuilderGeneratorHelper() => ContainerClassName = "Helpers"; + + public StringBuilderGeneratorHelper(GenFileOptions options) => ContainerClassName = options.ClassName; + + public void Append(string line) => Builder.Append(line); + public void AppendLine(string line) => Builder.AppendLine(line); + public void AppendFormat(string format, params object[] args) => Builder.AppendFormat(format, args); + public string AppendLanguages(IReadOnlyList languages) + { + var postFix = FunctionNamePostFix(languages); + Builder.Append(postFix); + return postFix; + } + + public void AppendHeader(string @namespace) + { + Builder.AppendLine(Constants.AutoGeneratedHeader); + Builder.AppendLine("#nullable enable"); + Builder.Append("namespace "); + Builder.Append(@namespace); + Builder.AppendLine(";"); + } + + public void AppendInnerClass(GenFileOptions options, IntegrityValidator validator) + { + if (options.InnerClassVisibility == InnerClassVisibility.NotGenerated) return; + + ContainerClassName = string.IsNullOrEmpty(options.InnerClassName) ? "Resources" : options.InnerClassName; + if (!string.IsNullOrEmpty(options.InnerClassInstanceName)) + { + validator.ValidateInconsistentModificator(options); + + Builder.Append(Indent); + Builder.Append("public "); + Builder.Append(ContainerClassName); + Builder.Append(" "); + Builder.Append(options.InnerClassInstanceName); + Builder.AppendLine(" { get; } = new();"); + Builder.AppendLine(); + } + + Builder.Append(Indent); + Builder.Append(GetInnerClassVisibility(options)); + Builder.Append(options.StaticClass ? " static" : string.Empty); + Builder.Append(options.PartialClass ? " partial class " : " class "); + + Builder.AppendLine(ContainerClassName); + Builder.Append(Indent); + Builder.AppendLine("{"); + + Indent += " "; + } + + public void AppendClassHeader(GenFileOptions options) + { + Builder.Append(options.PublicClass ? "public" : "internal"); + Builder.Append(options.StaticClass ? " static" : string.Empty); + Builder.Append(options.PartialClass ? " partial class " : " class "); + Builder.AppendLine(options.ClassName); + Builder.AppendLine("{"); + } + + public void AppendClassFooter(GenFileOptions options) + { + if (options.InnerClassVisibility != InnerClassVisibility.NotGenerated) + Builder.AppendLine(" }"); + + Builder.AppendLine("}"); + } + + public (bool valid, bool resourceAccessByName) GenerateMember(FallBackItem fallbackItem, GenFileOptions options, IntegrityValidator validator) + { + string memberName; + bool resourceAccessByName; + + if (RegexDefinitions.ValidMemberNamePattern.IsMatch(fallbackItem.Key)) + { + memberName = fallbackItem.Key; + resourceAccessByName = true; + } + else + { + memberName = RegexDefinitions.InvalidMemberNameSymbols.Replace(fallbackItem.Key, "_"); + resourceAccessByName = false; + } + + + if (!validator.ValidateMember(fallbackItem, options, ContainerClassName)) return (false, resourceAccessByName); + + Builder.AppendLine(); + + Builder.Append(Indent); + Builder.AppendLine("/// "); + + Builder.Append(Indent); + Builder.Append("/// Looks up a localized string similar to "); + Builder.Append(fallbackItem.Value.ToXmlCommentSafe(Indent)); + Builder.AppendLine("."); + + Builder.Append(Indent); + Builder.AppendLine("/// "); + + Builder.Append(Indent); + Builder.Append("public "); + Builder.Append(options.StaticMembers ? "static " : string.Empty); + Builder.Append("string"); + Builder.Append(options.NullForgivingOperators ? null : "?"); + Builder.Append(" "); + Builder.Append(memberName); + return (true, resourceAccessByName); + } + + public void AppendResourceManagerUsings() + { + Builder.Append("using "); + Builder.Append(Constants.SystemGlobalization); + Builder.AppendLine(";"); + + Builder.Append("using "); + Builder.Append(Constants.SystemResources); + Builder.AppendLine(";"); + + Builder.AppendLine(); + } + + public void AppendCodeUsings() + { + Builder.AppendLine("using static Aigamo.ResXGenerator.Helpers;"); + Builder.AppendLine(); + } + + public void GenerateResourceManagerMembers(GenFileOptions options) + { + Builder.Append(Indent); + Builder.Append("private static "); + Builder.Append(nameof(ResourceManager)); + Builder.Append("? "); + Builder.Append(Constants.SResourceManagerVariable); + Builder.AppendLine(";"); + + Builder.Append(Indent); + Builder.Append("public static "); + Builder.Append(nameof(ResourceManager)); + Builder.Append(" "); + Builder.Append(Constants.ResourceManagerVariable); + Builder.Append(" => "); + Builder.Append(Constants.SResourceManagerVariable); + Builder.Append(" ??= new "); + Builder.Append(nameof(ResourceManager)); + Builder.Append("(\""); + Builder.Append(options.EmbeddedFilename); + Builder.Append("\", typeof("); + Builder.Append(ContainerClassName); + Builder.AppendLine(").Assembly);"); + + Builder.Append(Indent); + Builder.Append("public "); + Builder.Append(options.StaticMembers ? "static " : string.Empty); + Builder.Append(nameof(CultureInfo)); + Builder.Append("? "); + Builder.Append(Constants.CultureInfoVariable); + Builder.AppendLine(" { get; set; }"); + } + + private static string GetInnerClassVisibility(GenFileOptions options) + { + if (options.InnerClassVisibility == InnerClassVisibility.SameAsOuter) + return options.PublicClass ? "public" : "internal"; + + return options.InnerClassVisibility.ToString().ToLowerInvariant(); + } +} diff --git a/Aigamo.ResXGenerator/Tools/Utilities.cs b/Aigamo.ResXGenerator/Tools/Utilities.cs index a99194c..ac458db 100644 --- a/Aigamo.ResXGenerator/Tools/Utilities.cs +++ b/Aigamo.ResXGenerator/Tools/Utilities.cs @@ -1,165 +1,175 @@ using System.Globalization; using System.Text.RegularExpressions; using Aigamo.ResXGenerator.Extensions; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; namespace Aigamo.ResXGenerator; public static class Utilities { - // Code from: https://github.com/dotnet/ResXResourceManager/blob/0ec11bae232151400a5a8ca7b9835ac063c516d0/src/ResXManager.Model/ResourceManager.cs#L267 - - private static bool IsValidLanguageName(string? languageName) - { - try - { - if (languageName.IsNullOrEmpty()) - { - return false; - } - - if (languageName.StartsWith("qps-", StringComparison.Ordinal)) - { - return true; - } - - var dash = languageName.IndexOf('-'); - if (dash >= 4 || (dash == -1 && languageName.Length >= 4)) - { - return false; - } - - var culture = new CultureInfo(languageName); - - while (!culture.IsNeutralCulture) - { - culture = culture.Parent; - } - - return culture.LCID != 4096; - } - catch - { - return false; - } - } - - // Code from: https://github.com/dotnet/ResXResourceManager/blob/0ec11bae232151400a5a8ca7b9835ac063c516d0/src/ResXManager.Model/ProjectFileExtensions.cs#L77 - - public static string GetBaseName(string filePath) - { - var name = Path.GetFileNameWithoutExtension(filePath); - var innerExtension = Path.GetExtension(name); - var languageName = innerExtension.TrimStart('.'); - - return IsValidLanguageName(languageName) ? Path.GetFileNameWithoutExtension(name) : name; - } - - // Code from: https://github.com/dotnet/ResXResourceManager/blob/c8b5798d760f202a1842a74191e6010c6e8bbbc0/src/ResXManager.VSIX/Visuals/MoveToResourceViewModel.cs#L120 - - public static string GetLocalNamespace( - string? resxPath, - string? targetPath, - string projectPath, - string projectName, - string? rootNamespace - ) - { - try - { - if (resxPath is null) - { - return string.Empty; - } - - var resxFolder = Path.GetDirectoryName(resxPath); - var projectFolder = Path.GetDirectoryName(projectPath); - rootNamespace ??= string.Empty; - - if (resxFolder is null || projectFolder is null) - { - return string.Empty; - } - - var localNamespace = string.Empty; - - if (!string.IsNullOrWhiteSpace(targetPath)) - { - localNamespace = Path.GetDirectoryName(targetPath) - .Trim(Path.DirectorySeparatorChar) - .Trim(Path.AltDirectorySeparatorChar) - .Replace(Path.DirectorySeparatorChar, '.') - .Replace(Path.AltDirectorySeparatorChar, '.'); - } - else if (resxFolder.StartsWith(projectFolder, StringComparison.OrdinalIgnoreCase)) - { - localNamespace = resxFolder - .Substring(projectFolder.Length) - .Trim(Path.DirectorySeparatorChar) - .Trim(Path.AltDirectorySeparatorChar) - .Replace(Path.DirectorySeparatorChar, '.') - .Replace(Path.AltDirectorySeparatorChar, '.'); - } - - if (string.IsNullOrEmpty(rootNamespace) && string.IsNullOrEmpty(localNamespace)) - { - // If local namespace is empty, e.g file is in root project folder, root namespace set to empty - // fallback to project name as a namespace - localNamespace = SanitizeNamespace(projectName); - } - else - { - localNamespace = (string.IsNullOrEmpty(localNamespace) - ? rootNamespace - : $"{rootNamespace}.{SanitizeNamespace(localNamespace, false)}") - .Trim('.'); - } - - return localNamespace; - } - catch (Exception) - { - return string.Empty; - } - } - - public static string GetClassNameFromPath(string resxFilePath) - { - // Fix issues with files that have names like xxx.aspx.resx - var className = resxFilePath; - while (className.Contains(".")) - { - className = Path.GetFileNameWithoutExtension(className); - } - - return className; - } - - public static string SanitizeNamespace(string ns, bool sanitizeFirstChar = true) - { - if (string.IsNullOrEmpty(ns)) - { - return ns; - } - - // A namespace must contain only alphabetic characters, decimal digits, dots and underscores, and must begin with an alphabetic character or underscore (_) - // In case there are invalid chars we'll use same logic as Visual Studio and replace them with underscore (_) and append underscore (_) if project does not start with alphabetic or underscore (_) - - var sanitizedNs = Regex - .Replace(ns, @"[^a-zA-Z0-9_\.]", "_"); - - // Handle folder containing multiple dots, e.g. 'test..test2' or starting, ending with dots - sanitizedNs = Regex - .Replace(sanitizedNs, @"\.+", "."); - - if (sanitizeFirstChar) - { - sanitizedNs = sanitizedNs.Trim('.'); - } - - return sanitizeFirstChar - // Handle namespace starting with digit - ? char.IsDigit(sanitizedNs[0]) ? $"_{sanitizedNs}" : sanitizedNs - : sanitizedNs; - } + // Code from: https://github.com/dotnet/ResXResourceManager/blob/0ec11bae232151400a5a8ca7b9835ac063c516d0/src/ResXManager.Model/ResourceManager.cs#L267 + + private static bool IsValidLanguageName(string? languageName) + { + try + { + if (languageName.IsNullOrEmpty()) + { + return false; + } + + if (languageName!.StartsWith("qps-", StringComparison.Ordinal)) + { + return true; + } + + var dash = languageName.IndexOf('-'); + if (dash >= 4 || (dash == -1 && languageName.Length >= 4)) + { + return false; + } + + var culture = new CultureInfo(languageName); + + while (!culture.IsNeutralCulture) + { + culture = culture.Parent; + } + + return culture.LCID != 4096; + } + catch + { + return false; + } + } + + // Code from: https://github.com/dotnet/ResXResourceManager/blob/0ec11bae232151400a5a8ca7b9835ac063c516d0/src/ResXManager.Model/ProjectFileExtensions.cs#L77 + + public static string GetBaseName(string filePath) + { + var name = Path.GetFileNameWithoutExtension(filePath); + var innerExtension = Path.GetExtension(name); + var languageName = innerExtension.TrimStart('.'); + + return IsValidLanguageName(languageName) ? Path.GetFileNameWithoutExtension(name) : name; + } + + // Code from: https://github.com/dotnet/ResXResourceManager/blob/c8b5798d760f202a1842a74191e6010c6e8bbbc0/src/ResXManager.VSIX/Visuals/MoveToResourceViewModel.cs#L120 + + public static string GetLocalNamespace( + string? resxPath, + string? targetPath, + string projectPath, + string projectName, + string? rootNamespace + ) + { + try + { + if (resxPath is null) + { + return string.Empty; + } + + var resxFolder = Path.GetDirectoryName(resxPath); + var projectFolder = Path.GetDirectoryName(projectPath); + rootNamespace ??= string.Empty; + + if (resxFolder is null || projectFolder is null) + { + return string.Empty; + } + + var localNamespace = string.Empty; + + if (!string.IsNullOrWhiteSpace(targetPath)) + { + localNamespace = Path.GetDirectoryName(targetPath)! + .Trim(Path.DirectorySeparatorChar) + .Trim(Path.AltDirectorySeparatorChar) + .Replace(Path.DirectorySeparatorChar, '.') + .Replace(Path.AltDirectorySeparatorChar, '.'); + } + else if (resxFolder.StartsWith(projectFolder, StringComparison.OrdinalIgnoreCase)) + { + localNamespace = resxFolder + .Substring(projectFolder.Length) + .Trim(Path.DirectorySeparatorChar) + .Trim(Path.AltDirectorySeparatorChar) + .Replace(Path.DirectorySeparatorChar, '.') + .Replace(Path.AltDirectorySeparatorChar, '.'); + } + + if (string.IsNullOrEmpty(rootNamespace) && string.IsNullOrEmpty(localNamespace)) + { + // If local namespace is empty, e.g file is in root project folder, root namespace set to empty + // fallback to project name as a namespace + localNamespace = SanitizeNamespace(projectName); + } + else + { + localNamespace = (string.IsNullOrEmpty(localNamespace) + ? rootNamespace + : $"{rootNamespace}.{SanitizeNamespace(localNamespace, false)}") + .Trim('.'); + } + + return localNamespace; + } + catch (Exception) + { + return string.Empty; + } + } + + public static string GetClassNameFromPath(string resxFilePath) + { + // Fix issues with files that have names like xxx.aspx.resx + var className = resxFilePath; + while (className.Contains(".")) + { + className = Path.GetFileNameWithoutExtension(className); + } + + return className; + } + + public static string SanitizeNamespace(this string ns, bool sanitizeFirstChar = true) + { + if (string.IsNullOrEmpty(ns)) + return ns; + + // A namespace must contain only alphabetic characters, decimal digits, dots and underscores, and must begin with an alphabetic character or underscore (_) + // In case there are invalid chars we'll use same logic as Visual Studio and replace them with underscore (_) and append underscore (_) if project does not start with alphabetic or underscore (_) + + var sanitizedNs = Regex.Replace(ns, @"[^a-zA-Z0-9_\.]", "_"); + + // Handle folder containing multiple dots, e.g. 'test..test2' or starting, ending with dots + sanitizedNs = Regex.Replace(sanitizedNs, @"\.+", "."); + + if (sanitizeFirstChar) sanitizedNs = sanitizedNs.Trim('.'); + + // Handle namespace starting with digit + if (sanitizeFirstChar) return char.IsDigit(sanitizedNs[0]) ? $"_{sanitizedNs}" : sanitizedNs; + return sanitizedNs; + } + + public static string NamespaceNameCompliant(this string ns) => Regex.Replace(ns, @"[\.\-_]", ""); + + public static Location LocateMember(FallBackItem fallBackItem, GenFileOptions options) + { + var line = fallBackItem.Line; + return Location.Create( + filePath: options.GroupedFile.MainFile.File.Path, + textSpan: new TextSpan(), + lineSpan: new LinePositionSpan( + start: new LinePosition(line.LineNumber - 1, line.LinePosition - 1), + end: new LinePosition(line.LineNumber - 1, line.LinePosition - 1 + fallBackItem.Key.Length) + ) + ); + } } diff --git a/Aigamo.ResXGenerator/build/Aigamo.ResXGenerator.props b/Aigamo.ResXGenerator/build/Aigamo.ResXGenerator.props index 878e986..37c4f89 100644 --- a/Aigamo.ResXGenerator/build/Aigamo.ResXGenerator.props +++ b/Aigamo.ResXGenerator/build/Aigamo.ResXGenerator.props @@ -19,6 +19,7 @@ + @@ -32,6 +33,7 @@ + diff --git a/DemoLocalization/DemoLocalization.csproj b/DemoLocalization/DemoLocalization.csproj new file mode 100644 index 0000000..13680ee --- /dev/null +++ b/DemoLocalization/DemoLocalization.csproj @@ -0,0 +1,33 @@ + + + + Exe + net8.0 + enable + enable + latest + false + true + StringLocalizer + + + + + + + + + + + + + + + + + + $([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx + + + + diff --git a/DemoLocalization/Langage.cs b/DemoLocalization/Langage.cs new file mode 100644 index 0000000..f940ee5 --- /dev/null +++ b/DemoLocalization/Langage.cs @@ -0,0 +1,7 @@ +namespace DemoLocalization; +public enum Langage +{ + English, + Danish, + French +} diff --git a/DemoLocalization/MainProcess.cs b/DemoLocalization/MainProcess.cs new file mode 100644 index 0000000..c4774e7 --- /dev/null +++ b/DemoLocalization/MainProcess.cs @@ -0,0 +1,16 @@ +namespace DemoLocalization; + +internal interface IMainProcess +{ + void Run(); +} + +internal class MainProcess(IResource resources) : IMainProcess +{ + public void Run() + { + // Afficher le message localisé + Console.WriteLine(resources.HelloWorld); + Console.ReadKey(); + } +} diff --git a/DemoLocalization/Options.cs b/DemoLocalization/Options.cs new file mode 100644 index 0000000..522c604 --- /dev/null +++ b/DemoLocalization/Options.cs @@ -0,0 +1,13 @@ +using CommandLine; + +namespace DemoLocalization; +public class Options +{ + [Option('l', "locale", Required = false, HelpText = """ + Set the langage of the application values: + - 0 -> English + - 1 -> Danish + - 2 -> French + """)] + public int Locale { get; set; } +} diff --git a/DemoLocalization/Program.cs b/DemoLocalization/Program.cs new file mode 100644 index 0000000..6709346 --- /dev/null +++ b/DemoLocalization/Program.cs @@ -0,0 +1,38 @@ +using System.Globalization; +using CommandLine; +using Microsoft.Extensions.DependencyInjection; +using ResXGenerator.Registration; + +namespace DemoLocalization; + +public static class Program +{ + public static void Main(string[] args) + { + // Configurer les services + var services = new ServiceCollection(); + services.AddLogging(); + services.AddLocalization(); + services.UsingResXGenerator(); + services.AddTransient(); + var provider = services.BuildServiceProvider(); + + Parser.Default.ParseArguments(args).WithParsed(o => + { + RunProgram(o, provider); + }); + } + + private static void RunProgram(Options opt, IServiceProvider provider) + { + CultureInfo.CurrentUICulture = opt.Locale switch + { + 1 => new CultureInfo("da"), + 2 => new CultureInfo("fr"), + _ => new CultureInfo("en") + }; + + var process = provider.GetService(); + process?.Run(); + } +} diff --git a/DemoLocalization/Resource.da.resx b/DemoLocalization/Resource.da.resx new file mode 100644 index 0000000..87b88ae --- /dev/null +++ b/DemoLocalization/Resource.da.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Hej Verden + + \ No newline at end of file diff --git a/DemoLocalization/Resource.fr.resx b/DemoLocalization/Resource.fr.resx new file mode 100644 index 0000000..c70ab5a --- /dev/null +++ b/DemoLocalization/Resource.fr.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Bonjour monde + + diff --git a/DemoLocalization/Resource.resx b/DemoLocalization/Resource.resx new file mode 100644 index 0000000..8bec740 --- /dev/null +++ b/DemoLocalization/Resource.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Hello World + + \ No newline at end of file diff --git a/README.md b/README.md index 00584a7..d07d4ed 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,67 @@ namespace Resources - The generator can now generate code to lookup translations instead of using the 20 year old System.Resources.ResourceManager +## New in version 4 +- The generator now include support for IStringLocalizer and static method to register in IServiceCollection (no more need of "magic string" :blush: +```c# +var local = provider.GetService(); +local.Actor // return the value string +``` + +or + +```c# +var myViewModel = provider.GetService(); + +public class MyViewModel(IArtistCategoriesNames resources) : IMyViewModel +{ + public string Value => resources.Actor // retur also the value string +} +``` + ## Options + +### GenerationType (per file or globally) +Use cases: https://github.com/ycanardeau/ResXGenerator/issues/6. + +Because the dependency injection on somes frameworks like Blazor. Uses the IStringLocalizer to get resources string. And to keep actual functionality. It now possible to choose the gerneration type by setting 'GenerationType'. + +This paramerter is optional The default value is 'ResourceManager' to keep the actual behavior by default. + +AllowedValues : +- ResourceManager + - When this option choosen the generator will use the classic [System.Resources.ResourceManager](https://learn.microsoft.com/en-us/dotnet/api/system.resources.resourcemanager?view=net-9.0) to get resources string. +- CodeGeneration (You can chose this option to replace the GenerateCode option) + - When this option choosen the generator will generate code to get resources string. See [Generate Code](#Generate-Code) for more details)) +- StringLocalizer + - When this option choosen the generator will generate interface and class to use with [Microsoft.Extensions.Localization](https://docs.microsoft.com/en-us/dotnet/core/extensions/localization) `IStringLocalizer`. To see how to use it see [Using IStringLocalizer](#IStringLocalizer) +- SameAsOuter + - When this option choosen the generator will use the same generation type as the outer class if any. If no outer class exist it will fallback to 'ResourceManager'. +```xml + + + StringLocalizer + + +``` + +or + +```xml + + + +``` + +If you want to apply this globally, use + +```xml + + StringLocalizer + +``` + ### PublicClass (per file or globally) Use cases: https://github.com/VocaDB/ResXFileCodeGenerator/issues/2. @@ -146,7 +205,7 @@ With global non-static class you can also reset `StaticClass` per file by settin ### Partial classes (per file or globally) -To extend an existing class, you can make your classes partial. +To extend an existing class, you can make your classes partial. *Not suitable for StringLocalizer*. ```xml @@ -166,7 +225,7 @@ or globally ### Static Members (per file or globally) -In some rare cases it might be useful for the members to be non-static. +In some rare cases it might be useful for the members to be non-static. *Not suitable for StringLocalizer*. ```xml @@ -217,7 +276,7 @@ or just the postfix globally ## Inner classes (per file or globally) -If your resx files are organized along with code files, it can be quite useful to ensure that the resources are not accessible outside the specific class the resx file belong to. +If your resx files are organized along with code files, it can be quite useful to ensure that the resources are not accessible outside the specific class the resx file belong to. *Not suitable for StringLocalizer* ```xml @@ -362,6 +421,7 @@ or globally For brevity, settings to make everything non-static is omitted. + ### Generate Code (per file or globally) By default the ancient `System.Resources.ResourceManager` is used. @@ -492,10 +552,42 @@ Alternatively it can be set with the attribute `SkipFile="true"`. ```xml - + ``` + +## Using IStringLocalizer + +To enablethe génération of interface and classes for your resource yo need to set de GenerationType to [StringLocalizer](#GenerationType). Note to use this you need to ensure you reference nuget on your project. +- [Microsoft.Extensions.DependencyInjection](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection) +- [Microsoft.Extensions.Localization](https://www.nuget.org/packages/Microsoft.Extensions.Localization) +- [Microsoft.Extensions.Logging](https://www.nuget.org/packages/Microsoft.Extensions.Logging) + +Now yous can register singletons for resources, simply using extension methods. + +Examples for a file ArtistCategoriesNames.resx : +```c# +using ResXGenerator.Registration; + +builder.Services + .AddLogging(); + .AddLocalization() + .UsingResXGenerator(); //This will register all resx class in one line of code +``` + +you can also (if you prefer to load ressources by namespace using individual registrations). A class of registration is created by namespace. +```c# +using MyResourceAssembly; + +builder.Services + .AddLogging(); + .AddLocalization() + .UsingArtistCategoriesNamesResX(); //This will register all resx class of the namespace in one line of code +``` + +Now simply use the dependency injection to get your resources classes. All interface is in the same namespase as it's resource file. (or configured namespace) + ## References - [Introducing C# Source Generators | .NET Blog](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/) From 977791549c53519cbea4be9c787f7cb3ba3bf5b3 Mon Sep 17 00:00:00 2001 From: FranckSix Date: Sun, 21 Sep 2025 00:53:01 -0400 Subject: [PATCH 03/14] Replace the option Replace the Tab in .editorconfig readjust files to instead of spaces readjust unit tests --- .editorconfig | 2 +- .../AdditionalTextStub.cs | 8 +- .../Aigamo.ResXGenerator.Tests.csproj | 88 +- Aigamo.ResXGenerator.Tests/CodeGenTests.cs | 131 +-- .../CodeResXTestsHelpers.cs | 451 +++++----- .../GeneratorLocalizerTests.cs | 539 +++++------ Aigamo.ResXGenerator.Tests/GeneratorTests.cs | 813 +++++++++-------- .../GithubIssues/Issue3/GeneratorTests.cs | 407 ++++----- .../GlobalRegisterTests.cs | 148 +-- .../HelperGeneratorTests.cs | 82 +- .../IntegrationTests/Test1.da-dk.resx | 6 +- .../IntegrationTests/Test1.da.resx | 6 +- .../IntegrationTests/Test1.en-us.resx | 6 +- .../IntegrationTests/Test1.resx | 6 +- .../IntegrationTests/Test2.da-dk.resx | 6 +- .../IntegrationTests/Test2.da.resx | 6 +- .../IntegrationTests/Test2.en-us.resx | 6 +- .../IntegrationTests/Test2.resx | 6 +- .../IntegrationTests/Test3.da-dk.resx | 6 +- .../IntegrationTests/Test3.da.resx | 6 +- .../IntegrationTests/Test3.en-us.resx | 6 +- .../IntegrationTests/Test3.resx | 6 +- .../IntegrationTests/Test4.da-us.da-dk.resx | 6 +- .../IntegrationTests/Test4.da-us.da.resx | 6 +- .../IntegrationTests/Test4.da-us.resx | 6 +- .../IntegrationTests/Test4.resx | 240 ++--- .../IntegrationTests/TestResxFiles.cs | 7 +- .../LocalizerRegisterTests.cs | 118 +-- .../Properties/launchSettings.json | 20 +- Aigamo.ResXGenerator.Tests/SettingsTests.cs | 846 +++++++++--------- Aigamo.ResXGenerator.Tests/UtilitiesTests.cs | 66 +- .../Aigamo.ResXGenerator.csproj | 98 +- .../Aigamo.ResXGenerator.targets | 24 +- Aigamo.ResXGenerator/Analyser.cs | 144 +-- .../Extensions/BoolExtensions.cs | 9 +- .../Extensions/EnumerableExtensions.cs | 9 +- .../Extensions/StringExtensions.cs | 25 +- .../Generators/CodeGenerator.cs | 116 +-- .../Generators/ComboGenerator.cs | 118 +-- .../Generators/GeneratorBase.cs | 58 +- .../Generators/LocalizerGenerator.cs | 109 +-- .../LocalizerGlobalRegisterGenerator.cs | 63 +- .../Generators/LocalizerRegisterGenerator.cs | 68 +- .../Generators/ResourceManagerGenerator.cs | 104 +-- .../Models/GenFilesNamespace.cs | 6 +- .../Properties/launchSettings.json | 20 +- Aigamo.ResXGenerator/SourceGenerator.cs | 337 ++++--- .../Tools/AdditionalTextWithHash.cs | 18 +- Aigamo.ResXGenerator/Tools/Constants.cs | 11 +- .../Tools/CultureInfoCombo.cs | 10 +- Aigamo.ResXGenerator/Tools/GenerationType.cs | 8 +- .../Tools/IntegrityValidator.cs | 210 ++--- .../Tools/NullableAttributes.cs | 312 +++---- .../Tools/RegexDefinitions.cs | 24 +- .../Tools/StringBuilderGeneratorHelper.cs | 358 ++++---- Aigamo.ResXGenerator/Tools/Utilities.cs | 324 +++---- .../build/Aigamo.ResXGenerator.props | 68 +- DemoLocalization/DemoLocalization.csproj | 50 +- DemoLocalization/Langage.cs | 1 + DemoLocalization/Options.cs | 11 +- DemoLocalization/Resource.da.resx | 240 ++--- DemoLocalization/Resource.fr.resx | 238 ++--- DemoLocalization/Resource.resx | 240 ++--- README.md | 412 ++++----- 64 files changed, 3966 insertions(+), 3933 deletions(-) diff --git a/.editorconfig b/.editorconfig index 7e4089c..cae39eb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,7 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -indent_style = space +indent_style = tab indent_size = 4 # XML project files diff --git a/Aigamo.ResXGenerator.Tests/AdditionalTextStub.cs b/Aigamo.ResXGenerator.Tests/AdditionalTextStub.cs index d19eb34..5bc9efc 100644 --- a/Aigamo.ResXGenerator.Tests/AdditionalTextStub.cs +++ b/Aigamo.ResXGenerator.Tests/AdditionalTextStub.cs @@ -1,13 +1,13 @@ -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; namespace Aigamo.ResXGenerator.Tests; internal class AdditionalTextStub(string path, string? text = null) : AdditionalText { - private readonly SourceText? _text = text is null ? null : SourceText.From(text); + private readonly SourceText? _text = text is null ? null : SourceText.From(text); - public override string Path { get; } = path; + public override string Path { get; } = path; - public override SourceText? GetText(CancellationToken cancellationToken = new()) => _text; + public override SourceText? GetText(CancellationToken cancellationToken = new()) => _text; } diff --git a/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj b/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj index f946bf0..670154f 100644 --- a/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj +++ b/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj @@ -1,50 +1,50 @@  - - net8.0 - false - latest - enable - enable - + + net8.0 + false + latest + enable + enable + - + - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - $([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx - - - CodeGeneration - - - true - - - StringLocalizer - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + $([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx + + + CodeGeneration + + + true + + + StringLocalizer + + - - - + + + diff --git a/Aigamo.ResXGenerator.Tests/CodeGenTests.cs b/Aigamo.ResXGenerator.Tests/CodeGenTests.cs index 0638c1b..61f055f 100644 --- a/Aigamo.ResXGenerator.Tests/CodeGenTests.cs +++ b/Aigamo.ResXGenerator.Tests/CodeGenTests.cs @@ -7,76 +7,77 @@ namespace Aigamo.ResXGenerator.Tests; public class CodeGenTests { - private static void Generate( - IResXGenerator generator, - bool publicClass = true, - bool staticClass = true, - bool partial = false, - bool nullForgivingOperators = false, - bool staticMembers = true - ) - { - var expected = $$""" - // ------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - // ------------------------------------------------------------------------------ - #nullable enable - namespace Resources; - using static Aigamo.ResXGenerator.Helpers; + private static void Generate( + IResXGenerator generator, + bool publicClass = true, + bool staticClass = true, + bool partial = false, + bool nullForgivingOperators = false, + bool staticMembers = true + ) + { + var expected = + $$""" + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace Resources; + using static Aigamo.ResXGenerator.Helpers; - {{(publicClass ? "public" : "internal")}}{{(partial ? " partial" : string.Empty)}}{{(staticClass ? " static" : string.Empty)}} class ActivityEntrySortRuleNames - { + {{(publicClass ? "public" : "internal")}}{{(partial ? " partial" : string.Empty)}}{{(staticClass ? " static" : string.Empty)}} class ActivityEntrySortRuleNames + { - /// - /// Looks up a localized string similar to Oldest. - /// - public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDate => GetString_1030_6("Oldest", "OldestDaDK", "OldestDa"); + /// + /// Looks up a localized string similar to Oldest. + /// + public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDate => GetString_1030_6("Oldest", "OldestDaDK", "OldestDa"); - /// - /// Looks up a localized string similar to Newest. - /// - public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDateDescending => GetString_1030_6("Newest", "NewestDaDK", "NewestDa"); - } + /// + /// Looks up a localized string similar to Newest. + /// + public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDateDescending => GetString_1030_6("Newest", "NewestDaDK", "NewestDa"); + } - """; + """; - var result = generator.Generate( - new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", - CustomToolNamespace = "Resources", - ClassName = "ActivityEntrySortRuleNames", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(string.Empty, CodeResXTestsHelpers.GetText()), Guid.NewGuid()), - subFiles: - [ - new AdditionalTextWithHash(new AdditionalTextStub("test.da.rex", CodeResXTestsHelpers.GetText("Da")), Guid.NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.da-dk.rex", CodeResXTestsHelpers.GetText("DaDK")), Guid.NewGuid()) - ] - ), - PublicClass = publicClass, - GenerateCode = true, - NullForgivingOperators = nullForgivingOperators, - StaticClass = staticClass, - PartialClass = partial, - StaticMembers = staticMembers - }); - result.ErrorsAndWarnings.Should().BeNullOrEmpty(); - result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } + var result = generator.Generate( + new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", + CustomToolNamespace = "Resources", + ClassName = "ActivityEntrySortRuleNames", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub(string.Empty, CodeResXTestsHelpers.GetText()), Guid.NewGuid()), + subFiles: + [ + new AdditionalTextWithHash(new AdditionalTextStub("test.da.rex", CodeResXTestsHelpers.GetText("Da")), Guid.NewGuid()), + new AdditionalTextWithHash(new AdditionalTextStub("test.da-dk.rex", CodeResXTestsHelpers.GetText("DaDK")), Guid.NewGuid()) + ] + ), + PublicClass = publicClass, + GenerateCode = true, + NullForgivingOperators = nullForgivingOperators, + StaticClass = staticClass, + PartialClass = partial, + StaticMembers = staticMembers + }); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } - [Fact] - public void Generate_StringBuilder_Public() - { - var generator = new CodeGenerator(); - Generate(generator); - Generate(generator, nullForgivingOperators: true); - } + [Fact] + public void Generate_StringBuilder_Public() + { + var generator = new CodeGenerator(); + Generate(generator); + Generate(generator, nullForgivingOperators: true); + } } diff --git a/Aigamo.ResXGenerator.Tests/CodeResXTestsHelpers.cs b/Aigamo.ResXGenerator.Tests/CodeResXTestsHelpers.cs index 8437414..55ea124 100644 --- a/Aigamo.ResXGenerator.Tests/CodeResXTestsHelpers.cs +++ b/Aigamo.ResXGenerator.Tests/CodeResXTestsHelpers.cs @@ -1,230 +1,235 @@ -namespace Aigamo.ResXGenerator.Tests; +namespace Aigamo.ResXGenerator.Tests; internal static class CodeResXTestsHelpers { - public static string GetText() => GetText(string.Empty); - public static string GetText(string language) => $""" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Oldest{language} - - - Newest{language} - - - """; + public static string GetText() => GetText(string.Empty); - public static string GetDaTextWithDuplicates() => """ - - - - Works. - - - Doeesnt Work. - - - """; + public static string GetText(string language) => + $""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Oldest{language} + + + Newest{language} + + + """; - public static string GetTextWithNewline() => """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - This entry has been deleted. It is still temporarily accessible, but won't show up in any of the listings. - - - This entry was merged to - - - Draft = entry is missing crucial information. This status indicates that you're requesting additional information to be added or corrected.<br /> - Finished = The entry has all the necessary information, but it hasn't been inspected by a trusted user yet.<br /> - Approved = The entry has been inspected and approved by a trusted user. Approved entries can only be edited by trusted users. - - - This entry is locked, meaning that only moderators are allowed to edit it. - - - Choose the language for this name. "Original" is the name in original language that isn't English, for example Japanese. If the original language is English, do not input a name in the "Original" language. - - - This page revision has been hidden. - - - """; + public static string GetTextWithNewline() => + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + This entry has been deleted. It is still temporarily accessible, but won't show up in any of the listings. + + + This entry was merged to + + + Draft = entry is missing crucial information. This status indicates that you're requesting additional information to be added or corrected.<br /> + Finished = The entry has all the necessary information, but it hasn't been inspected by a trusted user yet.<br /> + Approved = The entry has been inspected and approved by a trusted user. Approved entries can only be edited by trusted users. + + + + This entry is locked, meaning that only moderators are allowed to edit it. + + + Choose the language for this name. "Original" is the name in original language that isn't English, for example Japanese. If the original language is English, do not input a name in the "Original" language. + + + This page revision has been hidden. + + + """; } diff --git a/Aigamo.ResXGenerator.Tests/GeneratorLocalizerTests.cs b/Aigamo.ResXGenerator.Tests/GeneratorLocalizerTests.cs index 09c712f..21266c8 100644 --- a/Aigamo.ResXGenerator.Tests/GeneratorLocalizerTests.cs +++ b/Aigamo.ResXGenerator.Tests/GeneratorLocalizerTests.cs @@ -9,286 +9,291 @@ namespace Aigamo.ResXGenerator.Tests; public class GeneratorLocalizerTests { - private static void Generate( - IResXGenerator generator, - bool publicClass = false, - bool nullForgivingOperators = false - ) - { - var expected = $$""" - // ------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - // ------------------------------------------------------------------------------ - {{nullForgivingOperators.InterpolateCondition("#nullable disable", "#nullable enable")}} - using Microsoft.Extensions.Localization; - using System.Text; + private static void Generate( + IResXGenerator generator, + bool publicClass = false, + bool nullForgivingOperators = false + ) + { + var expected = + $$""" + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + {{nullForgivingOperators.InterpolateCondition("#nullable disable", "#nullable enable")}} + using Microsoft.Extensions.Localization; + using System.Text; - namespace VocaDb.Web.App_GlobalResources; + namespace VocaDb.Web.App_GlobalResources; - public interface IActivityEntrySortRuleNames - { - /// - /// Looks up a localized string similar to Oldest. - /// - string CreateDate {get;} - /// - /// Looks up a localized string similar to Newest. - /// - string CreateDateDescending {get;} - } + public interface IActivityEntrySortRuleNames + { + /// + /// Looks up a localized string similar to Oldest. + /// + string CreateDate {get;} + /// + /// Looks up a localized string similar to Newest. + /// + string CreateDateDescending {get;} + } - {{publicClass.InterpolateCondition("public", "internal")}} class ActivityEntrySortRuleNames(IStringLocalizer stringLocalizer) : IActivityEntrySortRuleNames - { - public string CreateDate => stringLocalizer["CreateDate"]; - public string CreateDateDescending => stringLocalizer["CreateDateDescending"]; - } - """; + {{publicClass.InterpolateCondition("public", "internal")}} class ActivityEntrySortRuleNames(IStringLocalizer stringLocalizer) : IActivityEntrySortRuleNames + { + public string CreateDate => stringLocalizer["CreateDate"]; + public string CreateDateDescending => stringLocalizer["CreateDateDescending"]; + } + """; - var result = generator.Generate( - options: new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", - GenerationType = GenerationType.StringLocalizer, - ClassName = "ActivityEntrySortRuleNames", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetText()), Guid.NewGuid()), - subFiles: [] - ), - PublicClass = publicClass, - NullForgivingOperators = nullForgivingOperators - } - ); - result.ErrorsAndWarnings.Should().BeNullOrEmpty(); - result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", + GenerationType = GenerationType.StringLocalizer, + ClassName = "ActivityEntrySortRuleNames", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetText()), Guid.NewGuid()), + subFiles: [] + ), + PublicClass = publicClass, + NullForgivingOperators = nullForgivingOperators + } + ); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } - [Fact] - public void Generate_Localizer_Public() - { - var generator = new LocalizerGenerator(); - Generate(generator); - Generate(generator, publicClass: true); - } + [Fact] + public void Generate_Localizer_Public() + { + var generator = new LocalizerGenerator(); + Generate(generator); + Generate(generator, publicClass: true); + } - [Fact] - public void Generate_Localizer_NullForgivingOperators() - { - var generator = new LocalizerGenerator(); - Generate(generator); - Generate(generator, nullForgivingOperators: true); - } + [Fact] + public void Generate_Localizer_NullForgivingOperators() + { + var generator = new LocalizerGenerator(); + Generate(generator); + Generate(generator, nullForgivingOperators: true); + } - [Fact] - public void Generate_Localizer_NewLine() - { - const string expected = """ - // ------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - // ------------------------------------------------------------------------------ - #nullable enable - using Microsoft.Extensions.Localization; - using System.Text; - - namespace VocaDb.Web.App_GlobalResources; + [Fact] + public void Generate_Localizer_NewLine() + { + const string expected = + """ + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + using Microsoft.Extensions.Localization; + using System.Text; - public interface ICommonMessages - { - /// - /// Looks up a localized string similar to This entry has been deleted. It is still temporarily accessible, but won't show up in any of the listings.. - /// - string EntryDeleted {get;} - /// - /// Looks up a localized string similar to This entry was merged to. - /// - string EntryMergedTo {get;} - /// - /// Looks up a localized string similar to Draft = entry is missing crucial information. This status indicates that you're requesting additional information to be added or corrected.<br /> - /// Finished = The entry has all the necessary information, but it hasn't been inspected by a trusted user yet.<br /> - /// Approved = The entry has been inspected and approved by a trusted user. Approved entries can only be edited by trusted users.. - /// - string EntryStatusExplanation {get;} - /// - /// Looks up a localized string similar to This entry is locked, meaning that only moderators are allowed to edit it.. - /// - string Locked {get;} - /// - /// Looks up a localized string similar to Choose the language for this name. "Original" is the name in original language that isn't English, for example Japanese. If the original language is English, do not input a name in the "Original" language.. - /// - string NameLanguageHelp {get;} - /// - /// Looks up a localized string similar to This page revision has been hidden.. - /// - string RevisionHidden {get;} - } - - internal class CommonMessages(IStringLocalizer stringLocalizer) : ICommonMessages - { - public string EntryDeleted => stringLocalizer["EntryDeleted"]; - public string EntryMergedTo => stringLocalizer["EntryMergedTo"]; - public string EntryStatusExplanation => stringLocalizer["EntryStatusExplanation"]; - public string Locked => stringLocalizer["Locked"]; - public string NameLanguageHelp => stringLocalizer["NameLanguageHelp"]; - public string RevisionHidden => stringLocalizer["RevisionHidden"]; - } - """; - var generator = new LocalizerGenerator(); - var result = generator.Generate( - options: new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - GenerationType = GenerationType.StringLocalizer, - ClassName = "CommonMessages", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetTextWithNewline()), Guid.NewGuid()), - subFiles: [] - ) - } - ); - result.ErrorsAndWarnings.Should().BeNullOrEmpty(); - result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } + namespace VocaDb.Web.App_GlobalResources; - [Fact] - public void Generate_Localizer_Name_DuplicateDataGivesWarning() - { - var generator = new LocalizerGenerator(); - var result = generator.Generate( - options: new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetDaTextWithDuplicates()), Guid.NewGuid()), - subFiles: [] - ), - GenerationType = GenerationType.StringLocalizer, - ClassName = "CommonMessages" - } - ); - var errs = result.ErrorsAndWarnings.ToList(); - errs.Should().NotBeNull(); - errs.Should().HaveCount(1); - errs[0].Id.Should().Be(Analyser.DuplicateWarning.Id); - errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); - errs[0].GetMessage().Should().Contain("DupKey"); - errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(5); - } + public interface ICommonMessages + { + /// + /// Looks up a localized string similar to This entry has been deleted. It is still temporarily accessible, but won't show up in any of the listings.. + /// + string EntryDeleted {get;} + /// + /// Looks up a localized string similar to This entry was merged to. + /// + string EntryMergedTo {get;} + /// + /// Looks up a localized string similar to Draft = entry is missing crucial information. This status indicates that you're requesting additional information to be added or corrected.<br /> + /// Finished = The entry has all the necessary information, but it hasn't been inspected by a trusted user yet.<br /> + /// Approved = The entry has been inspected and approved by a trusted user. Approved entries can only be edited by trusted users.. + /// + string EntryStatusExplanation {get;} + /// + /// Looks up a localized string similar to This entry is locked, meaning that only moderators are allowed to edit it.. + /// + string Locked {get;} + /// + /// Looks up a localized string similar to Choose the language for this name. "Original" is the name in original language that isn't English, for example Japanese. If the original language is English, do not input a name in the "Original" language.. + /// + string NameLanguageHelp {get;} + /// + /// Looks up a localized string similar to This page revision has been hidden.. + /// + string RevisionHidden {get;} + } - [Fact] - public void Generate_Localizer_Name_MemberSameAsFileGivesWarning() - { - const string text = """ - - - - Works. - - - """; + internal class CommonMessages(IStringLocalizer stringLocalizer) : ICommonMessages + { + public string EntryDeleted => stringLocalizer["EntryDeleted"]; + public string EntryMergedTo => stringLocalizer["EntryMergedTo"]; + public string EntryStatusExplanation => stringLocalizer["EntryStatusExplanation"]; + public string Locked => stringLocalizer["Locked"]; + public string NameLanguageHelp => stringLocalizer["NameLanguageHelp"]; + public string RevisionHidden => stringLocalizer["RevisionHidden"]; + } + """; + var generator = new LocalizerGenerator(); + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + GenerationType = GenerationType.StringLocalizer, + ClassName = "CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetTextWithNewline()), Guid.NewGuid()), + subFiles: [] + ) + } + ); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } - var generator = new LocalizerGenerator(); - var results = generator.Generate( - options: new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), - subFiles: [] - ), - GenerationType = GenerationType.StringLocalizer, - ClassName = "CommonMessages" - } - ); - var errs = results.ErrorsAndWarnings.ToList(); - errs.Should().NotBeNull(); - errs.Should().HaveCount(1); - errs[0].Id.Should().Be(Analyser.MemberSameAsClassWarning.Id); - errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); - errs[0].GetMessage().Should().Contain("CommonMessages"); - errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(2); - } + [Fact] + public void Generate_Localizer_Name_DuplicateDataGivesWarning() + { + var generator = new LocalizerGenerator(); + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetDaTextWithDuplicates()), Guid.NewGuid()), + subFiles: [] + ), + GenerationType = GenerationType.StringLocalizer, + ClassName = "CommonMessages" + } + ); + var errs = result.ErrorsAndWarnings.ToList(); + errs.Should().NotBeNull(); + errs.Should().HaveCount(1); + errs[0].Id.Should().Be(Analyser.DuplicateWarning.Id); + errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); + errs[0].GetMessage().Should().Contain("DupKey"); + errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(5); + } - [Fact] - public void Generate_Localizer_StaticAndPartial_Options_Prohibited() - { - const string text = """ - - - - Works. - - - """; + [Fact] + public void Generate_Localizer_Name_MemberSameAsFileGivesWarning() + { + const string text = + """ + + + + Works. + + + """; - var generator = new LocalizerGenerator(); - var results = generator.Generate( - options: new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), - subFiles: [] - ), - GenerationType = GenerationType.StringLocalizer, - ClassName = "CommonMessages", - StaticClass = true, - PartialClass = true - } - ); - var errs = results.ErrorsAndWarnings.ToList(); - errs.Should().NotBeNull(); - errs.Should().HaveCount(2); - errs[0].Id.Should().Be(Analyser.LocalizerStaticError.Id); - errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); - errs[1].Id.Should().Be(Analyser.LocalizerPartialError.Id); - errs[1].Severity.Should().Be(DiagnosticSeverity.Warning); - } + var generator = new LocalizerGenerator(); + var results = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), + subFiles: [] + ), + GenerationType = GenerationType.StringLocalizer, + ClassName = "CommonMessages" + } + ); + var errs = results.ErrorsAndWarnings.ToList(); + errs.Should().NotBeNull(); + errs.Should().HaveCount(1); + errs[0].Id.Should().Be(Analyser.MemberSameAsClassWarning.Id); + errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); + errs[0].GetMessage().Should().Contain("CommonMessages"); + errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(2); + } - [Fact] - public void Generate_Localizer_CustomNamespace_Option_Prohibited() - { - const string text = """ - - - - Works. - - - """; + [Fact] + public void Generate_Localizer_StaticAndPartial_Options_Prohibited() + { + const string text = + """ + + + + Works. + + + """; - var generator = new LocalizerGenerator(); - var results = generator.Generate( - options: new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - CustomToolNamespace = "Resources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), - subFiles: [] - ), - GenerationType = GenerationType.StringLocalizer, - ClassName = "CommonMessages" - } - ); - var errs = results.ErrorsAndWarnings.ToList(); - errs.Should().NotBeNull(); - errs.Should().HaveCount(1); - errs[0].Id.Should().Be(Analyser.LocalizationIncoherentNamespace.Id); - errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); - } + var generator = new LocalizerGenerator(); + var results = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), + subFiles: [] + ), + GenerationType = GenerationType.StringLocalizer, + ClassName = "CommonMessages", + StaticClass = true, + PartialClass = true + } + ); + var errs = results.ErrorsAndWarnings.ToList(); + errs.Should().NotBeNull(); + errs.Should().HaveCount(2); + errs[0].Id.Should().Be(Analyser.LocalizerStaticError.Id); + errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); + errs[1].Id.Should().Be(Analyser.LocalizerPartialError.Id); + errs[1].Severity.Should().Be(DiagnosticSeverity.Warning); + } + + [Fact] + public void Generate_Localizer_CustomNamespace_Option_Prohibited() + { + const string text = + """ + + + + Works. + + + """; + + var generator = new LocalizerGenerator(); + var results = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + CustomToolNamespace = "Resources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), + subFiles: [] + ), + GenerationType = GenerationType.StringLocalizer, + ClassName = "CommonMessages" + } + ); + var errs = results.ErrorsAndWarnings.ToList(); + errs.Should().NotBeNull(); + errs.Should().HaveCount(1); + errs[0].Id.Should().Be(Analyser.LocalizationIncoherentNamespace.Id); + errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); + } } diff --git a/Aigamo.ResXGenerator.Tests/GeneratorTests.cs b/Aigamo.ResXGenerator.Tests/GeneratorTests.cs index 48a58f8..358983a 100644 --- a/Aigamo.ResXGenerator.Tests/GeneratorTests.cs +++ b/Aigamo.ResXGenerator.Tests/GeneratorTests.cs @@ -8,402 +8,419 @@ namespace Aigamo.ResXGenerator.Tests; public class GeneratorTests { - private static void Generate( - IResXGenerator generator, - bool publicClass = true, - bool staticClass = true, - bool partial = false, - bool nullForgivingOperators = false, - bool staticMembers = true - ) - { - var expected = $$""" - // ------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - // ------------------------------------------------------------------------------ - #nullable enable - namespace Resources; - using System.Globalization; - using System.Resources; - - {{(publicClass ? "public" : "internal")}}{{(staticClass ? " static" : string.Empty)}}{{(partial ? " partial" : string.Empty)}} class ActivityEntrySortRuleNames - { - private static ResourceManager? s_resourceManager; - public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", typeof(ActivityEntrySortRuleNames).Assembly); - public{{(staticMembers ? " static" : string.Empty)}} CultureInfo? CultureInfo { get; set; } - - /// - /// Looks up a localized string similar to Oldest. - /// - public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo){{(nullForgivingOperators ? "!" : string.Empty)}}; - - /// - /// Looks up a localized string similar to Newest. - /// - public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo){{(nullForgivingOperators ? "!" : string.Empty)}}; - } - - """; - - var result = generator.Generate( - options: new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", - CustomToolNamespace = "Resources", - ClassName = "ActivityEntrySortRuleNames", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetText()), Guid.NewGuid()), - subFiles: [] - ), - PublicClass = publicClass, - NullForgivingOperators = nullForgivingOperators, - StaticClass = staticClass, - PartialClass = partial, - StaticMembers = staticMembers - } - ); - result.ErrorsAndWarnings.Should().BeNullOrEmpty(); - result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } - - private static void GenerateInner( - IResXGenerator generator, - bool publicClass = true, - bool staticClass = false, - bool partial = false, - bool nullForgivingOperators = false, - bool staticMembers = true, - string innerClassName = "inner", - InnerClassVisibility innerClassVisibility = InnerClassVisibility.SameAsOuter, - string innerClassInstanceName = "" - ) - { - var expected = $$""" - // ------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - // ------------------------------------------------------------------------------ - #nullable enable - namespace Resources; - using System.Globalization; - using System.Resources; - - {{(publicClass ? "public" : "internal")}}{{(partial ? " partial" : string.Empty)}}{{(staticClass ? " static" : string.Empty)}} class ActivityEntrySortRuleNames - {{{(string.IsNullOrEmpty(innerClassInstanceName) ? string.Empty : $"\n public {innerClassName} {innerClassInstanceName} {{ get; }} = new();\n")}} - {{(publicClass ? "public" : "internal")}}{{(partial ? " partial" : string.Empty)}}{{(staticClass ? " static" : string.Empty)}} class {{innerClassName}} - { - private static ResourceManager? s_resourceManager; - public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", typeof({{innerClassName}}).Assembly); - public{{(staticMembers ? " static" : string.Empty)}} CultureInfo? CultureInfo { get; set; } - - /// - /// Looks up a localized string similar to Oldest. - /// - public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo){{(nullForgivingOperators ? "!" : string.Empty)}}; - - /// - /// Looks up a localized string similar to Newest. - /// - public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo){{(nullForgivingOperators ? "!" : string.Empty)}}; - } - } - - """; - - var result = generator.Generate( - options: new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", - CustomToolNamespace = "Resources", - ClassName = "ActivityEntrySortRuleNames", - PublicClass = publicClass, - NullForgivingOperators = nullForgivingOperators, - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetText()), Guid.NewGuid()), - subFiles: [] - ), - StaticClass = staticClass, - PartialClass = partial, - StaticMembers = staticMembers, - InnerClassName = innerClassName, - InnerClassVisibility = innerClassVisibility, - InnerClassInstanceName = innerClassInstanceName - } - ); - result.ErrorsAndWarnings.Should().BeNullOrEmpty(); - result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } - - [Fact] - public void Generate_StringBuilder_Public() - { - var generator = new ResourceManagerGenerator(); - Generate(generator); - Generate(generator, nullForgivingOperators: true); - } - - [Fact] - public void Generate_StringBuilder_NonStatic() - { - var generator = new ResourceManagerGenerator(); - Generate(generator, staticClass: false); - Generate(generator, staticClass: false, nullForgivingOperators: true); - } - - [Fact] - public void Generate_StringBuilder_Internal() - { - var generator = new ResourceManagerGenerator(); - Generate(generator, false); - Generate(generator, false, nullForgivingOperators: true); - } - - [Fact] - public void Generate_StringBuilder_Partial() - { - var generator = new ResourceManagerGenerator(); - Generate(generator, partial: true); - Generate(generator, partial: true, nullForgivingOperators: true); - } - - [Fact] - public void Generate_StringBuilder_NonStaticMembers() - { - var generator = new ResourceManagerGenerator(); - Generate(generator, staticMembers: false); - Generate(generator, staticMembers: false, nullForgivingOperators: true); - } - - [Fact] - public void Generate_StringBuilder_Inner() - { - var generator = new ResourceManagerGenerator(); - GenerateInner(generator); - } - - [Fact] - public void Generate_StringBuilder_InnerInstance() - { - var generator = new ResourceManagerGenerator(); - GenerateInner(generator, innerClassInstanceName: "Resources", staticMembers: false); - } - - [Fact] - public void Generate_StringBuilder_NewLine() - { - const string expected = """ - // ------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - // ------------------------------------------------------------------------------ - #nullable enable - namespace VocaDb.Web.App_GlobalResources; - using System.Globalization; - using System.Resources; - - public static class CommonMessages - { - private static ResourceManager? s_resourceManager; - public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.CommonMessages", typeof(CommonMessages).Assembly); - public static CultureInfo? CultureInfo { get; set; } - - /// - /// Looks up a localized string similar to This entry has been deleted. It is still temporarily accessible, but won't show up in any of the listings.. - /// - public static string? EntryDeleted => ResourceManager.GetString(nameof(EntryDeleted), CultureInfo); - - /// - /// Looks up a localized string similar to This entry was merged to. - /// - public static string? EntryMergedTo => ResourceManager.GetString(nameof(EntryMergedTo), CultureInfo); - - /// - /// Looks up a localized string similar to Draft = entry is missing crucial information. This status indicates that you're requesting additional information to be added or corrected.<br /> - /// Finished = The entry has all the necessary information, but it hasn't been inspected by a trusted user yet.<br /> - /// Approved = The entry has been inspected and approved by a trusted user. Approved entries can only be edited by trusted users.. - /// - public static string? EntryStatusExplanation => ResourceManager.GetString(nameof(EntryStatusExplanation), CultureInfo); - - /// - /// Looks up a localized string similar to This entry is locked, meaning that only moderators are allowed to edit it.. - /// - public static string? Locked => ResourceManager.GetString(nameof(Locked), CultureInfo); - - /// - /// Looks up a localized string similar to Choose the language for this name. "Original" is the name in original language that isn't English, for example Japanese. If the original language is English, do not input a name in the "Original" language.. - /// - public static string? NameLanguageHelp => ResourceManager.GetString(nameof(NameLanguageHelp), CultureInfo); - - /// - /// Looks up a localized string similar to This page revision has been hidden.. - /// - public static string? RevisionHidden => ResourceManager.GetString(nameof(RevisionHidden), CultureInfo); - } - - """; - var generator = new ResourceManagerGenerator(); - var result = generator.Generate( - options: new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - CustomToolNamespace = null, - ClassName = "CommonMessages", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetTextWithNewline()), Guid.NewGuid()), - subFiles: [] - ), - PublicClass = true, - NullForgivingOperators = false, - StaticClass = true, - StaticMembers = true - } - ); - result.ErrorsAndWarnings.Should().BeNullOrEmpty(); - result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } - - [Fact] - public void Generate_StringBuilder_Name_PartialXmlWorks() - { - const string text = """ - - - - Works. - - - """; - - const string expected = """ - // ------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - // ------------------------------------------------------------------------------ - #nullable enable - namespace VocaDb.Web.App_GlobalResources; - using System.Globalization; - using System.Resources; - - public static class CommonMessages - { - private static ResourceManager? s_resourceManager; - public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.CommonMessages", typeof(CommonMessages).Assembly); - public static CultureInfo? CultureInfo { get; set; } - - /// - /// Looks up a localized string similar to Works.. - /// - public static string? Works => ResourceManager.GetString(nameof(Works), CultureInfo); - } - - """; - - var generator = new ResourceManagerGenerator(); - var result = generator.Generate( - options: new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - CustomToolNamespace = null, - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), - subFiles: [] - ), - ClassName = "CommonMessages", - PublicClass = true, - NullForgivingOperators = false, - StaticClass = true, - StaticMembers = true - } - ); - result.ErrorsAndWarnings.Should().BeNullOrEmpty(); - result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } - - [Fact] - public void Generate_StringBuilder_Name_DuplicateDataGivesWarning() - { - var generator = new ResourceManagerGenerator(); - var result = generator.Generate( - options: new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetDaTextWithDuplicates()), Guid.NewGuid()), - subFiles: [] - ), - CustomToolNamespace = null, - ClassName = "CommonMessages", - PublicClass = true, - NullForgivingOperators = false, - StaticClass = true - } - ); - var errs = result.ErrorsAndWarnings.ToList(); - errs.Should().NotBeNull(); - errs.Should().HaveCount(1); - errs[0].Id.Should().Be(Analyser.DuplicateWarning.Id); - errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); - errs[0].GetMessage().Should().Contain("DupKey"); - errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(5); - } - - [Fact] - public void Generate_StringBuilder_Name_MemberSameAsFileGivesWarning() - { - const string text = """ - - - - Works. - - - """; - - var generator = new ResourceManagerGenerator(); - var results = generator.Generate( - options: new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), - subFiles: [] - ), - CustomToolNamespace = null, - ClassName = "CommonMessages", - PublicClass = true, - NullForgivingOperators = false, - StaticClass = true - } - ); - var errs = results.ErrorsAndWarnings.ToList(); - errs.Should().NotBeNull(); - errs.Should().HaveCount(1); - errs[0].Id.Should().Be(Analyser.MemberSameAsClassWarning.Id); - errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); - errs[0].GetMessage().Should().Contain("CommonMessages"); - errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(2); - } + private static void Generate( + IResXGenerator generator, + bool publicClass = true, + bool staticClass = true, + bool partial = false, + bool nullForgivingOperators = false, + bool staticMembers = true + ) + { + var expected = + $$""" + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace Resources; + using System.Globalization; + using System.Resources; + + {{(publicClass ? "public" : "internal")}}{{(staticClass ? " static" : string.Empty)}}{{(partial ? " partial" : string.Empty)}} class ActivityEntrySortRuleNames + { + private static ResourceManager? s_resourceManager; + public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", typeof(ActivityEntrySortRuleNames).Assembly); + public{{(staticMembers ? " static" : string.Empty)}} CultureInfo? CultureInfo { get; set; } + + /// + /// Looks up a localized string similar to Oldest. + /// + public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo){{(nullForgivingOperators ? "!" : string.Empty)}}; + + /// + /// Looks up a localized string similar to Newest. + /// + public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo){{(nullForgivingOperators ? "!" : string.Empty)}}; + } + + """; + + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", + CustomToolNamespace = "Resources", + ClassName = "ActivityEntrySortRuleNames", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetText()), Guid.NewGuid()), + subFiles: [] + ), + PublicClass = publicClass, + NullForgivingOperators = nullForgivingOperators, + StaticClass = staticClass, + PartialClass = partial, + StaticMembers = staticMembers + } + ); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } + + private static void GenerateInner( + IResXGenerator generator, + bool publicClass = true, + bool staticClass = false, + bool partial = false, + bool nullForgivingOperators = false, + bool staticMembers = true, + string innerClassName = "inner", + InnerClassVisibility innerClassVisibility = InnerClassVisibility.SameAsOuter, + string innerClassInstanceName = "" + ) + { + var expected = + $$""" + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace Resources; + using System.Globalization; + using System.Resources; + + {{(publicClass ? "public" : "internal")}}{{(partial ? " partial" : string.Empty)}}{{(staticClass ? " static" : string.Empty)}} class ActivityEntrySortRuleNames + { + + """; + + if (!string.IsNullOrEmpty(innerClassInstanceName)) + expected += $$""" + public {{innerClassName}} {{innerClassInstanceName}} { get; } = new(); + + + """; + expected += + $$""" + {{(publicClass ? "public" : "internal")}}{{(partial ? " partial" : string.Empty)}}{{(staticClass ? " static" : string.Empty)}} class {{innerClassName}} + { + private static ResourceManager? s_resourceManager; + public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", typeof({{innerClassName}}).Assembly); + public{{(staticMembers ? " static" : string.Empty)}} CultureInfo? CultureInfo { get; set; } + + /// + /// Looks up a localized string similar to Oldest. + /// + public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo){{(nullForgivingOperators ? "!" : string.Empty)}}; + + /// + /// Looks up a localized string similar to Newest. + /// + public{{(staticMembers ? " static" : string.Empty)}} string{{(nullForgivingOperators ? string.Empty : "?")}} CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo){{(nullForgivingOperators ? "!" : string.Empty)}}; + } + } + + """; + + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", + CustomToolNamespace = "Resources", + ClassName = "ActivityEntrySortRuleNames", + PublicClass = publicClass, + NullForgivingOperators = nullForgivingOperators, + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetText()), Guid.NewGuid()), + subFiles: [] + ), + StaticClass = staticClass, + PartialClass = partial, + StaticMembers = staticMembers, + InnerClassName = innerClassName, + InnerClassVisibility = innerClassVisibility, + InnerClassInstanceName = innerClassInstanceName + } + ); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } + + [Fact] + public void Generate_StringBuilder_Public() + { + var generator = new ResourceManagerGenerator(); + Generate(generator); + Generate(generator, nullForgivingOperators: true); + } + + [Fact] + public void Generate_StringBuilder_NonStatic() + { + var generator = new ResourceManagerGenerator(); + Generate(generator, staticClass: false); + Generate(generator, staticClass: false, nullForgivingOperators: true); + } + + [Fact] + public void Generate_StringBuilder_Internal() + { + var generator = new ResourceManagerGenerator(); + Generate(generator, false); + Generate(generator, false, nullForgivingOperators: true); + } + + [Fact] + public void Generate_StringBuilder_Partial() + { + var generator = new ResourceManagerGenerator(); + Generate(generator, partial: true); + Generate(generator, partial: true, nullForgivingOperators: true); + } + + [Fact] + public void Generate_StringBuilder_NonStaticMembers() + { + var generator = new ResourceManagerGenerator(); + Generate(generator, staticMembers: false); + Generate(generator, staticMembers: false, nullForgivingOperators: true); + } + + [Fact] + public void Generate_StringBuilder_Inner() + { + var generator = new ResourceManagerGenerator(); + GenerateInner(generator); + } + + [Fact] + public void Generate_StringBuilder_InnerInstance() + { + var generator = new ResourceManagerGenerator(); + GenerateInner(generator, innerClassInstanceName: "Resources", staticMembers: false); + } + + [Fact] + public void Generate_StringBuilder_NewLine() + { + const string expected = + """ + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace VocaDb.Web.App_GlobalResources; + using System.Globalization; + using System.Resources; + + public static class CommonMessages + { + private static ResourceManager? s_resourceManager; + public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.CommonMessages", typeof(CommonMessages).Assembly); + public static CultureInfo? CultureInfo { get; set; } + + /// + /// Looks up a localized string similar to This entry has been deleted. It is still temporarily accessible, but won't show up in any of the listings.. + /// + public static string? EntryDeleted => ResourceManager.GetString(nameof(EntryDeleted), CultureInfo); + + /// + /// Looks up a localized string similar to This entry was merged to. + /// + public static string? EntryMergedTo => ResourceManager.GetString(nameof(EntryMergedTo), CultureInfo); + + /// + /// Looks up a localized string similar to Draft = entry is missing crucial information. This status indicates that you're requesting additional information to be added or corrected.<br /> + /// Finished = The entry has all the necessary information, but it hasn't been inspected by a trusted user yet.<br /> + /// Approved = The entry has been inspected and approved by a trusted user. Approved entries can only be edited by trusted users.. + /// + public static string? EntryStatusExplanation => ResourceManager.GetString(nameof(EntryStatusExplanation), CultureInfo); + + /// + /// Looks up a localized string similar to This entry is locked, meaning that only moderators are allowed to edit it.. + /// + public static string? Locked => ResourceManager.GetString(nameof(Locked), CultureInfo); + + /// + /// Looks up a localized string similar to Choose the language for this name. "Original" is the name in original language that isn't English, for example Japanese. If the original language is English, do not input a name in the "Original" language.. + /// + public static string? NameLanguageHelp => ResourceManager.GetString(nameof(NameLanguageHelp), CultureInfo); + + /// + /// Looks up a localized string similar to This page revision has been hidden.. + /// + public static string? RevisionHidden => ResourceManager.GetString(nameof(RevisionHidden), CultureInfo); + } + + """; + var generator = new ResourceManagerGenerator(); + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + CustomToolNamespace = null, + ClassName = "CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetTextWithNewline()), Guid.NewGuid()), + subFiles: [] + ), + PublicClass = true, + NullForgivingOperators = false, + StaticClass = true, + StaticMembers = true + } + ); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } + + [Fact] + public void Generate_StringBuilder_Name_PartialXmlWorks() + { + const string text = + """ + + + + Works. + + + """; + + const string expected = + """ + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace VocaDb.Web.App_GlobalResources; + using System.Globalization; + using System.Resources; + + public static class CommonMessages + { + private static ResourceManager? s_resourceManager; + public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.CommonMessages", typeof(CommonMessages).Assembly); + public static CultureInfo? CultureInfo { get; set; } + + /// + /// Looks up a localized string similar to Works.. + /// + public static string? Works => ResourceManager.GetString(nameof(Works), CultureInfo); + } + + """; + + var generator = new ResourceManagerGenerator(); + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + CustomToolNamespace = null, + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), + subFiles: [] + ), + ClassName = "CommonMessages", + PublicClass = true, + NullForgivingOperators = false, + StaticClass = true, + StaticMembers = true + } + ); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } + + [Fact] + public void Generate_StringBuilder_Name_DuplicateDataGivesWarning() + { + var generator = new ResourceManagerGenerator(); + var result = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetDaTextWithDuplicates()), Guid.NewGuid()), + subFiles: [] + ), + CustomToolNamespace = null, + ClassName = "CommonMessages", + PublicClass = true, + NullForgivingOperators = false, + StaticClass = true + } + ); + var errs = result.ErrorsAndWarnings.ToList(); + errs.Should().NotBeNull(); + errs.Should().HaveCount(1); + errs[0].Id.Should().Be(Analyser.DuplicateWarning.Id); + errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); + errs[0].GetMessage().Should().Contain("DupKey"); + errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(5); + } + + [Fact] + public void Generate_StringBuilder_Name_MemberSameAsFileGivesWarning() + { + const string text = + """ + + + + Works. + + + """; + + var generator = new ResourceManagerGenerator(); + var results = generator.Generate( + options: new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), + subFiles: [] + ), + CustomToolNamespace = null, + ClassName = "CommonMessages", + PublicClass = true, + NullForgivingOperators = false, + StaticClass = true + } + ); + var errs = results.ErrorsAndWarnings.ToList(); + errs.Should().NotBeNull(); + errs.Should().HaveCount(1); + errs[0].Id.Should().Be(Analyser.MemberSameAsClassWarning.Id); + errs[0].Severity.Should().Be(DiagnosticSeverity.Warning); + errs[0].GetMessage().Should().Contain("CommonMessages"); + errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(2); + } } diff --git a/Aigamo.ResXGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs b/Aigamo.ResXGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs index 1c89053..48b88ef 100644 --- a/Aigamo.ResXGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs +++ b/Aigamo.ResXGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs @@ -8,212 +8,215 @@ namespace Aigamo.ResXGenerator.Tests.GithubIssues.Issue3; public class GeneratorTests { - [Fact] - public void Generate_StringBuilder_Name_NotValidIdentifier() - { - const string text = """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - String '{0}' is not a valid identifier. - - - """; + [Fact] + public void Generate_StringBuilder_Name_NotValidIdentifier() + { + const string text = + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + String '{0}' is not a valid identifier. + + + """; - const string expected = """ - // ------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - // ------------------------------------------------------------------------------ - #nullable enable - namespace VocaDb.Web.App_GlobalResources; - using System.Globalization; - using System.Resources; + const string expected = + """ + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace VocaDb.Web.App_GlobalResources; + using System.Globalization; + using System.Resources; - public static class CommonMessages - { - private static ResourceManager? s_resourceManager; - public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.CommonMessages", typeof(CommonMessages).Assembly); - public static CultureInfo? CultureInfo { get; set; } + public static class CommonMessages + { + private static ResourceManager? s_resourceManager; + public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.CommonMessages", typeof(CommonMessages).Assembly); + public static CultureInfo? CultureInfo { get; set; } - /// - /// Looks up a localized string similar to String '{0}' is not a valid identifier.. - /// - public static string? Invalid_identifier__0_ => ResourceManager.GetString("Invalid identifier {0}", CultureInfo); - } + /// + /// Looks up a localized string similar to String '{0}' is not a valid identifier.. + /// + public static string? Invalid_identifier__0_ => ResourceManager.GetString("Invalid identifier {0}", CultureInfo); + } - """; + """; - var generator = new ResourceManagerGenerator(); - var source = generator.Generate( - new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", - CustomToolNamespace = null, - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), - subFiles: [] - ), - ClassName = "CommonMessages", - PublicClass = true, - NullForgivingOperators = false, - StaticClass = true, - StaticMembers = true - } - ); - source.ErrorsAndWarnings.Should().BeNullOrEmpty(); - source.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } + var generator = new ResourceManagerGenerator(); + var source = generator.Generate( + new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages", + CustomToolNamespace = null, + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), + subFiles: [] + ), + ClassName = "CommonMessages", + PublicClass = true, + NullForgivingOperators = false, + StaticClass = true, + StaticMembers = true + } + ); + source.ErrorsAndWarnings.Should().BeNullOrEmpty(); + source.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } - [Fact] - public void Generate_StringBuilder_Value_InvalidCharacter() - { - const string text = $""" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Old{"\0"}est - - - Newest - - - """; - var generator = new ResourceManagerGenerator(); - var options = new GenFileOptions - { - LocalNamespace = "VocaDb.Web.App_GlobalResources", - CustomToolNamespace = "Resources", - ClassName = "ActivityEntrySortRuleNames", - GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), - subFiles: [] - ), - PublicClass = true, - NullForgivingOperators = false, - StaticClass = true - }; - generator.Invoking(subject => subject.Generate(options)).Should().Throw(); - } + [Fact] + public void Generate_StringBuilder_Value_InvalidCharacter() + { + const string text = + $""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Old{"\0"}est + + + Newest + + + """; + var generator = new ResourceManagerGenerator(); + var options = new GenFileOptions + { + LocalNamespace = "VocaDb.Web.App_GlobalResources", + CustomToolNamespace = "Resources", + ClassName = "ActivityEntrySortRuleNames", + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), + subFiles: [] + ), + PublicClass = true, + NullForgivingOperators = false, + StaticClass = true + }; + generator.Invoking(subject => subject.Generate(options)).Should().Throw(); + } } diff --git a/Aigamo.ResXGenerator.Tests/GlobalRegisterTests.cs b/Aigamo.ResXGenerator.Tests/GlobalRegisterTests.cs index 9fd1ada..3d38e57 100644 --- a/Aigamo.ResXGenerator.Tests/GlobalRegisterTests.cs +++ b/Aigamo.ResXGenerator.Tests/GlobalRegisterTests.cs @@ -9,82 +9,82 @@ namespace Aigamo.ResXGenerator.Tests; public class GlobalRegisterTests { - private static void Generate( - IGlobalRegisterGenerator generator, - bool nullForgivingOperators = false) - { - string expected = $$""" - // ------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - // ------------------------------------------------------------------------------ - {{nullForgivingOperators.InterpolateCondition("#nullable disable", "#nullable enable")}} - using Microsoft.Extensions.DependencyInjection; - using VocaDb.Web.App_GlobalResources; - using VocaDb.Web.App_LocalResources; - - namespace ResXGenerator.Registration; - - public static class ResXGeneratorRegistrationExtension - { - public static IServiceCollection UsingResXGenerator(this IServiceCollection services) - { - services.UsingVocaDbWebAppGlobalResourcesResX(); - services.UsingVocaDbWebAppLocalResourcesResX(); - - return services; - } - } - """; + private static void Generate( + IGlobalRegisterGenerator generator, + bool nullForgivingOperators = false) + { + string expected = $$""" + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + {{nullForgivingOperators.InterpolateCondition("#nullable disable", "#nullable enable")}} + using Microsoft.Extensions.DependencyInjection; + using VocaDb.Web.App_GlobalResources; + using VocaDb.Web.App_LocalResources; + + namespace ResXGenerator.Registration; + + public static class ResXGeneratorRegistrationExtension + { + public static IServiceCollection UsingResXGenerator(this IServiceCollection services) + { + services.UsingVocaDbWebAppGlobalResourcesResX(); + services.UsingVocaDbWebAppLocalResourcesResX(); + + return services; + } + } + """; - var result = generator.Generate( - options: - [ - new GenFilesNamespace( - "VocaDb.Web.App_GlobalResources", - [ - new GenFileOptions - { - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", - NullForgivingOperators = nullForgivingOperators, - } - ]), - new GenFilesNamespace( - "VocaDb.Web.App_LocalResources", - [ - new GenFileOptions - { - EmbeddedFilename = "VocaDb.Web.App_LocalResources.LocalViewResources", - NullForgivingOperators = nullForgivingOperators, - }, - new GenFileOptions - { - EmbeddedFilename = "VocaDb.Web.App_LocalResources.SpecificResources", - NullForgivingOperators = nullForgivingOperators, - } - ]) - ] - ); + var result = generator.Generate( + options: + [ + new GenFilesNamespace( + "VocaDb.Web.App_GlobalResources", + [ + new GenFileOptions + { + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", + NullForgivingOperators = nullForgivingOperators, + } + ]), + new GenFilesNamespace( + "VocaDb.Web.App_LocalResources", + [ + new GenFileOptions + { + EmbeddedFilename = "VocaDb.Web.App_LocalResources.LocalViewResources", + NullForgivingOperators = nullForgivingOperators, + }, + new GenFileOptions + { + EmbeddedFilename = "VocaDb.Web.App_LocalResources.SpecificResources", + NullForgivingOperators = nullForgivingOperators, + } + ]) + ] + ); - result.ErrorsAndWarnings.Should().BeNullOrEmpty(); - result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } - [Fact] - public void CanRegisterLocalClass() - { - var generator = new LocalizerGlobalRegisterGenerator(); - Generate(generator); - } + [Fact] + public void CanRegisterLocalClass() + { + var generator = new LocalizerGlobalRegisterGenerator(); + Generate(generator); + } - [Fact] - public void CanRegisterLocalClassWithNullForgivingOperators() - { - var generator = new LocalizerGlobalRegisterGenerator(); - Generate(generator, true); - } + [Fact] + public void CanRegisterLocalClassWithNullForgivingOperators() + { + var generator = new LocalizerGlobalRegisterGenerator(); + Generate(generator, true); + } } diff --git a/Aigamo.ResXGenerator.Tests/HelperGeneratorTests.cs b/Aigamo.ResXGenerator.Tests/HelperGeneratorTests.cs index 7181ca5..1dc87fc 100644 --- a/Aigamo.ResXGenerator.Tests/HelperGeneratorTests.cs +++ b/Aigamo.ResXGenerator.Tests/HelperGeneratorTests.cs @@ -21,28 +21,29 @@ public void CanGenerateCombo() ), CancellationToken.None ); - const string expected = """ - // ------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - // ------------------------------------------------------------------------------ - #nullable enable - namespace Aigamo.ResXGenerator; - internal static partial class Helpers - { - public static string GetString_1030_6(string fallback, string da_DK, string da) => System.Globalization.CultureInfo.CurrentUICulture.LCID switch - { - 1030 => da_DK, - 6 => da, - _ => fallback - }; - } + const string expected = + """ + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace Aigamo.ResXGenerator; + internal static partial class Helpers + { + public static string GetString_1030_6(string fallback, string da_DK, string da) => System.Globalization.CultureInfo.CurrentUICulture.LCID switch + { + 1030 => da_DK, + 6 => da, + _ => fallback + }; + } - """; + """; result.ErrorsAndWarnings.Should().BeNullOrEmpty(); result.FileName.Should().Be("Aigamo.ResXGenerator.1030_6.g.cs"); result.SourceCode.Should().Be(expected); @@ -51,26 +52,27 @@ internal static partial class Helpers public void CanGenerateEmptyCombo() { var result = new ComboGenerator().Generate(new CultureInfoCombo(), CancellationToken.None); - const string expected = """ - // ------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - // ------------------------------------------------------------------------------ - #nullable enable - namespace Aigamo.ResXGenerator; - internal static partial class Helpers - { - public static string GetString_(string fallback) => System.Globalization.CultureInfo.CurrentUICulture.LCID switch - { - _ => fallback - }; - } + const string expected = + """ + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + #nullable enable + namespace Aigamo.ResXGenerator; + internal static partial class Helpers + { + public static string GetString_(string fallback) => System.Globalization.CultureInfo.CurrentUICulture.LCID switch + { + _ => fallback + }; + } - """; + """; result.ErrorsAndWarnings.Should().BeNullOrEmpty(); result.FileName.Should().Be("Aigamo.ResXGenerator..g.cs"); result.SourceCode.Should().Be(expected); diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.da-dk.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.da-dk.resx index b37f7ba..0de448d 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.da-dk.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.da-dk.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.da.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.da.resx index a9eb06d..7d7fd5a 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.da.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.da.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.en-us.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.en-us.resx index 67239d7..63413e6 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.en-us.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.en-us.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.resx index bd74026..6d5b07a 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test1.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.da-dk.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.da-dk.resx index b37f7ba..0de448d 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.da-dk.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.da-dk.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.da.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.da.resx index a9eb06d..7d7fd5a 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.da.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.da.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.en-us.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.en-us.resx index 67239d7..63413e6 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.en-us.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.en-us.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.resx index bd74026..6d5b07a 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.da-dk.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.da-dk.resx index b37f7ba..0de448d 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.da-dk.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.da-dk.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.da.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.da.resx index a9eb06d..7d7fd5a 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.da.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.da.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.en-us.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.en-us.resx index 67239d7..63413e6 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.en-us.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.en-us.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.resx index bd74026..6d5b07a 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test3.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da-dk.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da-dk.resx index b37f7ba..0de448d 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da-dk.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da-dk.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da.resx index a9eb06d..7d7fd5a 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.da.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.resx index 67239d7..63413e6 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.da-us.resx @@ -44,17 +44,17 @@ read any of the formats listed below. mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.resx index ec0a18e..ceb62be 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.resx +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test4.resx @@ -1,126 +1,126 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Oldest + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Oldest - - Newest + + Newest \ No newline at end of file diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/TestResxFiles.cs b/Aigamo.ResXGenerator.Tests/IntegrationTests/TestResxFiles.cs index d60555c..7d189f3 100644 --- a/Aigamo.ResXGenerator.Tests/IntegrationTests/TestResxFiles.cs +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/TestResxFiles.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using FluentAssertions; using Xunit; @@ -36,10 +36,7 @@ public void TestCodeGenResourceGen() } [Fact] - public void TestSkipFile_DoesNotGenerate() - { + public void TestSkipFile_DoesNotGenerate() => GetType().Assembly.GetTypes().Should() .NotContain(t => t.Name == "Test3"); - } - } diff --git a/Aigamo.ResXGenerator.Tests/LocalizerRegisterTests.cs b/Aigamo.ResXGenerator.Tests/LocalizerRegisterTests.cs index 2372125..71b4009 100644 --- a/Aigamo.ResXGenerator.Tests/LocalizerRegisterTests.cs +++ b/Aigamo.ResXGenerator.Tests/LocalizerRegisterTests.cs @@ -9,69 +9,69 @@ namespace Aigamo.ResXGenerator.Tests; public class LocalizerRegisterTests { - private static void Generate( - ILocalRegisterGenerator generator, - bool nullForgivingOperators = false) - { - string expected = $$""" - // ------------------------------------------------------------------------------ - // - // This code was generated by a tool. - // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // - // ------------------------------------------------------------------------------ - {{nullForgivingOperators.InterpolateCondition("#nullable disable", "#nullable enable")}} - using Microsoft.Extensions.DependencyInjection; + private static void Generate( + ILocalRegisterGenerator generator, + bool nullForgivingOperators = false) + { + string expected = $$""" + // ------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + // ------------------------------------------------------------------------------ + {{nullForgivingOperators.InterpolateCondition("#nullable disable", "#nullable enable")}} + using Microsoft.Extensions.DependencyInjection; - namespace VocaDb.Web.App_GlobalResources; + namespace VocaDb.Web.App_GlobalResources; - public static class VocaDbWebAppGlobalResourcesRegistrationExtensions - { - public static IServiceCollection UsingVocaDbWebAppGlobalResourcesResX(this IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - return services; - } - } - """; + public static class VocaDbWebAppGlobalResourcesRegistrationExtensions + { + public static IServiceCollection UsingVocaDbWebAppGlobalResourcesResX(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + return services; + } + } + """; - var result = generator.Generate( - options: new GenFilesNamespace( - "VocaDb.Web.App_GlobalResources", - [ - new GenFileOptions - { - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", - ClassName = "ActivityEntrySortRuleNames", - NullForgivingOperators = nullForgivingOperators, - }, - new GenFileOptions - { - EmbeddedFilename = "VocaDb.Web.App_GlobalResources.LocalResources", - ClassName = "LocalResources", - NullForgivingOperators = nullForgivingOperators, - } - ] - )); + var result = generator.Generate( + options: new GenFilesNamespace( + "VocaDb.Web.App_GlobalResources", + [ + new GenFileOptions + { + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", + ClassName = "ActivityEntrySortRuleNames", + NullForgivingOperators = nullForgivingOperators, + }, + new GenFileOptions + { + EmbeddedFilename = "VocaDb.Web.App_GlobalResources.LocalResources", + ClassName = "LocalResources", + NullForgivingOperators = nullForgivingOperators, + } + ] + )); - result.ErrorsAndWarnings.Should().BeNullOrEmpty(); - result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); - } + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } - [Fact] - public void CanRegisterLocalClass() - { - var generator = new LocalizerRegisterGenerator(); - Generate(generator); - } + [Fact] + public void CanRegisterLocalClass() + { + var generator = new LocalizerRegisterGenerator(); + Generate(generator); + } - [Fact] - public void CanRegisterLocalClassWithNullForgivingOperators() - { - var generator = new LocalizerRegisterGenerator(); - Generate(generator, true); - } + [Fact] + public void CanRegisterLocalClassWithNullForgivingOperators() + { + var generator = new LocalizerRegisterGenerator(); + Generate(generator, true); + } } diff --git a/Aigamo.ResXGenerator.Tests/Properties/launchSettings.json b/Aigamo.ResXGenerator.Tests/Properties/launchSettings.json index 6d7cf48..14e56a1 100644 --- a/Aigamo.ResXGenerator.Tests/Properties/launchSettings.json +++ b/Aigamo.ResXGenerator.Tests/Properties/launchSettings.json @@ -1,12 +1,12 @@ { - "profiles": { - "Aigamo.ResXGenerator.Tests": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:1103;http://localhost:1119" - } - } + "profiles": { + "Aigamo.ResXGenerator.Tests": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:1103;http://localhost:1119" + } + } } \ No newline at end of file diff --git a/Aigamo.ResXGenerator.Tests/SettingsTests.cs b/Aigamo.ResXGenerator.Tests/SettingsTests.cs index 89c615c..a2f7e43 100644 --- a/Aigamo.ResXGenerator.Tests/SettingsTests.cs +++ b/Aigamo.ResXGenerator.Tests/SettingsTests.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Aigamo.ResXGenerator.Tools; using FluentAssertions; using Microsoft.CodeAnalysis; @@ -9,447 +9,447 @@ namespace Aigamo.ResXGenerator.Tests; public class SettingsTests { - private static readonly GlobalOptions s_globalOptions = GlobalOptions.Select( - provider: new AnalyzerConfigOptionsProviderStub( - globalOptions: new AnalyzerConfigOptionsStub - { - RootNamespace = "namespace1", - MSBuildProjectFullPath = "project1.csproj", - MSBuildProjectName = "project1", - }, - fileOptions: null! - ), - token: default - ); + private static readonly GlobalOptions s_globalOptions = GlobalOptions.Select( + provider: new AnalyzerConfigOptionsProviderStub( + globalOptions: new AnalyzerConfigOptionsStub + { + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", + MSBuildProjectName = "project1", + }, + fileOptions: null! + ), + token: default + ); - [Fact] - public void GlobalDefaults() - { - var globalOptions = s_globalOptions; - globalOptions.ProjectName.Should().Be("project1"); - globalOptions.RootNamespace.Should().Be("namespace1"); - globalOptions.ProjectFullPath.Should().Be("project1.csproj"); - globalOptions.InnerClassName.Should().BeNullOrEmpty(); - globalOptions.ClassNamePostfix.Should().BeNullOrEmpty(); - globalOptions.InnerClassInstanceName.Should().BeNullOrEmpty(); - globalOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated); - globalOptions.NullForgivingOperators.Should().Be(false); - globalOptions.StaticClass.Should().Be(true); - globalOptions.StaticMembers.Should().Be(true); - globalOptions.PublicClass.Should().Be(false); - globalOptions.PartialClass.Should().Be(false); - globalOptions.IsValid.Should().Be(true); - globalOptions.GenerateCode.Should().Be(false); - globalOptions.GenerationType.Should().Be(GenerationType.ResourceManager); - } + [Fact] + public void GlobalDefaults() + { + var globalOptions = s_globalOptions; + globalOptions.ProjectName.Should().Be("project1"); + globalOptions.RootNamespace.Should().Be("namespace1"); + globalOptions.ProjectFullPath.Should().Be("project1.csproj"); + globalOptions.InnerClassName.Should().BeNullOrEmpty(); + globalOptions.ClassNamePostfix.Should().BeNullOrEmpty(); + globalOptions.InnerClassInstanceName.Should().BeNullOrEmpty(); + globalOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated); + globalOptions.NullForgivingOperators.Should().Be(false); + globalOptions.StaticClass.Should().Be(true); + globalOptions.StaticMembers.Should().Be(true); + globalOptions.PublicClass.Should().Be(false); + globalOptions.PartialClass.Should().Be(false); + globalOptions.IsValid.Should().Be(true); + globalOptions.GenerateCode.Should().Be(false); + globalOptions.GenerationType.Should().Be(GenerationType.ResourceManager); + } - [Fact] - public void GlobalSettings_CanReadAll() - { - var globalOptions = GlobalOptions.Select( - provider: new AnalyzerConfigOptionsProviderStub( - globalOptions: new AnalyzerConfigOptionsStub - { - RootNamespace = "namespace1", - MSBuildProjectFullPath = "project1.csproj", - MSBuildProjectName = "project1", - ResXGenerator_InnerClassName = "test1", - ResXGenerator_InnerClassInstanceName = "test2", - ResXGenerator_ClassNamePostfix = "test3", - ResXGenerator_InnerClassVisibility = "public", - ResXGenerator_NullForgivingOperators = "true", - ResXGenerator_StaticClass = "false", - ResXGenerator_StaticMembers = "false", - ResXGenerator_GenerateCode = "true", - ResXGenerator_PublicClass = "true", - ResXGenerator_PartialClass = "true", - ResXGenerator_GenerationType = "ResourceManager" - }, - fileOptions: null! - ), - token: default - ); - globalOptions.RootNamespace.Should().Be("namespace1"); - globalOptions.ProjectFullPath.Should().Be("project1.csproj"); - globalOptions.ProjectName.Should().Be("project1"); - globalOptions.InnerClassName.Should().Be("test1"); - globalOptions.InnerClassInstanceName.Should().Be("test2"); - globalOptions.ClassNamePostfix.Should().Be("test3"); - globalOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public); - globalOptions.NullForgivingOperators.Should().Be(true); - globalOptions.StaticClass.Should().Be(false); - globalOptions.GenerateCode.Should().Be(true); - globalOptions.StaticMembers.Should().Be(false); - globalOptions.PublicClass.Should().Be(true); - globalOptions.PartialClass.Should().Be(true); - globalOptions.IsValid.Should().Be(true); - globalOptions.GenerationType.Should().Be(GenerationType.ResourceManager); - } + [Fact] + public void GlobalSettings_CanReadAll() + { + var globalOptions = GlobalOptions.Select( + provider: new AnalyzerConfigOptionsProviderStub( + globalOptions: new AnalyzerConfigOptionsStub + { + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", + MSBuildProjectName = "project1", + ResXGenerator_InnerClassName = "test1", + ResXGenerator_InnerClassInstanceName = "test2", + ResXGenerator_ClassNamePostfix = "test3", + ResXGenerator_InnerClassVisibility = "public", + ResXGenerator_NullForgivingOperators = "true", + ResXGenerator_StaticClass = "false", + ResXGenerator_StaticMembers = "false", + ResXGenerator_GenerateCode = "true", + ResXGenerator_PublicClass = "true", + ResXGenerator_PartialClass = "true", + ResXGenerator_GenerationType = "ResourceManager" + }, + fileOptions: null! + ), + token: default + ); + globalOptions.RootNamespace.Should().Be("namespace1"); + globalOptions.ProjectFullPath.Should().Be("project1.csproj"); + globalOptions.ProjectName.Should().Be("project1"); + globalOptions.InnerClassName.Should().Be("test1"); + globalOptions.InnerClassInstanceName.Should().Be("test2"); + globalOptions.ClassNamePostfix.Should().Be("test3"); + globalOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public); + globalOptions.NullForgivingOperators.Should().Be(true); + globalOptions.StaticClass.Should().Be(false); + globalOptions.GenerateCode.Should().Be(true); + globalOptions.StaticMembers.Should().Be(false); + globalOptions.PublicClass.Should().Be(true); + globalOptions.PartialClass.Should().Be(true); + globalOptions.IsValid.Should().Be(true); + globalOptions.GenerationType.Should().Be(GenerationType.ResourceManager); + } - [Fact] - public void FileDefaults() - { - var fileOptions = GenFileOptions.Select( - file: new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), - subFiles: [] - ), - options: new AnalyzerConfigOptionsProviderStub( - globalOptions: null!, - fileOptions: new AnalyzerConfigOptionsStub() - ), - globalOptions: s_globalOptions - ); - fileOptions.InnerClassName.Should().BeNullOrEmpty(); - fileOptions.InnerClassInstanceName.Should().BeNullOrEmpty(); - fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated); - fileOptions.NullForgivingOperators.Should().Be(false); - fileOptions.StaticClass.Should().Be(true); - fileOptions.StaticMembers.Should().Be(true); - fileOptions.PublicClass.Should().Be(false); - fileOptions.PartialClass.Should().Be(false); - fileOptions.GenerateCode.Should().Be(false); - fileOptions.LocalNamespace.Should().Be("namespace1"); - fileOptions.CustomToolNamespace.Should().BeNullOrEmpty(); - fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx"); - fileOptions.ClassName.Should().Be("Path1"); - fileOptions.SkipFile.Should().Be(false); - fileOptions.IsValid.Should().Be(true); - fileOptions.GenerationType.Should().Be(GenerationType.ResourceManager); - } + [Fact] + public void FileDefaults() + { + var fileOptions = GenFileOptions.Select( + file: new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), + subFiles: [] + ), + options: new AnalyzerConfigOptionsProviderStub( + globalOptions: null!, + fileOptions: new AnalyzerConfigOptionsStub() + ), + globalOptions: s_globalOptions + ); + fileOptions.InnerClassName.Should().BeNullOrEmpty(); + fileOptions.InnerClassInstanceName.Should().BeNullOrEmpty(); + fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated); + fileOptions.NullForgivingOperators.Should().Be(false); + fileOptions.StaticClass.Should().Be(true); + fileOptions.StaticMembers.Should().Be(true); + fileOptions.PublicClass.Should().Be(false); + fileOptions.PartialClass.Should().Be(false); + fileOptions.GenerateCode.Should().Be(false); + fileOptions.LocalNamespace.Should().Be("namespace1"); + fileOptions.CustomToolNamespace.Should().BeNullOrEmpty(); + fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx"); + fileOptions.ClassName.Should().Be("Path1"); + fileOptions.SkipFile.Should().Be(false); + fileOptions.IsValid.Should().Be(true); + fileOptions.GenerationType.Should().Be(GenerationType.ResourceManager); + } - [Theory] - [InlineData("project1.csproj", "Path1.resx", null, "project1", "project1.Path1")] - [InlineData("project1.csproj", "Path1.resx", "", "project1", "project1.Path1")] - [InlineData("project1.csproj", "Path1.resx", "rootNamespace", "rootNamespace", "rootNamespace.Path1")] - [InlineData(@"ProjectFolder/project1.csproj", @"ProjectFolder/SubFolder/Path1.resx", "rootNamespace", "rootNamespace.SubFolder", "rootNamespace.SubFolder.Path1")] - [InlineData(@"ProjectFolder/project1.csproj", @"ProjectFolder/SubFolder With Space/Path1.resx", "rootNamespace", "rootNamespace.SubFolder_With_Space", "rootNamespace.SubFolder_With_Space.Path1")] - [InlineData(@"ProjectFolder/project1.csproj", @"ProjectFolder/SubFolder/Path1.resx", null, "SubFolder", "SubFolder.Path1")] - [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/Path1.resx", null, "_8_project", "_8_project.Path1")] - [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/Path1.resx", "", "_8_project", "_8_project.Path1")] - [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/SubFolder/Path1.resx", null, "SubFolder", "SubFolder.Path1")] - [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/SubFolder/Path1.resx", "", "SubFolder", "SubFolder.Path1")] - public void FileSettings_RespectsEmptyRootNamespace( - string msBuildProjectFullPath, - string mainFile, - string rootNamespace, - string expectedLocalNamespace, - string expectedEmbeddedFilename - ) - { - var fileOptions = GenFileOptions.Select( - file: new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(mainFile), Guid.NewGuid()), - subFiles: [] - ), - options: new AnalyzerConfigOptionsProviderStub( - globalOptions: null!, - fileOptions: new AnalyzerConfigOptionsStub() - ), - globalOptions: GlobalOptions.Select( - provider: new AnalyzerConfigOptionsProviderStub( - globalOptions: new AnalyzerConfigOptionsStub - { - MSBuildProjectName = Path.GetFileNameWithoutExtension(msBuildProjectFullPath), - RootNamespace = rootNamespace, - MSBuildProjectFullPath = msBuildProjectFullPath - }, - fileOptions: null! - ), - token: default - ) - ); - fileOptions.InnerClassName.Should().BeNullOrEmpty(); - fileOptions.InnerClassInstanceName.Should().BeNullOrEmpty(); - fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated); - fileOptions.NullForgivingOperators.Should().Be(false); - fileOptions.StaticClass.Should().Be(true); - fileOptions.StaticMembers.Should().Be(true); - fileOptions.PublicClass.Should().Be(false); - fileOptions.PartialClass.Should().Be(false); - fileOptions.GenerateCode.Should().Be(false); - fileOptions.LocalNamespace.Should().Be(expectedLocalNamespace); - fileOptions.CustomToolNamespace.Should().BeNullOrEmpty(); - fileOptions.GroupedFile.MainFile.File.Path.Should().Be(mainFile); - fileOptions.EmbeddedFilename.Should().Be(expectedEmbeddedFilename); - fileOptions.ClassName.Should().Be("Path1"); - fileOptions.IsValid.Should().Be(true); - fileOptions.GenerationType.Should().Be(GenerationType.ResourceManager); - } + [Theory] + [InlineData("project1.csproj", "Path1.resx", null, "project1", "project1.Path1")] + [InlineData("project1.csproj", "Path1.resx", "", "project1", "project1.Path1")] + [InlineData("project1.csproj", "Path1.resx", "rootNamespace", "rootNamespace", "rootNamespace.Path1")] + [InlineData(@"ProjectFolder/project1.csproj", @"ProjectFolder/SubFolder/Path1.resx", "rootNamespace", "rootNamespace.SubFolder", "rootNamespace.SubFolder.Path1")] + [InlineData(@"ProjectFolder/project1.csproj", @"ProjectFolder/SubFolder With Space/Path1.resx", "rootNamespace", "rootNamespace.SubFolder_With_Space", "rootNamespace.SubFolder_With_Space.Path1")] + [InlineData(@"ProjectFolder/project1.csproj", @"ProjectFolder/SubFolder/Path1.resx", null, "SubFolder", "SubFolder.Path1")] + [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/Path1.resx", null, "_8_project", "_8_project.Path1")] + [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/Path1.resx", "", "_8_project", "_8_project.Path1")] + [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/SubFolder/Path1.resx", null, "SubFolder", "SubFolder.Path1")] + [InlineData(@"ProjectFolder/8 project.csproj", @"ProjectFolder/SubFolder/Path1.resx", "", "SubFolder", "SubFolder.Path1")] + public void FileSettings_RespectsEmptyRootNamespace( + string msBuildProjectFullPath, + string mainFile, + string rootNamespace, + string expectedLocalNamespace, + string expectedEmbeddedFilename + ) + { + var fileOptions = GenFileOptions.Select( + file: new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub(mainFile), Guid.NewGuid()), + subFiles: [] + ), + options: new AnalyzerConfigOptionsProviderStub( + globalOptions: null!, + fileOptions: new AnalyzerConfigOptionsStub() + ), + globalOptions: GlobalOptions.Select( + provider: new AnalyzerConfigOptionsProviderStub( + globalOptions: new AnalyzerConfigOptionsStub + { + MSBuildProjectName = Path.GetFileNameWithoutExtension(msBuildProjectFullPath), + RootNamespace = rootNamespace, + MSBuildProjectFullPath = msBuildProjectFullPath + }, + fileOptions: null! + ), + token: default + ) + ); + fileOptions.InnerClassName.Should().BeNullOrEmpty(); + fileOptions.InnerClassInstanceName.Should().BeNullOrEmpty(); + fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated); + fileOptions.NullForgivingOperators.Should().Be(false); + fileOptions.StaticClass.Should().Be(true); + fileOptions.StaticMembers.Should().Be(true); + fileOptions.PublicClass.Should().Be(false); + fileOptions.PartialClass.Should().Be(false); + fileOptions.GenerateCode.Should().Be(false); + fileOptions.LocalNamespace.Should().Be(expectedLocalNamespace); + fileOptions.CustomToolNamespace.Should().BeNullOrEmpty(); + fileOptions.GroupedFile.MainFile.File.Path.Should().Be(mainFile); + fileOptions.EmbeddedFilename.Should().Be(expectedEmbeddedFilename); + fileOptions.ClassName.Should().Be("Path1"); + fileOptions.IsValid.Should().Be(true); + fileOptions.GenerationType.Should().Be(GenerationType.ResourceManager); + } - [Fact] - public void File_PostFix() - { - var fileOptions = GenFileOptions.Select( - file: new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), - subFiles: [] - ), - options: new AnalyzerConfigOptionsProviderStub( - globalOptions: null!, - fileOptions: new AnalyzerConfigOptionsStub { ClassNamePostfix = "test1" } - ), - globalOptions: s_globalOptions - ); - fileOptions.ClassName.Should().Be("Path1test1"); - fileOptions.IsValid.Should().Be(true); - } + [Fact] + public void File_PostFix() + { + var fileOptions = GenFileOptions.Select( + file: new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), + subFiles: [] + ), + options: new AnalyzerConfigOptionsProviderStub( + globalOptions: null!, + fileOptions: new AnalyzerConfigOptionsStub { ClassNamePostfix = "test1" } + ), + globalOptions: s_globalOptions + ); + fileOptions.ClassName.Should().Be("Path1test1"); + fileOptions.IsValid.Should().Be(true); + } - [Fact] - public void FileSettings_CanReadAll() - { - var fileOptions = GenFileOptions.Select( - file: new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), - subFiles: [] - ), - options: new AnalyzerConfigOptionsProviderStub( - globalOptions: null!, - fileOptions: new AnalyzerConfigOptionsStub - { - RootNamespace = "namespace1", - MSBuildProjectFullPath = "project1.csproj", - CustomToolNamespace = "ns1", - InnerClassName = "test1", - InnerClassInstanceName = "test2", - InnerClassVisibility = "public", - NullForgivingOperators = "true", - StaticClass = "false", - StaticMembers = "false", - PublicClass = "true", - PartialClass = "true", - GenerateCode = "true", - GenerationType = "ResourceManager" - } - ), - globalOptions: s_globalOptions - ); - fileOptions.InnerClassName.Should().Be("test1"); - fileOptions.InnerClassInstanceName.Should().Be("test2"); - fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public); - fileOptions.NullForgivingOperators.Should().Be(false); - fileOptions.StaticClass.Should().Be(false); - fileOptions.StaticMembers.Should().Be(false); - fileOptions.PublicClass.Should().Be(true); - fileOptions.PartialClass.Should().Be(true); - fileOptions.IsValid.Should().Be(true); - fileOptions.GenerateCode.Should().Be(true); - fileOptions.LocalNamespace.Should().Be("namespace1"); - fileOptions.CustomToolNamespace.Should().Be("ns1"); - fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx"); - fileOptions.ClassName.Should().Be("Path1"); - fileOptions.GenerationType.Should().Be(GenerationType.ResourceManager); - } + [Fact] + public void FileSettings_CanReadAll() + { + var fileOptions = GenFileOptions.Select( + file: new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), + subFiles: [] + ), + options: new AnalyzerConfigOptionsProviderStub( + globalOptions: null!, + fileOptions: new AnalyzerConfigOptionsStub + { + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", + CustomToolNamespace = "ns1", + InnerClassName = "test1", + InnerClassInstanceName = "test2", + InnerClassVisibility = "public", + NullForgivingOperators = "true", + StaticClass = "false", + StaticMembers = "false", + PublicClass = "true", + PartialClass = "true", + GenerateCode = "true", + GenerationType = "ResourceManager" + } + ), + globalOptions: s_globalOptions + ); + fileOptions.InnerClassName.Should().Be("test1"); + fileOptions.InnerClassInstanceName.Should().Be("test2"); + fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public); + fileOptions.NullForgivingOperators.Should().Be(false); + fileOptions.StaticClass.Should().Be(false); + fileOptions.StaticMembers.Should().Be(false); + fileOptions.PublicClass.Should().Be(true); + fileOptions.PartialClass.Should().Be(true); + fileOptions.IsValid.Should().Be(true); + fileOptions.GenerateCode.Should().Be(true); + fileOptions.LocalNamespace.Should().Be("namespace1"); + fileOptions.CustomToolNamespace.Should().Be("ns1"); + fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx"); + fileOptions.ClassName.Should().Be("Path1"); + fileOptions.GenerationType.Should().Be(GenerationType.ResourceManager); + } - [Fact] - public void FileSettings_RespectsGlobalDefaults() - { - var globalOptions = GlobalOptions.Select( - provider: new AnalyzerConfigOptionsProviderStub( - globalOptions: new AnalyzerConfigOptionsStub - { - RootNamespace = "namespace1", - MSBuildProjectFullPath = "project1.csproj", - MSBuildProjectName = "project1", - ResXGenerator_InnerClassName = "test1", - ResXGenerator_InnerClassInstanceName = "test2", - ResXGenerator_ClassNamePostfix = "test3", - ResXGenerator_InnerClassVisibility = "public", - ResXGenerator_NullForgivingOperators = "true", - ResXGenerator_StaticClass = "false", - ResXGenerator_StaticMembers = "false", - ResXGenerator_PublicClass = "true", - ResXGenerator_PartialClass = "true", - }, - fileOptions: null! - ), - token: default - ); - var fileOptions = GenFileOptions.Select( - file: new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), - subFiles: [] - ), - options: new AnalyzerConfigOptionsProviderStub( - globalOptions: null!, - fileOptions: new AnalyzerConfigOptionsStub() - ), - globalOptions: globalOptions - ); - fileOptions.InnerClassName.Should().Be("test1"); - fileOptions.InnerClassInstanceName.Should().Be("test2"); - fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public); - fileOptions.NullForgivingOperators.Should().Be(true); - fileOptions.StaticClass.Should().Be(false); - fileOptions.StaticMembers.Should().Be(false); - fileOptions.PublicClass.Should().Be(true); - fileOptions.PartialClass.Should().Be(true); - fileOptions.GenerateCode.Should().Be(false); - fileOptions.LocalNamespace.Should().Be("namespace1"); - fileOptions.CustomToolNamespace.Should().BeNullOrEmpty(); - fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx"); - fileOptions.ClassName.Should().Be("Path1test3"); - fileOptions.IsValid.Should().Be(true); - } + [Fact] + public void FileSettings_RespectsGlobalDefaults() + { + var globalOptions = GlobalOptions.Select( + provider: new AnalyzerConfigOptionsProviderStub( + globalOptions: new AnalyzerConfigOptionsStub + { + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", + MSBuildProjectName = "project1", + ResXGenerator_InnerClassName = "test1", + ResXGenerator_InnerClassInstanceName = "test2", + ResXGenerator_ClassNamePostfix = "test3", + ResXGenerator_InnerClassVisibility = "public", + ResXGenerator_NullForgivingOperators = "true", + ResXGenerator_StaticClass = "false", + ResXGenerator_StaticMembers = "false", + ResXGenerator_PublicClass = "true", + ResXGenerator_PartialClass = "true", + }, + fileOptions: null! + ), + token: default + ); + var fileOptions = GenFileOptions.Select( + file: new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), + subFiles: [] + ), + options: new AnalyzerConfigOptionsProviderStub( + globalOptions: null!, + fileOptions: new AnalyzerConfigOptionsStub() + ), + globalOptions: globalOptions + ); + fileOptions.InnerClassName.Should().Be("test1"); + fileOptions.InnerClassInstanceName.Should().Be("test2"); + fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public); + fileOptions.NullForgivingOperators.Should().Be(true); + fileOptions.StaticClass.Should().Be(false); + fileOptions.StaticMembers.Should().Be(false); + fileOptions.PublicClass.Should().Be(true); + fileOptions.PartialClass.Should().Be(true); + fileOptions.GenerateCode.Should().Be(false); + fileOptions.LocalNamespace.Should().Be("namespace1"); + fileOptions.CustomToolNamespace.Should().BeNullOrEmpty(); + fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx"); + fileOptions.ClassName.Should().Be("Path1test3"); + fileOptions.IsValid.Should().Be(true); + } - [Fact] - public void FileSettings_CanSkipIndividualFile() - { - var fileOptions = GenFileOptions.Select( - file: new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), - subFiles: [] - ), - options: new AnalyzerConfigOptionsProviderStub( - globalOptions: null!, - fileOptions: new AnalyzerConfigOptionsStub - { - RootNamespace = "namespace1", - MSBuildProjectFullPath = "project1.csproj", - SkipFile = "true", - } - ), - globalOptions: s_globalOptions - ); + [Fact] + public void FileSettings_CanSkipIndividualFile() + { + var fileOptions = GenFileOptions.Select( + file: new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), + subFiles: [] + ), + options: new AnalyzerConfigOptionsProviderStub( + globalOptions: null!, + fileOptions: new AnalyzerConfigOptionsStub + { + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", + SkipFile = "true", + } + ), + globalOptions: s_globalOptions + ); - fileOptions.LocalNamespace.Should().Be("namespace1"); - fileOptions.SkipFile.Should().Be(true); - fileOptions.IsValid.Should().Be(true); - } + fileOptions.LocalNamespace.Should().Be("namespace1"); + fileOptions.SkipFile.Should().Be(true); + fileOptions.IsValid.Should().Be(true); + } - [Fact] - public void FileSettings_CanSwitchGenerationType() - { - var fileOptions = GenFileOptions.Select( - file: new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), - subFiles: [] - ), - options: new AnalyzerConfigOptionsProviderStub( - globalOptions: null!, - fileOptions: new AnalyzerConfigOptionsStub - { - RootNamespace = "namespace1", - MSBuildProjectFullPath = "project1.csproj", - GenerationType = "StringLocalizer", - } - ), - globalOptions: s_globalOptions - ); - fileOptions.LocalNamespace.Should().Be("namespace1"); - fileOptions.GenerationType.Should().Be(GenerationType.StringLocalizer); - fileOptions.IsValid.Should().Be(true); - } + [Fact] + public void FileSettings_CanSwitchGenerationType() + { + var fileOptions = GenFileOptions.Select( + file: new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), + subFiles: [] + ), + options: new AnalyzerConfigOptionsProviderStub( + globalOptions: null!, + fileOptions: new AnalyzerConfigOptionsStub + { + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", + GenerationType = "StringLocalizer", + } + ), + globalOptions: s_globalOptions + ); + fileOptions.LocalNamespace.Should().Be("namespace1"); + fileOptions.GenerationType.Should().Be(GenerationType.StringLocalizer); + fileOptions.IsValid.Should().Be(true); + } - [Fact] - public void GlobalOptions_CanSwitchGenerationType() - { - var globalOptions = GlobalOptions.Select( - provider: new AnalyzerConfigOptionsProviderStub( - globalOptions: new AnalyzerConfigOptionsStub - { - RootNamespace = "namespace1", - MSBuildProjectFullPath = "project1.csproj", - MSBuildProjectName = "project1", - ResXGenerator_GenerationType = "StringLocalizer" - }, - fileOptions: null! - ), - token: default - ); + [Fact] + public void GlobalOptions_CanSwitchGenerationType() + { + var globalOptions = GlobalOptions.Select( + provider: new AnalyzerConfigOptionsProviderStub( + globalOptions: new AnalyzerConfigOptionsStub + { + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", + MSBuildProjectName = "project1", + ResXGenerator_GenerationType = "StringLocalizer" + }, + fileOptions: null! + ), + token: default + ); - globalOptions.RootNamespace.Should().Be("namespace1"); - globalOptions.GenerationType.Should().Be(GenerationType.StringLocalizer); - globalOptions.IsValid.Should().Be(true); - } + globalOptions.RootNamespace.Should().Be("namespace1"); + globalOptions.GenerationType.Should().Be(GenerationType.StringLocalizer); + globalOptions.IsValid.Should().Be(true); + } - private class AnalyzerConfigOptionsStub : AnalyzerConfigOptions - { - // ReSharper disable InconsistentNaming - public string? MSBuildProjectFullPath { get; init; } - // ReSharper disable InconsistentNaming - public string? MSBuildProjectName { get; init; } - public string? RootNamespace { get; init; } - public string? ResXGenerator_ClassNamePostfix { get; init; } - public string? ResXGenerator_PublicClass { get; init; } - public string? ResXGenerator_NullForgivingOperators { get; init; } - public string? ResXGenerator_StaticClass { get; init; } - public string? ResXGenerator_StaticMembers { get; init; } - public string? ResXGenerator_PartialClass { get; init; } - public string? ResXGenerator_InnerClassVisibility { get; init; } - public string? ResXGenerator_InnerClassName { get; init; } - public string? ResXGenerator_InnerClassInstanceName { get; init; } - public string? ResXGenerator_GenerateCode { get; init; } - public string? ResXGenerator_GenerationType { get; init; } - public string? CustomToolNamespace { get; init; } - public string? TargetPath { get; init; } - public string? ClassNamePostfix { get; init; } - public string? PublicClass { get; init; } - public string? NullForgivingOperators { get; init; } - public string? StaticClass { get; init; } - public string? StaticMembers { get; init; } - public string? PartialClass { get; init; } - public string? InnerClassVisibility { get; init; } - public string? InnerClassName { get; init; } - public string? InnerClassInstanceName { get; init; } - public string? GenerateCode { get; init; } - public string? GenerationType { get; init; } - public string? SkipFile { get; init; } + private class AnalyzerConfigOptionsStub : AnalyzerConfigOptions + { + // ReSharper disable InconsistentNaming + public string? MSBuildProjectFullPath { get; init; } + // ReSharper disable InconsistentNaming + public string? MSBuildProjectName { get; init; } + public string? RootNamespace { get; init; } + public string? ResXGenerator_ClassNamePostfix { get; init; } + public string? ResXGenerator_PublicClass { get; init; } + public string? ResXGenerator_NullForgivingOperators { get; init; } + public string? ResXGenerator_StaticClass { get; init; } + public string? ResXGenerator_StaticMembers { get; init; } + public string? ResXGenerator_PartialClass { get; init; } + public string? ResXGenerator_InnerClassVisibility { get; init; } + public string? ResXGenerator_InnerClassName { get; init; } + public string? ResXGenerator_InnerClassInstanceName { get; init; } + public string? ResXGenerator_GenerateCode { get; init; } + public string? ResXGenerator_GenerationType { get; init; } + public string? CustomToolNamespace { get; init; } + public string? TargetPath { get; init; } + public string? ClassNamePostfix { get; init; } + public string? PublicClass { get; init; } + public string? NullForgivingOperators { get; init; } + public string? StaticClass { get; init; } + public string? StaticMembers { get; init; } + public string? PartialClass { get; init; } + public string? InnerClassVisibility { get; init; } + public string? InnerClassName { get; init; } + public string? InnerClassInstanceName { get; init; } + public string? GenerateCode { get; init; } + public string? GenerationType { get; init; } + public string? SkipFile { get; init; } - // ReSharper restore InconsistentNaming + // ReSharper restore InconsistentNaming - public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) - { - string? GetVal() - { - return key switch - { - "build_property.MSBuildProjectFullPath" => MSBuildProjectFullPath, - "build_property.MSBuildProjectName" => MSBuildProjectName, - "build_property.RootNamespace" => RootNamespace, - "build_property.ResXGenerator_GenerateCode" => ResXGenerator_GenerateCode, - "build_property.ResXGenerator_ClassNamePostfix" => ResXGenerator_ClassNamePostfix, - "build_property.ResXGenerator_PublicClass" => ResXGenerator_PublicClass, - "build_property.ResXGenerator_NullForgivingOperators" => ResXGenerator_NullForgivingOperators, - "build_property.ResXGenerator_StaticClass" => ResXGenerator_StaticClass, - "build_property.ResXGenerator_StaticMembers" => ResXGenerator_StaticMembers, - "build_property.ResXGenerator_PartialClass" => ResXGenerator_PartialClass, - "build_property.ResXGenerator_InnerClassVisibility" => ResXGenerator_InnerClassVisibility, - "build_property.ResXGenerator_InnerClassName" => ResXGenerator_InnerClassName, - "build_property.ResXGenerator_InnerClassInstanceName" => ResXGenerator_InnerClassInstanceName, - "build_property.ResXGenerator_GenerationType" => ResXGenerator_GenerationType, - "build_metadata.EmbeddedResource.CustomToolNamespace" => CustomToolNamespace, - "build_metadata.EmbeddedResource.TargetPath" => TargetPath, - "build_metadata.EmbeddedResource.ClassNamePostfix" => ClassNamePostfix, - "build_metadata.EmbeddedResource.PublicClass" => PublicClass, - "build_metadata.EmbeddedResource.NullForgivingOperators" => NullForgivingOperators, - "build_metadata.EmbeddedResource.StaticClass" => StaticClass, - "build_metadata.EmbeddedResource.StaticMembers" => StaticMembers, - "build_metadata.EmbeddedResource.PartialClass" => PartialClass, - "build_metadata.EmbeddedResource.InnerClassVisibility" => InnerClassVisibility, - "build_metadata.EmbeddedResource.InnerClassName" => InnerClassName, - "build_metadata.EmbeddedResource.InnerClassInstanceName" => InnerClassInstanceName, - "build_metadata.EmbeddedResource.GenerateCode" => GenerateCode, - "build_metadata.EmbeddedResource.SkipFile" => SkipFile, - "build_metadata.EmbeddedResource.GenerationType" => GenerationType, - _ => null - }; - } + public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) + { + string? GetVal() + { + return key switch + { + "build_property.MSBuildProjectFullPath" => MSBuildProjectFullPath, + "build_property.MSBuildProjectName" => MSBuildProjectName, + "build_property.RootNamespace" => RootNamespace, + "build_property.ResXGenerator_GenerateCode" => ResXGenerator_GenerateCode, + "build_property.ResXGenerator_ClassNamePostfix" => ResXGenerator_ClassNamePostfix, + "build_property.ResXGenerator_PublicClass" => ResXGenerator_PublicClass, + "build_property.ResXGenerator_NullForgivingOperators" => ResXGenerator_NullForgivingOperators, + "build_property.ResXGenerator_StaticClass" => ResXGenerator_StaticClass, + "build_property.ResXGenerator_StaticMembers" => ResXGenerator_StaticMembers, + "build_property.ResXGenerator_PartialClass" => ResXGenerator_PartialClass, + "build_property.ResXGenerator_InnerClassVisibility" => ResXGenerator_InnerClassVisibility, + "build_property.ResXGenerator_InnerClassName" => ResXGenerator_InnerClassName, + "build_property.ResXGenerator_InnerClassInstanceName" => ResXGenerator_InnerClassInstanceName, + "build_property.ResXGenerator_GenerationType" => ResXGenerator_GenerationType, + "build_metadata.EmbeddedResource.CustomToolNamespace" => CustomToolNamespace, + "build_metadata.EmbeddedResource.TargetPath" => TargetPath, + "build_metadata.EmbeddedResource.ClassNamePostfix" => ClassNamePostfix, + "build_metadata.EmbeddedResource.PublicClass" => PublicClass, + "build_metadata.EmbeddedResource.NullForgivingOperators" => NullForgivingOperators, + "build_metadata.EmbeddedResource.StaticClass" => StaticClass, + "build_metadata.EmbeddedResource.StaticMembers" => StaticMembers, + "build_metadata.EmbeddedResource.PartialClass" => PartialClass, + "build_metadata.EmbeddedResource.InnerClassVisibility" => InnerClassVisibility, + "build_metadata.EmbeddedResource.InnerClassName" => InnerClassName, + "build_metadata.EmbeddedResource.InnerClassInstanceName" => InnerClassInstanceName, + "build_metadata.EmbeddedResource.GenerateCode" => GenerateCode, + "build_metadata.EmbeddedResource.SkipFile" => SkipFile, + "build_metadata.EmbeddedResource.GenerationType" => GenerationType, + _ => null + }; + } - value = GetVal(); - return value is not null; - } - } + value = GetVal(); + return value is not null; + } + } - private class AnalyzerConfigOptionsProviderStub(AnalyzerConfigOptions globalOptions, AnalyzerConfigOptions fileOptions) : AnalyzerConfigOptionsProvider - { - private readonly AnalyzerConfigOptions _fileOptions = fileOptions; + private class AnalyzerConfigOptionsProviderStub(AnalyzerConfigOptions globalOptions, AnalyzerConfigOptions fileOptions) : AnalyzerConfigOptionsProvider + { + private readonly AnalyzerConfigOptions _fileOptions = fileOptions; - public override AnalyzerConfigOptions GlobalOptions { get; } = globalOptions; + public override AnalyzerConfigOptions GlobalOptions { get; } = globalOptions; - public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException(); + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException(); - public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => _fileOptions; - } + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => _fileOptions; + } } diff --git a/Aigamo.ResXGenerator.Tests/UtilitiesTests.cs b/Aigamo.ResXGenerator.Tests/UtilitiesTests.cs index 19b42fc..5e3f05f 100644 --- a/Aigamo.ResXGenerator.Tests/UtilitiesTests.cs +++ b/Aigamo.ResXGenerator.Tests/UtilitiesTests.cs @@ -1,43 +1,43 @@ -using FluentAssertions; +using FluentAssertions; using Xunit; namespace Aigamo.ResXGenerator.Tests; public class UtilitiesTests { - [Theory] - [InlineData("Valid", "Valid")] - [InlineData("_Valid", "_Valid")] - [InlineData("Valid123", "Valid123")] - [InlineData("Valid_123", "Valid_123")] - [InlineData("Valid.123", "Valid.123")] - [InlineData("8Ns", "_8Ns")] - [InlineData("Ns+InvalidChar", "Ns_InvalidChar")] - [InlineData("Ns..Folder...Folder2", "Ns.Folder.Folder2")] - [InlineData("Ns.Folder.", "Ns.Folder")] - [InlineData(".Ns.Folder", "Ns.Folder")] - [InlineData("Folder with space", "Folder_with_space")] - [InlineData("folder with .. space", "folder_with_._space")] - public void SanitizeNamespace(string input, string expected) => Utilities.SanitizeNamespace(input).Should().Be(expected); + [Theory] + [InlineData("Valid", "Valid")] + [InlineData("_Valid", "_Valid")] + [InlineData("Valid123", "Valid123")] + [InlineData("Valid_123", "Valid_123")] + [InlineData("Valid.123", "Valid.123")] + [InlineData("8Ns", "_8Ns")] + [InlineData("Ns+InvalidChar", "Ns_InvalidChar")] + [InlineData("Ns..Folder...Folder2", "Ns.Folder.Folder2")] + [InlineData("Ns.Folder.", "Ns.Folder")] + [InlineData(".Ns.Folder", "Ns.Folder")] + [InlineData("Folder with space", "Folder_with_space")] + [InlineData("folder with .. space", "folder_with_._space")] + public void SanitizeNamespace(string input, string expected) => input.SanitizeNamespace().Should().Be(expected); - [Theory] - [InlineData("Valid", "Valid")] - [InlineData(".Valid", ".Valid")] - [InlineData("8Ns", "8Ns")] - [InlineData("..Ns", ".Ns")] - public void SanitizeNamespaceWithoutFirstCharRules(string input, string expected) => Utilities.SanitizeNamespace(input, false).Should().Be(expected); + [Theory] + [InlineData("Valid", "Valid")] + [InlineData(".Valid", ".Valid")] + [InlineData("8Ns", "8Ns")] + [InlineData("..Ns", ".Ns")] + public void SanitizeNamespaceWithoutFirstCharRules(string input, string expected) => input.SanitizeNamespace(false).Should().Be(expected); - [Fact] - public void GetLocalNamespace_ShouldNotGenerateIllegalNamespace() - { - var ns = Utilities.GetLocalNamespace("resx", "asd.asd", "path", "name", "root"); - ns.Should().Be("root"); - } + [Fact] + public void GetLocalNamespace_ShouldNotGenerateIllegalNamespace() + { + var ns = Utilities.GetLocalNamespace("resx", "asd.asd", "path", "name", "root"); + ns.Should().Be("root"); + } - [Fact] - public void ResxFileName_ShouldNotGenerateIllegalClassNames() - { - var ns = Utilities.GetClassNameFromPath("test.cshtml.resx"); - ns.Should().Be("test"); - } + [Fact] + public void ResxFileName_ShouldNotGenerateIllegalClassNames() + { + var ns = Utilities.GetClassNameFromPath("test.cshtml.resx"); + ns.Should().Be("test"); + } } diff --git a/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj b/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj index 1ba97a6..06ad302 100644 --- a/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj +++ b/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj @@ -1,53 +1,53 @@ - - netstandard2.0 - latest - enable - true - VocaDB, Aigamo - MIT - https://github.com/ycanardeau/ResXGenerator - Aigamo.ResXGenerator - git - https://github.com/ycanardeau/ResXGenerator - ResXGenerator is a C# source generator to generate strongly-typed resource classes for looking up localized strings. - false - false - true - $(NoWarn);NU5128 - true - enable - true - - - - - true - build\ - - - - - - all - - - - - - - - - - - - - README.md - - - - - + + netstandard2.0 + latest + enable + true + VocaDB, Aigamo + MIT + https://github.com/ycanardeau/ResXGenerator + Aigamo.ResXGenerator + git + https://github.com/ycanardeau/ResXGenerator + ResXGenerator is a C# source generator to generate strongly-typed resource classes for looking up localized strings. + false + false + true + $(NoWarn);NU5128 + true + enable + true + + + + + true + build\ + + + + + + all + + + + + + + + + + + + + README.md + + + + + diff --git a/Aigamo.ResXGenerator/Aigamo.ResXGenerator.targets b/Aigamo.ResXGenerator/Aigamo.ResXGenerator.targets index b8866e1..1eea3d2 100644 --- a/Aigamo.ResXGenerator/Aigamo.ResXGenerator.targets +++ b/Aigamo.ResXGenerator/Aigamo.ResXGenerator.targets @@ -1,16 +1,16 @@ - + - - $(TargetsForTfmSpecificContentInPackage);PackBuildOutputs - + + $(TargetsForTfmSpecificContentInPackage);PackBuildOutputs + - - - - - - - - + + + + + + + + diff --git a/Aigamo.ResXGenerator/Analyser.cs b/Aigamo.ResXGenerator/Analyser.cs index 737a4bf..658ed8d 100644 --- a/Aigamo.ResXGenerator/Analyser.cs +++ b/Aigamo.ResXGenerator/Analyser.cs @@ -7,84 +7,84 @@ namespace Aigamo.ResXGenerator; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class Analyser : DiagnosticAnalyzer { - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( - DuplicateWarning, - MemberSameAsClassWarning, - MemberWithStaticError, - LocalizerStaticError, - LocalizerPartialError, - LocalizationIncoherentNamespace, - FatalError - ); + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + DuplicateWarning, + MemberSameAsClassWarning, + MemberWithStaticError, + LocalizerStaticError, + LocalizerPartialError, + LocalizationIncoherentNamespace, + FatalError + ); - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); - // We only use the DiagnosticDescriptor to report from the Source Generator - } + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + // We only use the DiagnosticDescriptor to report from the Source Generator + } - public static readonly DiagnosticDescriptor DuplicateWarning = new( - id: "AigamoResXGenerator001", - title: "Duplicate member", - messageFormat: "Ignored added member '{0}'", - category: "ResXGenerator", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor DuplicateWarning = new( + id: "AigamoResXGenerator001", + title: "Duplicate member", + messageFormat: "Ignored added member '{0}'", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor MemberSameAsClassWarning = new( - id: "AigamoResXGenerator002", - title: "Member same name as class", - messageFormat: "Ignored member '{0}' has same name as class", - category: "ResXGenerator", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor MemberSameAsClassWarning = new( + id: "AigamoResXGenerator002", + title: "Member same name as class", + messageFormat: "Ignored member '{0}' has same name as class", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor MemberWithStaticError = new( - id: "AigamoResXGenerator003", - title: "Incompatible settings", - messageFormat: "Cannot have static members/class with an class instance", - category: "ResXGenerator", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor MemberWithStaticError = new( + id: "AigamoResXGenerator003", + title: "Incompatible settings", + messageFormat: "Cannot have static members/class with an class instance", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor LocalizerStaticError = new( - id: "AigamoResXGenerator004", - title: "Incompatible settings", - messageFormat: "When using StringLocalizer , the static option not available. Parameter ignored.", - category: "ResXGenerator", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor LocalizerStaticError = new( + id: "AigamoResXGenerator004", + title: "Incompatible settings", + messageFormat: "When using StringLocalizer , the static option not available. Parameter ignored.", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor LocalizerPartialError = new( - id: "AigamoResXGenerator005", - title: "Incompatible settings", - messageFormat: "When using StringLocalizer , the partial option not available. Parameter ignored.", - category: "ResXGenerator", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor LocalizerPartialError = new( + id: "AigamoResXGenerator005", + title: "Incompatible settings", + messageFormat: "When using StringLocalizer , the partial option not available. Parameter ignored.", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); - public static readonly DiagnosticDescriptor LocalizationIncoherentNamespace = new( - id: "AigamoResXGenerator006", - title: "Incoherent namespace", - messageFormat: - "When using StringLocalizer, the namespace must be the same as the Resx file. Either remove the CustomToolNamespace or change the GenerationType to ResourceManager. Parameter ignored.", - category: "ResXGenerator", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor LocalizationIncoherentNamespace = new( + id: "AigamoResXGenerator006", + title: "Incoherent namespace", + messageFormat: + "When using StringLocalizer, the namespace must be the same as the Resx file. Either remove the CustomToolNamespace or change the GenerationType to ResourceManager. Parameter ignored.", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); - public static DiagnosticDescriptor FatalError => new( - id: "AigamoResXGenerator999", - title: "Fatal Error generated", - messageFormat: "An error occured on generation file {0} error {1}", - category: "ResXGenerator", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true - ); + public static DiagnosticDescriptor FatalError => new( + id: "AigamoResXGenerator999", + title: "Fatal Error generated", + messageFormat: "An error occured on generation file {0} error {1}", + category: "ResXGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); } diff --git a/Aigamo.ResXGenerator/Extensions/BoolExtensions.cs b/Aigamo.ResXGenerator/Extensions/BoolExtensions.cs index babcd5a..5c3abe8 100644 --- a/Aigamo.ResXGenerator/Extensions/BoolExtensions.cs +++ b/Aigamo.ResXGenerator/Extensions/BoolExtensions.cs @@ -1,5 +1,6 @@ namespace Aigamo.ResXGenerator.Extensions; - public static class BoolExtensions - { - public static T InterpolateCondition(this bool condition, T valueIfTrue, T valueIfFalse) => condition ? valueIfTrue : valueIfFalse; - } + +public static class BoolExtensions +{ + public static T InterpolateCondition(this bool condition, T valueIfTrue, T valueIfFalse) => condition ? valueIfTrue : valueIfFalse; +} diff --git a/Aigamo.ResXGenerator/Extensions/EnumerableExtensions.cs b/Aigamo.ResXGenerator/Extensions/EnumerableExtensions.cs index 1058f68..6bda930 100644 --- a/Aigamo.ResXGenerator/Extensions/EnumerableExtensions.cs +++ b/Aigamo.ResXGenerator/Extensions/EnumerableExtensions.cs @@ -1,8 +1,9 @@ namespace Aigamo.ResXGenerator.Extensions; + public static class EnumerableExtensions { - public static void ForEach(this IEnumerable col, Action action) - { - foreach (var i in col) action(i); - } + public static void ForEach(this IEnumerable col, Action action) + { + foreach (var i in col) action(i); + } } diff --git a/Aigamo.ResXGenerator/Extensions/StringExtensions.cs b/Aigamo.ResXGenerator/Extensions/StringExtensions.cs index e09f555..28ea526 100644 --- a/Aigamo.ResXGenerator/Extensions/StringExtensions.cs +++ b/Aigamo.ResXGenerator/Extensions/StringExtensions.cs @@ -5,18 +5,19 @@ namespace Aigamo.ResXGenerator.Extensions; internal static class StringExtensions { - public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrEmpty(value); + public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrEmpty(value); - public static string ToXmlCommentSafe(this string input, string indent) - { - var lines = HttpUtility.HtmlEncode(input.Trim()).GetCodeLines(); - return string.Join($"{Environment.NewLine}{indent}/// ", lines); - } - public static string Indent(this string input, int level = 1) - { - var indent = new string(' ', level * Constants.IndentCount); - return string.Join(Environment.NewLine, input.GetCodeLines().Select(l => indent + l)); - } + public static string ToXmlCommentSafe(this string input) => input.ToXmlCommentSafe(string.Empty); + public static string ToXmlCommentSafe(this string input, string indent) + { + var lines = HttpUtility.HtmlEncode(input.Trim()).GetCodeLines(); + return string.Join($"{Environment.NewLine}{indent}/// ", lines); + } + public static string Indent(this string input, int level = 1) + { + var indent = new string('\t', level); + return string.Join($"{Environment.NewLine}{indent}", input.GetCodeLines()); + } - public static IEnumerable GetCodeLines(this string input) => RegexDefinitions.NewLine.Split(input); + public static IEnumerable GetCodeLines(this string input) => RegexDefinitions.NewLine.Split(input); } diff --git a/Aigamo.ResXGenerator/Generators/CodeGenerator.cs b/Aigamo.ResXGenerator/Generators/CodeGenerator.cs index 6b12759..c8cd498 100644 --- a/Aigamo.ResXGenerator/Generators/CodeGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/CodeGenerator.cs @@ -9,75 +9,75 @@ namespace Aigamo.ResXGenerator.Generators; public sealed class CodeGenerator : GeneratorBase, IResXGenerator { - private StringBuilderGeneratorHelper Helper { get; set; } + private StringBuilderGeneratorHelper Helper { get; set; } - public override GeneratedOutput Generate(GenFileOptions options, CancellationToken cancellationToken = default) - { - Init(options); + public override GeneratedOutput Generate(GenFileOptions options, CancellationToken cancellationToken = default) + { + Init(options); - Helper = new StringBuilderGeneratorHelper(Options); + Helper = new StringBuilderGeneratorHelper(Options); - Content = Options.GroupedFile.MainFile.File.GetText(cancellationToken); - if (Content is null) - { - GeneratedFileName = Options.GroupedFile.MainFile.File.Path; - Helper.Append("//ERROR reading file:"); - return Helper.GetOutput(GeneratedFileName, Validator); - } + Content = Options.GroupedFile.MainFile.File.GetText(cancellationToken); + if (Content is null) + { + GeneratedFileName = Options.GroupedFile.MainFile.File.Path; + Helper.Append("//ERROR reading file:"); + return Helper.GetOutput(GeneratedFileName, Validator); + } - GeneratedFileName = $"{Options.LocalNamespace}.{Options.ClassName}.g.cs"; + GeneratedFileName = $"{Options.LocalNamespace}.{Options.ClassName}.g.cs"; - Helper.AppendHeader(options.CustomToolNamespace ?? options.LocalNamespace); - Helper.AppendCodeUsings(); - Helper.AppendClassHeader(Options); - Helper.AppendInnerClass(Options, Validator); - GenerateCode(cancellationToken); - Helper.AppendClassFooter(Options); + Helper.AppendHeader(options.CustomToolNamespace ?? options.LocalNamespace); + Helper.AppendCodeUsings(); + Helper.AppendClassHeader(Options); + Helper.AppendInnerClass(Options, Validator); + GenerateCode(cancellationToken); + Helper.AppendClassFooter(Options); - return Helper.GetOutput(GeneratedFileName, Validator); - } + return Helper.GetOutput(GeneratedFileName, Validator); + } - private void GenerateCode(CancellationToken cancellationToken) - { - var combo = new CultureInfoCombo(Options.GroupedFile.SubFiles); - var definedLanguages = combo.GetDefinedLanguages(); + private void GenerateCode(CancellationToken cancellationToken) + { + var combo = new CultureInfoCombo(Options.GroupedFile.SubFiles); + var definedLanguages = combo.GetDefinedLanguages(); - var fallback = ReadResxFile(Content!); - var subfiles = definedLanguages.Select(lang => - { - var subcontent = lang.FileWithHash.File.GetText(cancellationToken); - return subcontent is null - ? null - : ReadResxFile(subcontent)? - .GroupBy(x => x.Key) - .ToImmutableDictionary(x => x.Key, x => x.First().Value); - }).ToList(); + var fallback = ReadResxFile(Content!); + var subfiles = definedLanguages.Select(lang => + { + var subcontent = lang.FileWithHash.File.GetText(cancellationToken); + return subcontent is null + ? null + : ReadResxFile(subcontent)? + .GroupBy(x => x.Key) + .ToImmutableDictionary(x => x.Key, x => x.First().Value); + }).ToList(); - if (fallback is null || subfiles.Any(x => x is null)) - { - Helper.AppendFormat("//could not read {0} or one of its children", Options.GroupedFile.MainFile.File.Path); - return; - } + if (fallback is null || subfiles.Any(x => x is null)) + { + Helper.AppendFormat("//could not read {0} or one of its children", Options.GroupedFile.MainFile.File.Path); + return; + } - fallback.ForEach(fbi => - { - cancellationToken.ThrowIfCancellationRequested(); - if (Helper.GenerateMember(fbi, Options, Validator) is not { valid: true }) return; + fallback.ForEach(fbi => + { + cancellationToken.ThrowIfCancellationRequested(); + if (Helper.GenerateMember(fbi, Options, Validator) is not { valid: true }) return; - Helper.Append(" => GetString_"); - Helper.AppendLanguages(definedLanguages); - Helper.Append("("); - Helper.Append(SymbolDisplay.FormatLiteral(fbi.Value, true)); + Helper.Append(" => GetString_"); + Helper.AppendLanguages(definedLanguages); + Helper.Append("("); + Helper.Append(SymbolDisplay.FormatLiteral(fbi.Value, true)); - subfiles.ForEach(xml => - { - Helper.Append(", "); - if (!xml!.TryGetValue(fbi.Key, out var langValue)) - langValue = fbi.Value; - Helper.Append(SymbolDisplay.FormatLiteral(langValue, true)); - }); + subfiles.ForEach(xml => + { + Helper.Append(", "); + if (!xml!.TryGetValue(fbi.Key, out var langValue)) + langValue = fbi.Value; + Helper.Append(SymbolDisplay.FormatLiteral(langValue, true)); + }); - Helper.AppendLine(");"); - }); - } + Helper.AppendLine(");"); + }); + } } diff --git a/Aigamo.ResXGenerator/Generators/ComboGenerator.cs b/Aigamo.ResXGenerator/Generators/ComboGenerator.cs index f8a1ff6..aab4bcf 100644 --- a/Aigamo.ResXGenerator/Generators/ComboGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/ComboGenerator.cs @@ -8,76 +8,76 @@ namespace Aigamo.ResXGenerator.Generators; public sealed class ComboGenerator : GeneratorBase, IComboGenerator { - private const string OutputStringFilenameFormat = "Aigamo.ResXGenerator.{0}.g.cs"; - private static readonly Dictionary> s_allChildren = new(); - private StringBuilderGeneratorHelper Helper { get; set; } + private const string OutputStringFilenameFormat = "Aigamo.ResXGenerator.{0}.g.cs"; + private static readonly Dictionary> s_allChildren = new(); + private StringBuilderGeneratorHelper Helper { get; set; } - /// - /// Build all CultureInfo children - /// - static ComboGenerator() - { - var all = CultureInfo.GetCultures(CultureTypes.AllCultures); + /// + /// Build all CultureInfo children + /// + static ComboGenerator() + { + var all = CultureInfo.GetCultures(CultureTypes.AllCultures); - all.ForEach(cultureInfo => - { - if (cultureInfo.LCID == 4096 || cultureInfo.IsNeutralCulture || cultureInfo.Name.IsNullOrEmpty()) - return; + all.ForEach(cultureInfo => + { + if (cultureInfo.LCID == 4096 || cultureInfo.IsNeutralCulture || cultureInfo.Name.IsNullOrEmpty()) + return; - var parent = cultureInfo.Parent; - if (!s_allChildren.TryGetValue(parent.LCID, out var v)) - s_allChildren[parent.LCID] = v = []; - v.Add(cultureInfo.LCID); - }); - } + var parent = cultureInfo.Parent; + if (!s_allChildren.TryGetValue(parent.LCID, out var v)) + s_allChildren[parent.LCID] = v = []; + v.Add(cultureInfo.LCID); + }); + } - public override GeneratedOutput Generate(CultureInfoCombo options, CancellationToken cancellationToken = default) - { - Init(options); - Helper = new StringBuilderGeneratorHelper(); + public override GeneratedOutput Generate(CultureInfoCombo options, CancellationToken cancellationToken = default) + { + Init(options); + Helper = new StringBuilderGeneratorHelper(); - var definedLanguages = Options.GetDefinedLanguages(); + var definedLanguages = Options.GetDefinedLanguages(); - Helper.AppendHeader("Aigamo.ResXGenerator"); + Helper.AppendHeader("Aigamo.ResXGenerator"); - Helper.AppendLine("internal static partial class Helpers"); - Helper.AppendLine("{"); + Helper.AppendLine("internal static partial class Helpers"); + Helper.AppendLine("{"); - Helper.Append(" public static string GetString_"); - var functionNamePostFix = Helper.AppendLanguages(definedLanguages); - Helper.Append("(string fallback"); - definedLanguages.ForEach(ci => - { - Helper.Append(", "); - Helper.Append("string "); - Helper.Append(ci.Name); - }); + Helper.Append("\tpublic static string GetString_"); + var functionNamePostFix = Helper.AppendLanguages(definedLanguages); + Helper.Append("(string fallback"); + definedLanguages.ForEach(ci => + { + Helper.Append(", "); + Helper.Append("string "); + Helper.Append(ci.Name); + }); - GeneratedFileName = string.Format(OutputStringFilenameFormat, functionNamePostFix); + GeneratedFileName = string.Format(OutputStringFilenameFormat, functionNamePostFix); - Helper.Append(") => "); - Helper.Append(Constants.SystemGlobalization); - Helper.AppendLine(".CultureInfo.CurrentUICulture.LCID switch"); - Helper.AppendLine(" {"); - var already = new HashSet(); - definedLanguages.ForEach(ci => - { - var findParents = FindParents(ci.LCID).Except(already).ToList(); - findParents - .Select(parent => - { - already.Add(parent); - return $" {parent} => {ci.Name.Replace('-', '_')},"; - }) - .ForEach(l => Helper.AppendLine(l)); - }); + Helper.Append(") => "); + Helper.Append(Constants.SystemGlobalization); + Helper.AppendLine(".CultureInfo.CurrentUICulture.LCID switch"); + Helper.AppendLine("\t{"); + var already = new HashSet(); + definedLanguages.ForEach(ci => + { + var findParents = FindParents(ci.LCID).Except(already).ToList(); + findParents + .Select(parent => + { + already.Add(parent); + return $"\t\t{parent} => {ci.Name.Replace('-', '_')},"; + }) + .ForEach(l => Helper.AppendLine(l)); + }); - Helper.AppendLine(" _ => fallback"); - Helper.AppendLine(" };"); - Helper.AppendLine("}"); + Helper.AppendLine("\t\t_ => fallback"); + Helper.AppendLine("\t};"); + Helper.AppendLine("}"); - return Helper.GetOutput(GeneratedFileName, Validator); - } + return Helper.GetOutput(GeneratedFileName, Validator); + } - private static IEnumerable FindParents(int toFind) => s_allChildren.TryGetValue(toFind, out var v) ? v.Prepend(toFind) : [toFind]; + private static IEnumerable FindParents(int toFind) => s_allChildren.TryGetValue(toFind, out var v) ? v.Prepend(toFind) : [toFind]; } diff --git a/Aigamo.ResXGenerator/Generators/GeneratorBase.cs b/Aigamo.ResXGenerator/Generators/GeneratorBase.cs index 905ffb5..12fd0e6 100644 --- a/Aigamo.ResXGenerator/Generators/GeneratorBase.cs +++ b/Aigamo.ResXGenerator/Generators/GeneratorBase.cs @@ -8,33 +8,33 @@ namespace Aigamo.ResXGenerator.Generators; public abstract class GeneratorBase : IGenerator { - protected SourceText Content { get; set; } - public string GeneratedFileName { get; protected set; } - protected T Options { get; private set; } - protected IntegrityValidator Validator { get; private set; } - - protected void Init(T options) - { - Options = options; - Validator = new IntegrityValidator(); - Content = SourceText.From(string.Empty); - GeneratedFileName = string.Empty; - } - - public abstract GeneratedOutput Generate(T options, CancellationToken cancellationToken = default); - - protected static IEnumerable ReadResxFile(SourceText content) - { - using var reader = new StringReader(content.ToString()); - - if (XDocument.Load(reader, LoadOptions.SetLineInfo).Root is { } element) - return element - .Descendants() - .Where(static data => data.Name == "data") - .Select(static data => new FallBackItem(data.Attribute("name")!.Value, data.Descendants("value").First().Value, data.Attribute("name")!)); - - return []; - } - - protected GeneratedOutput GetOutput(string sourceCode) => new(GeneratedFileName, sourceCode, Validator.ErrorsAndWarnings); + protected SourceText Content { get; set; } + public string GeneratedFileName { get; protected set; } + protected T Options { get; private set; } + protected IntegrityValidator Validator { get; private set; } + + protected void Init(T options) + { + Options = options; + Validator = new IntegrityValidator(); + Content = SourceText.From(string.Empty); + GeneratedFileName = string.Empty; + } + + public abstract GeneratedOutput Generate(T options, CancellationToken cancellationToken = default); + + protected static IEnumerable ReadResxFile(SourceText content) + { + using var reader = new StringReader(content.ToString()); + + if (XDocument.Load(reader, LoadOptions.SetLineInfo).Root is { } element) + return element + .Descendants() + .Where(static data => data.Name == "data") + .Select(static data => new FallBackItem(data.Attribute("name")!.Value, data.Descendants("value").First().Value, data.Attribute("name")!)); + + return []; + } + + protected GeneratedOutput GetOutput(string sourceCode) => new(GeneratedFileName, sourceCode, Validator.ErrorsAndWarnings); } diff --git a/Aigamo.ResXGenerator/Generators/LocalizerGenerator.cs b/Aigamo.ResXGenerator/Generators/LocalizerGenerator.cs index 068b190..b000844 100644 --- a/Aigamo.ResXGenerator/Generators/LocalizerGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/LocalizerGenerator.cs @@ -6,58 +6,59 @@ namespace Aigamo.ResXGenerator.Generators; public sealed class LocalizerGenerator : GeneratorBase, IResXGenerator { - public override GeneratedOutput Generate(GenFileOptions options, CancellationToken cancellationToken = default) - { - Init(options); - var generatedFileName = $"{options.LocalNamespace}.{options.ClassName}.g.cs"; - - Content = options.GroupedFile.MainFile.File.GetText(cancellationToken); - if (Content is null) return new GeneratedOutput(options.GroupedFile.MainFile.File.Path, "//ERROR reading file:", []); - - Validator.ValidateInconsistentNameSpace(Options); - Validator.ValidateLocalizationModifiers(Options); - - var sourceCode = GenerateResourceManager(); - - return new GeneratedOutput(generatedFileName, sourceCode, Validator.ErrorsAndWarnings); - } - - //Note with StringLocalization injection work with singleton or transient - //No static class where allowed - private string GenerateResourceManager() - { - var fallback = ReadResxFile(Content)?.ToList(); - - return fallback is null ? "//could not read {0} or one of its children" : - $$""" - {{Constants.AutoGeneratedHeader}} - {{Options.NullForgivingOperators.InterpolateCondition("#nullable disable", "#nullable enable")}} - using Microsoft.Extensions.Localization; - using System.Text; - - namespace {{Options.LocalNamespace}}; - - public interface I{{Options.ClassName}} - { - {{string.Join(Environment.NewLine, fallback.Select(GenerateInterfaceMembers)).Indent()}} - } - - {{Options.PublicClass.InterpolateCondition("public", "internal")}} class {{Options.ClassName}}(IStringLocalizer<{{Options.ClassName}}> stringLocalizer) : I{{Options.ClassName}} - { - {{string.Join(Environment.NewLine, fallback.Select(GenerateMembers)).Indent()}} - } - """; - } - - private string GenerateMembers(FallBackItem fallbackItem) => !Validator.ValidateMember(fallbackItem, Options) ? - $"// Skipped invalid member name: {fallbackItem.Key}" : - $"public string {fallbackItem.Key} => stringLocalizer[\"{fallbackItem.Key}\"];"; - - private static string GenerateInterfaceMembers(FallBackItem fallbackItem) => - $$""" - /// - /// Looks up a localized string similar to {{fallbackItem.Value.ToXmlCommentSafe(string.Empty)}}. - /// - string {{fallbackItem.Key}} {get;} - """; + public override GeneratedOutput Generate(GenFileOptions options, CancellationToken cancellationToken = default) + { + Init(options); + var generatedFileName = $"{options.LocalNamespace}.{options.ClassName}.g.cs"; + + Content = options.GroupedFile.MainFile.File.GetText(cancellationToken); + if (Content is null) return new GeneratedOutput(options.GroupedFile.MainFile.File.Path, "//ERROR reading file:", []); + + Validator.ValidateInconsistentNameSpace(Options); + Validator.ValidateLocalizationModifiers(Options); + + var sourceCode = GenerateResourceManager(); + + return new GeneratedOutput(generatedFileName, sourceCode, Validator.ErrorsAndWarnings); + } + + //Note with StringLocalization injection work with singleton or transient + //No static class where allowed + private string GenerateResourceManager() + { + var fallback = ReadResxFile(Content)?.ToList(); + + return fallback is null + ? "//could not read {0} or one of its children" + : $$""" + {{Constants.AutoGeneratedHeader}} + {{Options.NullForgivingOperators.InterpolateCondition("#nullable disable", "#nullable enable")}} + using Microsoft.Extensions.Localization; + using System.Text; + + namespace {{Options.LocalNamespace}}; + + public interface I{{Options.ClassName}} + { + {{string.Join(Environment.NewLine, fallback.Select(GenerateInterfaceMembers)).Indent()}} + } + + {{Options.PublicClass.InterpolateCondition("public", "internal")}} class {{Options.ClassName}}(IStringLocalizer<{{Options.ClassName}}> stringLocalizer) : I{{Options.ClassName}} + { + {{string.Join(Environment.NewLine, fallback.Select(GenerateMembers)).Indent()}} + } + """; + } + + private string GenerateMembers(FallBackItem fallbackItem) => !Validator.ValidateMember(fallbackItem, Options) ? + $"// Skipped invalid member name: {fallbackItem.Key}" : + $"public string {fallbackItem.Key} => stringLocalizer[\"{fallbackItem.Key}\"];"; + + private static string GenerateInterfaceMembers(FallBackItem fallbackItem) => + $$""" + /// + /// Looks up a localized string similar to {{fallbackItem.Value.ToXmlCommentSafe()}}. + /// + string {{fallbackItem.Key}} {get;} + """; } diff --git a/Aigamo.ResXGenerator/Generators/LocalizerGlobalRegisterGenerator.cs b/Aigamo.ResXGenerator/Generators/LocalizerGlobalRegisterGenerator.cs index c47d85b..49f49f8 100644 --- a/Aigamo.ResXGenerator/Generators/LocalizerGlobalRegisterGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/LocalizerGlobalRegisterGenerator.cs @@ -7,35 +7,36 @@ namespace Aigamo.ResXGenerator.Generators; public sealed class LocalizerGlobalRegisterGenerator : GeneratorBase>, IGlobalRegisterGenerator { - public override GeneratedOutput Generate(ImmutableArray options, CancellationToken cancellationToken = default) - { - Init(options); - - GeneratedFileName = "Aigamo.ResXGenerator.Registers.g.cs"; - - var sourceCode = $$""" - {{Constants.AutoGeneratedHeader}} - {{Options.All(f => f.NullForgivingOperator).InterpolateCondition("#nullable disable", "#nullable enable")}} - using Microsoft.Extensions.DependencyInjection; - {{string.Join(Environment.NewLine, Options.Select(GenerateUsing))}} - - namespace ResXGenerator.Registration; - - public static class ResXGeneratorRegistrationExtension - { - public static IServiceCollection UsingResXGenerator(this IServiceCollection services) - { - {{string.Join(Environment.NewLine, Options.Select(GenerateRegisterCall)).Indent(2)}} - - return services; - } - } - """; - - return GetOutput(sourceCode); - } - - private static string GenerateRegisterCall(GenFilesNamespace nsur) => $"services.{nsur.NameOfUsingMethodRegistration}();"; - - private static string GenerateUsing(GenFilesNamespace nsur) => $"using {nsur.Namespace};"; + public override GeneratedOutput Generate(ImmutableArray options, CancellationToken cancellationToken = default) + { + Init(options); + + GeneratedFileName = "Aigamo.ResXGenerator.Registers.g.cs"; + + var sourceCode = + $$""" + {{Constants.AutoGeneratedHeader}} + {{Options.All(f => f.NullForgivingOperator).InterpolateCondition("#nullable disable", "#nullable enable")}} + using Microsoft.Extensions.DependencyInjection; + {{string.Join(Environment.NewLine, Options.Select(GenerateUsing))}} + + namespace ResXGenerator.Registration; + + public static class ResXGeneratorRegistrationExtension + { + public static IServiceCollection UsingResXGenerator(this IServiceCollection services) + { + {{string.Join(Environment.NewLine, Options.Select(GenerateRegisterCall)).Indent(2)}} + + return services; + } + } + """; + + return GetOutput(sourceCode); + } + + private static string GenerateRegisterCall(GenFilesNamespace nsur) => $"services.{nsur.NameOfUsingMethodRegistration}();"; + + private static string GenerateUsing(GenFilesNamespace nsur) => $"using {nsur.Namespace};"; } diff --git a/Aigamo.ResXGenerator/Generators/LocalizerRegisterGenerator.cs b/Aigamo.ResXGenerator/Generators/LocalizerRegisterGenerator.cs index 8264bce..0aa3324 100644 --- a/Aigamo.ResXGenerator/Generators/LocalizerRegisterGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/LocalizerRegisterGenerator.cs @@ -6,38 +6,38 @@ namespace Aigamo.ResXGenerator.Generators; public sealed class LocalizerRegisterGenerator : GeneratorBase, ILocalRegisterGenerator { - public override GeneratedOutput Generate(GenFilesNamespace options, CancellationToken cancellationToken = default) - { - Init(options); - - var generatedFileName = $"{options.Namespace}.Registers.g.cs"; - - var sourceCode = GenerateResourceRegistration(options); - - return new GeneratedOutput(generatedFileName, sourceCode, Validator.ErrorsAndWarnings); - } - - private string GenerateResourceRegistration(GenFilesNamespace options) - { - var items = options.Files.Select(f => f.ClassName).ToList(); - - return $$""" - {{Constants.AutoGeneratedHeader}} - {{Options.NullForgivingOperator.InterpolateCondition("#nullable disable", "#nullable enable")}} - using Microsoft.Extensions.DependencyInjection; - - namespace {{options.Namespace}}; - - public static class {{options.SafeNamespaceName}}RegistrationExtensions - { - public static IServiceCollection {{options.NameOfUsingMethodRegistration}}(this IServiceCollection services) - { - {{string.Join(Environment.NewLine, items.Select(GenerateRegistrationCalls)).Indent(2)}} - return services; - } - } - """; - } - - private static string GenerateRegistrationCalls(string className) => $"services.AddSingleton();"; + public override GeneratedOutput Generate(GenFilesNamespace options, CancellationToken cancellationToken = default) + { + Init(options); + + var generatedFileName = $"{options.Namespace}.Registers.g.cs"; + + var sourceCode = GenerateResourceRegistration(options); + + return new GeneratedOutput(generatedFileName, sourceCode, Validator.ErrorsAndWarnings); + } + + private string GenerateResourceRegistration(GenFilesNamespace options) + { + var items = options.Files.Select(f => f.ClassName).ToList(); + + return $$""" + {{Constants.AutoGeneratedHeader}} + {{Options.NullForgivingOperator.InterpolateCondition("#nullable disable", "#nullable enable")}} + using Microsoft.Extensions.DependencyInjection; + + namespace {{options.Namespace}}; + + public static class {{options.SafeNamespaceName}}RegistrationExtensions + { + public static IServiceCollection {{options.NameOfUsingMethodRegistration}}(this IServiceCollection services) + { + {{string.Join(Environment.NewLine, items.Select(GenerateRegistrationCalls)).Indent(2)}} + return services; + } + } + """; + } + + private static string GenerateRegistrationCalls(string className) => $"services.AddSingleton();"; } diff --git a/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs b/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs index 7b51940..4f0e550 100644 --- a/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs @@ -7,69 +7,69 @@ namespace Aigamo.ResXGenerator.Generators; public sealed class ResourceManagerGenerator : GeneratorBase, IResXGenerator { - private StringBuilderGeneratorHelper Helper { get; set; } + private StringBuilderGeneratorHelper Helper { get; set; } - public override GeneratedOutput Generate(GenFileOptions options, CancellationToken cancellationToken = default) - { - Init(options); + public override GeneratedOutput Generate(GenFileOptions options, CancellationToken cancellationToken = default) + { + Init(options); - Helper = new StringBuilderGeneratorHelper(options); + Helper = new StringBuilderGeneratorHelper(options); - Content = Options.GroupedFile.MainFile.File.GetText(cancellationToken); - if (Content is null) - { - GeneratedFileName = Options.GroupedFile.MainFile.File.Path; - Helper.Append("//ERROR reading file:"); - return Helper.GetOutput(GeneratedFileName, Validator); - } + Content = Options.GroupedFile.MainFile.File.GetText(cancellationToken); + if (Content is null) + { + GeneratedFileName = Options.GroupedFile.MainFile.File.Path; + Helper.Append("//ERROR reading file:"); + return Helper.GetOutput(GeneratedFileName, Validator); + } - GeneratedFileName = $"{Options.LocalNamespace}.{Options.ClassName}.g.cs"; + GeneratedFileName = $"{Options.LocalNamespace}.{Options.ClassName}.g.cs"; - Helper.AppendHeader(Options.CustomToolNamespace ?? Options.LocalNamespace); - Helper.AppendResourceManagerUsings(); - Helper.AppendClassHeader(options); - Helper.AppendInnerClass(options, Validator); - GenerateResourceManager(cancellationToken); - Helper.AppendClassFooter(options); + Helper.AppendHeader(Options.CustomToolNamespace ?? Options.LocalNamespace); + Helper.AppendResourceManagerUsings(); + Helper.AppendClassHeader(options); + Helper.AppendInnerClass(options, Validator); + GenerateResourceManager(cancellationToken); + Helper.AppendClassFooter(options); - return Helper.GetOutput(GeneratedFileName, Validator); - } + return Helper.GetOutput(GeneratedFileName, Validator); + } - private void GenerateResourceManager(CancellationToken cancellationToken) - { - Helper.GenerateResourceManagerMembers(Options); + private void GenerateResourceManager(CancellationToken cancellationToken) + { + Helper.GenerateResourceManagerMembers(Options); - var members = ReadResxFile(Content!); + var members = ReadResxFile(Content!); - members?.ForEach(fbi => - { - cancellationToken.ThrowIfCancellationRequested(); - CreateMember(fbi); - }); - } + members?.ForEach(fbi => + { + cancellationToken.ThrowIfCancellationRequested(); + CreateMember(fbi); + }); + } - private void CreateMember(FallBackItem fallbackItem) - { - if (Helper.GenerateMember(fallbackItem, Options, Validator) is not { valid: true } output) return; + private void CreateMember(FallBackItem fallbackItem) + { + if (Helper.GenerateMember(fallbackItem, Options, Validator) is not { valid: true } output) return; - var (_, resourceAccessByName) = output; + var (_, resourceAccessByName) = output; - if (resourceAccessByName) - { - Helper.Append(" => ResourceManager.GetString(nameof("); - Helper.Append(fallbackItem.Key); - Helper.Append("), "); - } - else - { - Helper.Append(@" => ResourceManager.GetString("""); - Helper.Append(fallbackItem.Key.Replace(@"""", @"\""")); - Helper.Append(@""", "); - } + if (resourceAccessByName) + { + Helper.Append(" => ResourceManager.GetString(nameof("); + Helper.Append(fallbackItem.Key); + Helper.Append("), "); + } + else + { + Helper.Append(@" => ResourceManager.GetString("""); + Helper.Append(fallbackItem.Key.Replace(@"""", @"\""")); + Helper.Append(@""", "); + } - Helper.Append(Constants.CultureInfoVariable); - Helper.Append(")"); - Helper.Append(Options.NullForgivingOperators ? "!" : string.Empty); - Helper.AppendLine(";"); - } + Helper.Append(Constants.CultureInfoVariable); + Helper.Append(")"); + Helper.Append(Options.NullForgivingOperators ? "!" : string.Empty); + Helper.AppendLine(";"); + } } diff --git a/Aigamo.ResXGenerator/Models/GenFilesNamespace.cs b/Aigamo.ResXGenerator/Models/GenFilesNamespace.cs index 87aef2e..01c078f 100644 --- a/Aigamo.ResXGenerator/Models/GenFilesNamespace.cs +++ b/Aigamo.ResXGenerator/Models/GenFilesNamespace.cs @@ -5,9 +5,9 @@ namespace Aigamo.ResXGenerator.Models; public record GenFilesNamespace(string Namespace, ImmutableArray Files) { - public string SafeNamespaceName { get; } = Namespace.NamespaceNameCompliant(); + public string SafeNamespaceName { get; } = Namespace.NamespaceNameCompliant(); - public bool NullForgivingOperator => Files.All(f => f.NullForgivingOperators); + public bool NullForgivingOperator => Files.All(f => f.NullForgivingOperators); - public string NameOfUsingMethodRegistration => $"Using{SafeNamespaceName}ResX"; + public string NameOfUsingMethodRegistration => $"Using{SafeNamespaceName}ResX"; } diff --git a/Aigamo.ResXGenerator/Properties/launchSettings.json b/Aigamo.ResXGenerator/Properties/launchSettings.json index de696d6..d04f985 100644 --- a/Aigamo.ResXGenerator/Properties/launchSettings.json +++ b/Aigamo.ResXGenerator/Properties/launchSettings.json @@ -1,11 +1,11 @@ { - "profiles": { - "Debug": { - "commandName": "DebugRoslynComponent", - "targetProject": "..\\Aigamo.ResXGenerator.Tests\\Aigamo.ResXGenerator.Tests.csproj" - }, - "Profil 1": { - "commandName": "Project" - } - } -} \ No newline at end of file + "profiles": { + "Debug": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\Aigamo.ResXGenerator.Tests\\Aigamo.ResXGenerator.Tests.csproj" + }, + "Profil 1": { + "commandName": "Project" + } + } +} diff --git a/Aigamo.ResXGenerator/SourceGenerator.cs b/Aigamo.ResXGenerator/SourceGenerator.cs index 4ba55a1..6131455 100644 --- a/Aigamo.ResXGenerator/SourceGenerator.cs +++ b/Aigamo.ResXGenerator/SourceGenerator.cs @@ -10,173 +10,172 @@ namespace Aigamo.ResXGenerator; [Generator] public class SourceGenerator : IIncrementalGenerator { - public void Initialize(IncrementalGeneratorInitializationContext context) - { - var globalOptions = context.AnalyzerConfigOptionsProvider.Select(GlobalOptions.Select); - - // Note: Each Resx file will get a hash (random guid) so we can easily differentiate in the pipeline when the file changed or just some options - var allResxFiles = context.AdditionalTextsProvider.Where(static af => af.Path.EndsWith(".resx")) - .Select(static (f, _) => new AdditionalTextWithHash(f, Guid.NewGuid())); - - var monitor = allResxFiles - .Collect() - .SelectMany(static (x, _) => GroupResxFiles.Group(x)) - .Combine(globalOptions) - .Combine(context.AnalyzerConfigOptionsProvider) - .Select(static (x, _) => GenFileOptions.Select(x.Left.Left, x.Right, x.Left.Right)) - .Where(static x => x is { IsValid: true, SkipFile: false }); - - //-------------------------------- - // Classic ResxManager - //-------------------------------- - var inputsResXManager = monitor - .Where(static x => x is { GenerationType: GenerationType.ResourceManager, GenerateCode: false }); - GenerateResXFiles(context, inputsResXManager); - - //-------------------------- - //Code generation for strongly typed access to resources - //-------------------------- - var inputsResXCodeGen = monitor - .Where(static x => x is { GenerationType: GenerationType.CodeGeneration } or { GenerationType: GenerationType.ResourceManager, GenerateCode: true }); - GenerateCodeFiles(context, inputsResXCodeGen); - - //-------------------------- - //IStringLocalizer service injection - //Need nugets - // Microsoft.Extensions.DependencyInjection - // Microsoft.Extensions.Localization - // Microsoft.Extensions.Logging - //-------------------------- - var inputResXLocalizer = monitor - .Where(static x => x is { GenerationType: GenerationType.StringLocalizer }) - .Collect() - .SelectMany(static (x, _) => x.GroupBy(f => f.LocalNamespace)) - .Select(static (x, _) => new GenFilesNamespace(x.Key, x.ToImmutableArray())); - GenerateLocalizerRegister(context, inputResXLocalizer); // Generate one localizer per namespace+class combo - } - - private static void GenerateResXFiles(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) - { - var generator = new ResourceManagerGenerator(); - context.RegisterSourceOutput(inputs, (ctx, file) => - { - try - { - var output = generator.Generate(file, ctx.CancellationToken); - output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); - ctx.AddSource(output.FileName, output.SourceCode); - } - catch (Exception e) - { - ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"ResXManager({file.ClassName})", e)); - } - }); - } - - private static void GenerateCodeFiles(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) - { - var generator = new CodeGenerator(); - context.RegisterSourceOutput(inputs, (ctx, file) => - { - try - { - var output = generator.Generate(file, ctx.CancellationToken); - output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); - ctx.AddSource(output.FileName, output.SourceCode); - } - catch (Exception e) - { - ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"ResXManager({file.ClassName})", e)); - } - }); - - var inputsCombos = inputs.Select(static (x, _) => x.GroupedFile); - GenerateResXCombos(context, inputsCombos); - } - - private static void GenerateResXCombos(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider monitor) - { - var generator = new ComboGenerator(); - var detectAllCombosOfResx = monitor - .Collect() - .SelectMany((x, _) => GroupResxFiles.DetectChildCombos(x)); - context.RegisterSourceOutput(detectAllCombosOfResx, (ctx, combo) => - { - try - { - var output = generator.Generate(combo, ctx.CancellationToken); - output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); - ctx.AddSource(output.FileName, output.SourceCode); - } - catch (Exception e) - { - ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, generator.GeneratedFileName, e)); - } - }); - } - - private static void GenerateLocalizerRegister(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) - { - var generator = new LocalizerRegisterGenerator(); - - context.RegisterSourceOutput(inputs, (ctx, ns) => - { - try - { - var output = generator.Generate(ns, ctx.CancellationToken); - output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); - ctx.AddSource(output.FileName, output.SourceCode); - } - catch (Exception e) - { - ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, "Registration of localizers", e)); - } - }); - - GenerateLocalizerResXClasses(context, inputs); - GenerateLocalizerGlobalRegister(context, inputs); - } - - private static void GenerateLocalizerResXClasses(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) - { - var generator = new LocalizerGenerator(); - //Debugger.Launch(); - - var resxFiles = inputs - .SelectMany(static (x, _) => x.Files.GroupBy(f => f.ClassName)) - .Select(static (x, _) => x.First()); - - context.RegisterSourceOutput(resxFiles, (ctx, file) => - { - try - { - var output = generator.Generate(file, ctx.CancellationToken); - output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); - ctx.AddSource(output.FileName, output.SourceCode); - } - catch (Exception e) - { - ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"Error while generating class for {file.ClassName}", e)); - } - }); - } - - private static void GenerateLocalizerGlobalRegister(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) - { - var globalGenerator = new LocalizerGlobalRegisterGenerator(); - var global = inputs.Collect(); - - context.RegisterSourceOutput(global, (ctx, gns) => - { - try - { - var output = globalGenerator.Generate(gns, ctx.CancellationToken); - ctx.AddSource(output.FileName, output.SourceCode); - } - catch (Exception e) - { - ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, " Global localizer registration", e)); - } - }); - } + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var globalOptions = context.AnalyzerConfigOptionsProvider.Select(GlobalOptions.Select); + + // Note: Each Resx file will get a hash (random guid) so we can easily differentiate in the pipeline when the file changed or just some options + var allResxFiles = context.AdditionalTextsProvider.Where(static af => af.Path.EndsWith(".resx")) + .Select(static (f, _) => new AdditionalTextWithHash(f, Guid.NewGuid())); + + var monitor = allResxFiles + .Collect() + .SelectMany(static (x, _) => GroupResxFiles.Group(x)) + .Combine(globalOptions) + .Combine(context.AnalyzerConfigOptionsProvider) + .Select(static (x, _) => GenFileOptions.Select(x.Left.Left, x.Right, x.Left.Right)) + .Where(static x => x is { IsValid: true, SkipFile: false }); + + //-------------------------------- + // Classic ResxManager + //-------------------------------- + var inputsResXManager = monitor + .Where(static x => x is { GenerationType: GenerationType.ResourceManager, GenerateCode: false }); + GenerateResXFiles(context, inputsResXManager); + + //-------------------------- + //Code generation for strongly typed access to resources + //-------------------------- + var inputsResXCodeGen = monitor + .Where(static x => x is { GenerationType: GenerationType.CodeGeneration } or { GenerationType: GenerationType.ResourceManager, GenerateCode: true }); + GenerateCodeFiles(context, inputsResXCodeGen); + + //-------------------------- + //IStringLocalizer service injection + //Need nugets + // Microsoft.Extensions.DependencyInjection + // Microsoft.Extensions.Localization + // Microsoft.Extensions.Logging + //-------------------------- + var inputResXLocalizer = monitor + .Where(static x => x is { GenerationType: GenerationType.StringLocalizer }) + .Collect() + .SelectMany(static (x, _) => x.GroupBy(f => f.LocalNamespace)) + .Select(static (x, _) => new GenFilesNamespace(x.Key, x.ToImmutableArray())); + GenerateLocalizerRegister(context, inputResXLocalizer); // Generate one localizer per namespace+class combo + } + + private static void GenerateResXFiles(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) + { + var generator = new ResourceManagerGenerator(); + context.RegisterSourceOutput(inputs, (ctx, file) => + { + try + { + var output = generator.Generate(file, ctx.CancellationToken); + output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); + ctx.AddSource(output.FileName, output.SourceCode); + } + catch (Exception e) + { + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"ResXManager({file.ClassName})", e)); + } + }); + } + + private static void GenerateCodeFiles(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) + { + var generator = new CodeGenerator(); + context.RegisterSourceOutput(inputs, (ctx, file) => + { + try + { + var output = generator.Generate(file, ctx.CancellationToken); + output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); + ctx.AddSource(output.FileName, output.SourceCode); + } + catch (Exception e) + { + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"ResXManager({file.ClassName})", e)); + } + }); + + var inputsCombos = inputs.Select(static (x, _) => x.GroupedFile); + GenerateResXCombos(context, inputsCombos); + } + + private static void GenerateResXCombos(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider monitor) + { + var generator = new ComboGenerator(); + var detectAllCombosOfResx = monitor + .Collect() + .SelectMany((x, _) => GroupResxFiles.DetectChildCombos(x)); + context.RegisterSourceOutput(detectAllCombosOfResx, (ctx, combo) => + { + try + { + var output = generator.Generate(combo, ctx.CancellationToken); + output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); + ctx.AddSource(output.FileName, output.SourceCode); + } + catch (Exception e) + { + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, generator.GeneratedFileName, e)); + } + }); + } + + private static void GenerateLocalizerRegister(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) + { + var generator = new LocalizerRegisterGenerator(); + + context.RegisterSourceOutput(inputs, (ctx, ns) => + { + try + { + var output = generator.Generate(ns, ctx.CancellationToken); + output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); + ctx.AddSource(output.FileName, output.SourceCode); + } + catch (Exception e) + { + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, "Registration of localizers", e)); + } + }); + + GenerateLocalizerResXClasses(context, inputs); + GenerateLocalizerGlobalRegister(context, inputs); + } + + private static void GenerateLocalizerResXClasses(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) + { + var generator = new LocalizerGenerator(); + + var resxFiles = inputs + .SelectMany(static (x, _) => x.Files.GroupBy(f => f.ClassName)) + .Select(static (x, _) => x.First()); + + context.RegisterSourceOutput(resxFiles, (ctx, file) => + { + try + { + var output = generator.Generate(file, ctx.CancellationToken); + output.ErrorsAndWarnings.ForEach(ctx.ReportDiagnostic); + ctx.AddSource(output.FileName, output.SourceCode); + } + catch (Exception e) + { + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"Error while generating class for {file.ClassName}", e)); + } + }); + } + + private static void GenerateLocalizerGlobalRegister(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) + { + var globalGenerator = new LocalizerGlobalRegisterGenerator(); + var global = inputs.Collect(); + + context.RegisterSourceOutput(global, (ctx, gns) => + { + try + { + var output = globalGenerator.Generate(gns, ctx.CancellationToken); + ctx.AddSource(output.FileName, output.SourceCode); + } + catch (Exception e) + { + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, " Global localizer registration", e)); + } + }); + } } diff --git a/Aigamo.ResXGenerator/Tools/AdditionalTextWithHash.cs b/Aigamo.ResXGenerator/Tools/AdditionalTextWithHash.cs index 5372587..ab5a040 100644 --- a/Aigamo.ResXGenerator/Tools/AdditionalTextWithHash.cs +++ b/Aigamo.ResXGenerator/Tools/AdditionalTextWithHash.cs @@ -4,15 +4,15 @@ namespace Aigamo.ResXGenerator.Tools; public readonly record struct AdditionalTextWithHash(AdditionalText File, Guid Hash) { - public bool Equals(AdditionalTextWithHash other) => File.Path.Equals(other.File.Path) && Hash.Equals(other.Hash); + public bool Equals(AdditionalTextWithHash other) => File.Path.Equals(other.File.Path) && Hash.Equals(other.Hash); - public override int GetHashCode() - { - unchecked - { - return File.GetHashCode() * 397 ^ Hash.GetHashCode(); - } - } + public override int GetHashCode() + { + unchecked + { + return File.GetHashCode() * 397 ^ Hash.GetHashCode(); + } + } - public override string ToString() => $"{nameof(File)}: {File?.Path}, {nameof(Hash)}: {Hash}"; + public override string ToString() => $"{nameof(File)}: {File?.Path}, {nameof(Hash)}: {Hash}"; } diff --git a/Aigamo.ResXGenerator/Tools/Constants.cs b/Aigamo.ResXGenerator/Tools/Constants.cs index e278e45..1f93921 100644 --- a/Aigamo.ResXGenerator/Tools/Constants.cs +++ b/Aigamo.ResXGenerator/Tools/Constants.cs @@ -7,17 +7,16 @@ internal static class Constants public const string AutoGeneratedHeader = """ // ------------------------------------------------------------------------------ - // - // This code was generated by a tool. + // + // This code was generated by a tool. // - // Changes to this file may cause incorrect behavior and will be lost if - // the code is regenerated. - // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // // ------------------------------------------------------------------------------ """; public const string SResourceManagerVariable = "s_resourceManager"; public const string ResourceManagerVariable = "ResourceManager"; public const string CultureInfoVariable = "CultureInfo"; - public const int IndentCount = 4; } diff --git a/Aigamo.ResXGenerator/Tools/CultureInfoCombo.cs b/Aigamo.ResXGenerator/Tools/CultureInfoCombo.cs index b20c054..abcc33a 100644 --- a/Aigamo.ResXGenerator/Tools/CultureInfoCombo.cs +++ b/Aigamo.ResXGenerator/Tools/CultureInfoCombo.cs @@ -19,18 +19,16 @@ public CultureInfoCombo(IReadOnlyList? files) .ToList() ?? []; } - public IReadOnlyList<(string Iso, AdditionalTextWithHash File)> CultureInfos { get;} + public IReadOnlyList<(string Iso, AdditionalTextWithHash File)> CultureInfos { get; } public IReadOnlyList GetDefinedLanguages() => CultureInfos? .Select(x => (x.File, new CultureInfo(x.Iso))) .Select(x => new ComboItem(x.Item2.Name.Replace('-', '_'), x.Item2.LCID, x.File)) .ToList() ?? []; - public bool Equals(CultureInfoCombo other) - { - return (CultureInfos ?? []).Select(x => x.Iso) - .SequenceEqual(other.CultureInfos?.Select(x => x.Iso) ?? []); - } + public bool Equals(CultureInfoCombo other) => + (CultureInfos ?? []).Select(x => x.Iso) + .SequenceEqual(other.CultureInfos?.Select(x => x.Iso) ?? []); public override int GetHashCode() { diff --git a/Aigamo.ResXGenerator/Tools/GenerationType.cs b/Aigamo.ResXGenerator/Tools/GenerationType.cs index 0ea787f..63b3d1c 100644 --- a/Aigamo.ResXGenerator/Tools/GenerationType.cs +++ b/Aigamo.ResXGenerator/Tools/GenerationType.cs @@ -2,8 +2,8 @@ public enum GenerationType { - ResourceManager, - CodeGeneration, - StringLocalizer, - SameAsOuter + ResourceManager, + CodeGeneration, + StringLocalizer, + SameAsOuter } diff --git a/Aigamo.ResXGenerator/Tools/IntegrityValidator.cs b/Aigamo.ResXGenerator/Tools/IntegrityValidator.cs index 8cc01db..ac52baa 100644 --- a/Aigamo.ResXGenerator/Tools/IntegrityValidator.cs +++ b/Aigamo.ResXGenerator/Tools/IntegrityValidator.cs @@ -7,109 +7,109 @@ namespace Aigamo.ResXGenerator.Tools; public class IntegrityValidator { - private readonly HashSet _alreadyAddedMembers = []; - public List ErrorsAndWarnings { get; } = []; - - public bool ValidateMember(FallBackItem fallBackItem, GenFileOptions options) => ValidateMember(fallBackItem, options, options.ClassName); - - public bool ValidateMember(FallBackItem fallBackItem, GenFileOptions options, string className) - { - var valid = true; - - if (!_alreadyAddedMembers.Add(fallBackItem.Key)) - { - ErrorsAndWarnings.Add(Diagnostic.Create( - descriptor: Analyser.DuplicateWarning, - location: Utilities.LocateMember(fallBackItem, options), - fallBackItem.Key - )); - - valid = false; - } - - if (fallBackItem.Key != className) return valid; - - ErrorsAndWarnings.Add(Diagnostic.Create( - descriptor: Analyser.MemberSameAsClassWarning, - location: Utilities.LocateMember(fallBackItem, options), - fallBackItem.Key - )); - - valid = false; - - return valid; - } - - /// - /// Cannot have static members/class with a class instance - /// - /// - /// - public bool ValidateInconsistentModificator(GenFileOptions options) - { - if (options.StaticClass == options.StaticMembers) return true; - - ErrorsAndWarnings.Add(Diagnostic.Create( - descriptor: Analyser.MemberWithStaticError, - location: Location.Create( - filePath: options.GroupedFile.MainFile.File.Path, - textSpan: new TextSpan(), - lineSpan: new LinePositionSpan() - ) - )); - - return false; - } - - public bool ValidateLocalizationModifiers(GenFileOptions options) - { - var valid = true; - if (options.GenerationType != GenerationType.StringLocalizer) return valid; - if (options.StaticClass || options.StaticMembers) - { - ErrorsAndWarnings.Add(Diagnostic.Create( - descriptor: Analyser.LocalizerStaticError, - location: Location.Create( - filePath: options.GroupedFile.MainFile.File.Path, - textSpan: new TextSpan(), - lineSpan: new LinePositionSpan() - ) - )); - valid = false; - } - - if (options.PartialClass) - { - ErrorsAndWarnings.Add(Diagnostic.Create( - descriptor: Analyser.LocalizerPartialError, - location: Location.Create( - filePath: options.GroupedFile.MainFile.File.Path, - textSpan: new TextSpan(), - lineSpan: new LinePositionSpan()))); - - valid = false; - } - - return valid; - } - - public bool ValidateInconsistentNameSpace(GenFileOptions options) - { - if (options.GenerationType != GenerationType.StringLocalizer || - options.CustomToolNamespace.IsNullOrEmpty() || - options.CustomToolNamespace == options.LocalNamespace) - return true; - - ErrorsAndWarnings.Add(Diagnostic.Create( - descriptor: Analyser.LocalizationIncoherentNamespace, - location: Location.Create( - filePath: options.GroupedFile.MainFile.File.Path, - textSpan: new TextSpan(), - lineSpan: new LinePositionSpan() - ) - )); - - return false; - - } + private readonly HashSet _alreadyAddedMembers = []; + public List ErrorsAndWarnings { get; } = []; + + public bool ValidateMember(FallBackItem fallBackItem, GenFileOptions options) => ValidateMember(fallBackItem, options, options.ClassName); + + public bool ValidateMember(FallBackItem fallBackItem, GenFileOptions options, string className) + { + var valid = true; + + if (!_alreadyAddedMembers.Add(fallBackItem.Key)) + { + ErrorsAndWarnings.Add(Diagnostic.Create( + descriptor: Analyser.DuplicateWarning, + location: Utilities.LocateMember(fallBackItem, options), + fallBackItem.Key + )); + + valid = false; + } + + if (fallBackItem.Key != className) return valid; + + ErrorsAndWarnings.Add(Diagnostic.Create( + descriptor: Analyser.MemberSameAsClassWarning, + location: Utilities.LocateMember(fallBackItem, options), + fallBackItem.Key + )); + + valid = false; + + return valid; + } + + /// + /// Cannot have static members/class with a class instance + /// + /// + /// + public bool ValidateInconsistentModificator(GenFileOptions options) + { + if (options.StaticClass == options.StaticMembers) return true; + + ErrorsAndWarnings.Add(Diagnostic.Create( + descriptor: Analyser.MemberWithStaticError, + location: Location.Create( + filePath: options.GroupedFile.MainFile.File.Path, + textSpan: new TextSpan(), + lineSpan: new LinePositionSpan() + ) + )); + + return false; + } + + public bool ValidateLocalizationModifiers(GenFileOptions options) + { + var valid = true; + if (options.GenerationType != GenerationType.StringLocalizer) return valid; + if (options.StaticClass || options.StaticMembers) + { + ErrorsAndWarnings.Add(Diagnostic.Create( + descriptor: Analyser.LocalizerStaticError, + location: Location.Create( + filePath: options.GroupedFile.MainFile.File.Path, + textSpan: new TextSpan(), + lineSpan: new LinePositionSpan() + ) + )); + valid = false; + } + + if (options.PartialClass) + { + ErrorsAndWarnings.Add(Diagnostic.Create( + descriptor: Analyser.LocalizerPartialError, + location: Location.Create( + filePath: options.GroupedFile.MainFile.File.Path, + textSpan: new TextSpan(), + lineSpan: new LinePositionSpan()))); + + valid = false; + } + + return valid; + } + + public bool ValidateInconsistentNameSpace(GenFileOptions options) + { + if (options.GenerationType != GenerationType.StringLocalizer || + options.CustomToolNamespace.IsNullOrEmpty() || + options.CustomToolNamespace == options.LocalNamespace) + return true; + + ErrorsAndWarnings.Add(Diagnostic.Create( + descriptor: Analyser.LocalizationIncoherentNamespace, + location: Location.Create( + filePath: options.GroupedFile.MainFile.File.Path, + textSpan: new TextSpan(), + lineSpan: new LinePositionSpan() + ) + )); + + return false; + + } } diff --git a/Aigamo.ResXGenerator/Tools/NullableAttributes.cs b/Aigamo.ResXGenerator/Tools/NullableAttributes.cs index 29c3148..bb4264d 100644 --- a/Aigamo.ResXGenerator/Tools/NullableAttributes.cs +++ b/Aigamo.ResXGenerator/Tools/NullableAttributes.cs @@ -13,201 +13,201 @@ namespace Aigamo.ResXGenerator.Tools { -#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 - /// Specifies that null is allowed as an input even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public + public #else - internal + internal #endif - sealed class AllowNullAttribute : Attribute - { } + sealed class AllowNullAttribute : Attribute + { } - /// Specifies that null is disallowed as an input even if the corresponding type allows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public + public #else - internal + internal #endif - sealed class DisallowNullAttribute : Attribute - { } + sealed class DisallowNullAttribute : Attribute + { } - /// Specifies that an output may be null even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public + public #else - internal + internal #endif - sealed class MaybeNullAttribute : Attribute - { } + sealed class MaybeNullAttribute : Attribute + { } - /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public + public #else - internal + internal #endif - sealed class NotNullAttribute : Attribute - { } + sealed class NotNullAttribute : Attribute + { } - /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public + public #else - internal + internal #endif - sealed class MaybeNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter may be null. - /// - public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; - - /// Gets the return value condition. - public bool ReturnValue { get; } - } - - /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public + public #else - internal + internal #endif - sealed class NotNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// - public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; - - /// Gets the return value condition. - public bool ReturnValue { get; } - } - - /// Specifies that the output will be non-null if the named parameter is non-null. - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] + sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public + public #else - internal + internal #endif - sealed class NotNullIfNotNullAttribute : Attribute - { - /// Initializes the attribute with the associated parameter name. - /// - /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. - /// - public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; - - /// Gets the associated parameter name. - public string ParameterName { get; } - } - - /// Applied to a method that will never return under any circumstance. - [AttributeUsage(AttributeTargets.Method, Inherited = false)] + sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public + public #else - internal + internal #endif - sealed class DoesNotReturnAttribute : Attribute - { } + sealed class DoesNotReturnAttribute : Attribute + { } - /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. - [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] #if SYSTEM_PRIVATE_CORELIB - public + public #else - internal + internal #endif - sealed class DoesNotReturnIfAttribute : Attribute - { - /// Initializes the attribute with the specified parameter value. - /// - /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to - /// the associated parameter matches this value. - /// - public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; - - /// Gets the condition parameter value. - public bool ParameterValue { get; } - } + sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } #endif -#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 - /// Specifies that the method or property will ensure that the listed field and property members have not-null values. - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] #if SYSTEM_PRIVATE_CORELIB - public + public #else - internal + internal #endif - sealed class MemberNotNullAttribute : Attribute - { - /// Initializes the attribute with a field or property member. - /// - /// The field or property member that is promised to be not-null. - /// - public MemberNotNullAttribute(string member) => Members = new[] { member }; - - /// Initializes the attribute with the list of field and property members. - /// - /// The list of field and property members that are promised to be not-null. - /// - public MemberNotNullAttribute(params string[] members) => Members = members; - - /// Gets field or property member names. - public string[] Members { get; } - } - - /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + sealed class MemberNotNullAttribute : Attribute + { + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = new[] { member }; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } + } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] #if SYSTEM_PRIVATE_CORELIB - public + public #else - internal + internal #endif - sealed class MemberNotNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition and a field or property member. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// - /// - /// The field or property member that is promised to be not-null. - /// - public MemberNotNullWhenAttribute(bool returnValue, string member) - { - ReturnValue = returnValue; - Members = new[] { member }; - } - - /// Initializes the attribute with the specified return value condition and list of field and property members. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// - /// - /// The list of field and property members that are promised to be not-null. - /// - public MemberNotNullWhenAttribute(bool returnValue, params string[] members) - { - ReturnValue = returnValue; - Members = members; - } - - /// Gets the return value condition. - public bool ReturnValue { get; } - - /// Gets field or property member names. - public string[] Members { get; } - } + sealed class MemberNotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + } #endif } diff --git a/Aigamo.ResXGenerator/Tools/RegexDefinitions.cs b/Aigamo.ResXGenerator/Tools/RegexDefinitions.cs index 95b6e9a..240a9ee 100644 --- a/Aigamo.ResXGenerator/Tools/RegexDefinitions.cs +++ b/Aigamo.ResXGenerator/Tools/RegexDefinitions.cs @@ -4,18 +4,18 @@ namespace Aigamo.ResXGenerator.Tools; internal static class RegexDefinitions { - public static readonly Regex ValidMemberNamePattern = new( - pattern: @"^[\p{L}\p{Nl}_][\p{Cf}\p{L}\p{Mc}\p{Mn}\p{Nd}\p{Nl}\p{Pc}]*$", - options: RegexOptions.Compiled | RegexOptions.CultureInvariant - ); + public static readonly Regex ValidMemberNamePattern = new( + pattern: @"^[\p{L}\p{Nl}_][\p{Cf}\p{L}\p{Mc}\p{Mn}\p{Nd}\p{Nl}\p{Pc}]*$", + options: RegexOptions.Compiled | RegexOptions.CultureInvariant + ); - public static readonly Regex InvalidMemberNameSymbols = new( - pattern: @"[^\p{Cf}\p{L}\p{Mc}\p{Mn}\p{Nd}\p{Nl}\p{Pc}]", - options: RegexOptions.Compiled | RegexOptions.CultureInvariant - ); + public static readonly Regex InvalidMemberNameSymbols = new( + pattern: @"[^\p{Cf}\p{L}\p{Mc}\p{Mn}\p{Nd}\p{Nl}\p{Pc}]", + options: RegexOptions.Compiled | RegexOptions.CultureInvariant + ); - public static readonly Regex NewLine = new( - pattern: @"\r\n|\n\r|\n|\r", - options: RegexOptions.Compiled | RegexOptions.CultureInvariant - ); + public static readonly Regex NewLine = new( + pattern: @"\r\n|\n\r|\n|\r", + options: RegexOptions.Compiled | RegexOptions.CultureInvariant + ); } diff --git a/Aigamo.ResXGenerator/Tools/StringBuilderGeneratorHelper.cs b/Aigamo.ResXGenerator/Tools/StringBuilderGeneratorHelper.cs index e1a3555..df992cd 100644 --- a/Aigamo.ResXGenerator/Tools/StringBuilderGeneratorHelper.cs +++ b/Aigamo.ResXGenerator/Tools/StringBuilderGeneratorHelper.cs @@ -8,183 +8,183 @@ namespace Aigamo.ResXGenerator.Tools; public class StringBuilderGeneratorHelper { - private string ContainerClassName { get; set; } - private string Indent { get; set; } = " "; - private StringBuilder Builder { get; } = new(); - - public static string FunctionNamePostFix(IReadOnlyList? definedLanguages) => string.Join("_", definedLanguages?.Select(x => x.LCID) ?? []); - - public GeneratedOutput GetOutput(string fileName, IntegrityValidator validator) => new(fileName, Builder.ToString(), validator.ErrorsAndWarnings); - - public StringBuilderGeneratorHelper() => ContainerClassName = "Helpers"; - - public StringBuilderGeneratorHelper(GenFileOptions options) => ContainerClassName = options.ClassName; - - public void Append(string line) => Builder.Append(line); - public void AppendLine(string line) => Builder.AppendLine(line); - public void AppendFormat(string format, params object[] args) => Builder.AppendFormat(format, args); - public string AppendLanguages(IReadOnlyList languages) - { - var postFix = FunctionNamePostFix(languages); - Builder.Append(postFix); - return postFix; - } - - public void AppendHeader(string @namespace) - { - Builder.AppendLine(Constants.AutoGeneratedHeader); - Builder.AppendLine("#nullable enable"); - Builder.Append("namespace "); - Builder.Append(@namespace); - Builder.AppendLine(";"); - } - - public void AppendInnerClass(GenFileOptions options, IntegrityValidator validator) - { - if (options.InnerClassVisibility == InnerClassVisibility.NotGenerated) return; - - ContainerClassName = string.IsNullOrEmpty(options.InnerClassName) ? "Resources" : options.InnerClassName; - if (!string.IsNullOrEmpty(options.InnerClassInstanceName)) - { - validator.ValidateInconsistentModificator(options); - - Builder.Append(Indent); - Builder.Append("public "); - Builder.Append(ContainerClassName); - Builder.Append(" "); - Builder.Append(options.InnerClassInstanceName); - Builder.AppendLine(" { get; } = new();"); - Builder.AppendLine(); - } - - Builder.Append(Indent); - Builder.Append(GetInnerClassVisibility(options)); - Builder.Append(options.StaticClass ? " static" : string.Empty); - Builder.Append(options.PartialClass ? " partial class " : " class "); - - Builder.AppendLine(ContainerClassName); - Builder.Append(Indent); - Builder.AppendLine("{"); - - Indent += " "; - } - - public void AppendClassHeader(GenFileOptions options) - { - Builder.Append(options.PublicClass ? "public" : "internal"); - Builder.Append(options.StaticClass ? " static" : string.Empty); - Builder.Append(options.PartialClass ? " partial class " : " class "); - Builder.AppendLine(options.ClassName); - Builder.AppendLine("{"); - } - - public void AppendClassFooter(GenFileOptions options) - { - if (options.InnerClassVisibility != InnerClassVisibility.NotGenerated) - Builder.AppendLine(" }"); - - Builder.AppendLine("}"); - } - - public (bool valid, bool resourceAccessByName) GenerateMember(FallBackItem fallbackItem, GenFileOptions options, IntegrityValidator validator) - { - string memberName; - bool resourceAccessByName; - - if (RegexDefinitions.ValidMemberNamePattern.IsMatch(fallbackItem.Key)) - { - memberName = fallbackItem.Key; - resourceAccessByName = true; - } - else - { - memberName = RegexDefinitions.InvalidMemberNameSymbols.Replace(fallbackItem.Key, "_"); - resourceAccessByName = false; - } - - - if (!validator.ValidateMember(fallbackItem, options, ContainerClassName)) return (false, resourceAccessByName); - - Builder.AppendLine(); - - Builder.Append(Indent); - Builder.AppendLine("/// "); - - Builder.Append(Indent); - Builder.Append("/// Looks up a localized string similar to "); - Builder.Append(fallbackItem.Value.ToXmlCommentSafe(Indent)); - Builder.AppendLine("."); - - Builder.Append(Indent); - Builder.AppendLine("/// "); - - Builder.Append(Indent); - Builder.Append("public "); - Builder.Append(options.StaticMembers ? "static " : string.Empty); - Builder.Append("string"); - Builder.Append(options.NullForgivingOperators ? null : "?"); - Builder.Append(" "); - Builder.Append(memberName); - return (true, resourceAccessByName); - } - - public void AppendResourceManagerUsings() - { - Builder.Append("using "); - Builder.Append(Constants.SystemGlobalization); - Builder.AppendLine(";"); - - Builder.Append("using "); - Builder.Append(Constants.SystemResources); - Builder.AppendLine(";"); - - Builder.AppendLine(); - } - - public void AppendCodeUsings() - { - Builder.AppendLine("using static Aigamo.ResXGenerator.Helpers;"); - Builder.AppendLine(); - } - - public void GenerateResourceManagerMembers(GenFileOptions options) - { - Builder.Append(Indent); - Builder.Append("private static "); - Builder.Append(nameof(ResourceManager)); - Builder.Append("? "); - Builder.Append(Constants.SResourceManagerVariable); - Builder.AppendLine(";"); - - Builder.Append(Indent); - Builder.Append("public static "); - Builder.Append(nameof(ResourceManager)); - Builder.Append(" "); - Builder.Append(Constants.ResourceManagerVariable); - Builder.Append(" => "); - Builder.Append(Constants.SResourceManagerVariable); - Builder.Append(" ??= new "); - Builder.Append(nameof(ResourceManager)); - Builder.Append("(\""); - Builder.Append(options.EmbeddedFilename); - Builder.Append("\", typeof("); - Builder.Append(ContainerClassName); - Builder.AppendLine(").Assembly);"); - - Builder.Append(Indent); - Builder.Append("public "); - Builder.Append(options.StaticMembers ? "static " : string.Empty); - Builder.Append(nameof(CultureInfo)); - Builder.Append("? "); - Builder.Append(Constants.CultureInfoVariable); - Builder.AppendLine(" { get; set; }"); - } - - private static string GetInnerClassVisibility(GenFileOptions options) - { - if (options.InnerClassVisibility == InnerClassVisibility.SameAsOuter) - return options.PublicClass ? "public" : "internal"; - - return options.InnerClassVisibility.ToString().ToLowerInvariant(); - } + private string ContainerClassName { get; set; } + private string Indent { get; set; } = "\t"; + private StringBuilder Builder { get; } = new(); + + public static string FunctionNamePostFix(IReadOnlyList? definedLanguages) => string.Join("_", definedLanguages?.Select(x => x.LCID) ?? []); + + public GeneratedOutput GetOutput(string fileName, IntegrityValidator validator) => new(fileName, Builder.ToString(), validator.ErrorsAndWarnings); + + public StringBuilderGeneratorHelper() => ContainerClassName = "Helpers"; + + public StringBuilderGeneratorHelper(GenFileOptions options) => ContainerClassName = options.ClassName; + + public void Append(string line) => Builder.Append(line); + public void AppendLine(string line) => Builder.AppendLine(line); + public void AppendFormat(string format, params object[] args) => Builder.AppendFormat(format, args); + public string AppendLanguages(IReadOnlyList languages) + { + var postFix = FunctionNamePostFix(languages); + Builder.Append(postFix); + return postFix; + } + + public void AppendHeader(string @namespace) + { + Builder.AppendLine(Constants.AutoGeneratedHeader); + Builder.AppendLine("#nullable enable"); + Builder.Append("namespace "); + Builder.Append(@namespace); + Builder.AppendLine(";"); + } + + public void AppendInnerClass(GenFileOptions options, IntegrityValidator validator) + { + if (options.InnerClassVisibility == InnerClassVisibility.NotGenerated) return; + + ContainerClassName = string.IsNullOrEmpty(options.InnerClassName) ? "Resources" : options.InnerClassName; + if (!string.IsNullOrEmpty(options.InnerClassInstanceName)) + { + validator.ValidateInconsistentModificator(options); + + Builder.Append(Indent); + Builder.Append("public "); + Builder.Append(ContainerClassName); + Builder.Append(" "); + Builder.Append(options.InnerClassInstanceName); + Builder.AppendLine(" { get; } = new();"); + Builder.AppendLine(); + } + + Builder.Append(Indent); + Builder.Append(GetInnerClassVisibility(options)); + Builder.Append(options.StaticClass ? " static" : string.Empty); + Builder.Append(options.PartialClass ? " partial class " : " class "); + + Builder.AppendLine(ContainerClassName); + Builder.Append(Indent); + Builder.AppendLine("{"); + + Indent += "\t"; + } + + public void AppendClassHeader(GenFileOptions options) + { + Builder.Append(options.PublicClass ? "public" : "internal"); + Builder.Append(options.StaticClass ? " static" : string.Empty); + Builder.Append(options.PartialClass ? " partial class " : " class "); + Builder.AppendLine(options.ClassName); + Builder.AppendLine("{"); + } + + public void AppendClassFooter(GenFileOptions options) + { + if (options.InnerClassVisibility != InnerClassVisibility.NotGenerated) + Builder.AppendLine("\t}"); + + Builder.AppendLine("}"); + } + + public (bool valid, bool resourceAccessByName) GenerateMember(FallBackItem fallbackItem, GenFileOptions options, IntegrityValidator validator) + { + string memberName; + bool resourceAccessByName; + + if (RegexDefinitions.ValidMemberNamePattern.IsMatch(fallbackItem.Key)) + { + memberName = fallbackItem.Key; + resourceAccessByName = true; + } + else + { + memberName = RegexDefinitions.InvalidMemberNameSymbols.Replace(fallbackItem.Key, "_"); + resourceAccessByName = false; + } + + + if (!validator.ValidateMember(fallbackItem, options, ContainerClassName)) return (false, resourceAccessByName); + + Builder.AppendLine(); + + Builder.Append(Indent); + Builder.AppendLine("/// "); + + Builder.Append(Indent); + Builder.Append("/// Looks up a localized string similar to "); + Builder.Append(fallbackItem.Value.ToXmlCommentSafe(Indent)); + Builder.AppendLine("."); + + Builder.Append(Indent); + Builder.AppendLine("/// "); + + Builder.Append(Indent); + Builder.Append("public "); + Builder.Append(options.StaticMembers ? "static " : string.Empty); + Builder.Append("string"); + Builder.Append(options.NullForgivingOperators ? null : "?"); + Builder.Append(" "); + Builder.Append(memberName); + return (true, resourceAccessByName); + } + + public void AppendResourceManagerUsings() + { + Builder.Append("using "); + Builder.Append(Constants.SystemGlobalization); + Builder.AppendLine(";"); + + Builder.Append("using "); + Builder.Append(Constants.SystemResources); + Builder.AppendLine(";"); + + Builder.AppendLine(); + } + + public void AppendCodeUsings() + { + Builder.AppendLine("using static Aigamo.ResXGenerator.Helpers;"); + Builder.AppendLine(); + } + + public void GenerateResourceManagerMembers(GenFileOptions options) + { + Builder.Append(Indent); + Builder.Append("private static "); + Builder.Append(nameof(ResourceManager)); + Builder.Append("? "); + Builder.Append(Constants.SResourceManagerVariable); + Builder.AppendLine(";"); + + Builder.Append(Indent); + Builder.Append("public static "); + Builder.Append(nameof(ResourceManager)); + Builder.Append(" "); + Builder.Append(Constants.ResourceManagerVariable); + Builder.Append(" => "); + Builder.Append(Constants.SResourceManagerVariable); + Builder.Append(" ??= new "); + Builder.Append(nameof(ResourceManager)); + Builder.Append("(\""); + Builder.Append(options.EmbeddedFilename); + Builder.Append("\", typeof("); + Builder.Append(ContainerClassName); + Builder.AppendLine(").Assembly);"); + + Builder.Append(Indent); + Builder.Append("public "); + Builder.Append(options.StaticMembers ? "static " : string.Empty); + Builder.Append(nameof(CultureInfo)); + Builder.Append("? "); + Builder.Append(Constants.CultureInfoVariable); + Builder.AppendLine(" { get; set; }"); + } + + private static string GetInnerClassVisibility(GenFileOptions options) + { + if (options.InnerClassVisibility == InnerClassVisibility.SameAsOuter) + return options.PublicClass ? "public" : "internal"; + + return options.InnerClassVisibility.ToString().ToLowerInvariant(); + } } diff --git a/Aigamo.ResXGenerator/Tools/Utilities.cs b/Aigamo.ResXGenerator/Tools/Utilities.cs index ac458db..d18eb2c 100644 --- a/Aigamo.ResXGenerator/Tools/Utilities.cs +++ b/Aigamo.ResXGenerator/Tools/Utilities.cs @@ -10,166 +10,166 @@ namespace Aigamo.ResXGenerator; public static class Utilities { - // Code from: https://github.com/dotnet/ResXResourceManager/blob/0ec11bae232151400a5a8ca7b9835ac063c516d0/src/ResXManager.Model/ResourceManager.cs#L267 - - private static bool IsValidLanguageName(string? languageName) - { - try - { - if (languageName.IsNullOrEmpty()) - { - return false; - } - - if (languageName!.StartsWith("qps-", StringComparison.Ordinal)) - { - return true; - } - - var dash = languageName.IndexOf('-'); - if (dash >= 4 || (dash == -1 && languageName.Length >= 4)) - { - return false; - } - - var culture = new CultureInfo(languageName); - - while (!culture.IsNeutralCulture) - { - culture = culture.Parent; - } - - return culture.LCID != 4096; - } - catch - { - return false; - } - } - - // Code from: https://github.com/dotnet/ResXResourceManager/blob/0ec11bae232151400a5a8ca7b9835ac063c516d0/src/ResXManager.Model/ProjectFileExtensions.cs#L77 - - public static string GetBaseName(string filePath) - { - var name = Path.GetFileNameWithoutExtension(filePath); - var innerExtension = Path.GetExtension(name); - var languageName = innerExtension.TrimStart('.'); - - return IsValidLanguageName(languageName) ? Path.GetFileNameWithoutExtension(name) : name; - } - - // Code from: https://github.com/dotnet/ResXResourceManager/blob/c8b5798d760f202a1842a74191e6010c6e8bbbc0/src/ResXManager.VSIX/Visuals/MoveToResourceViewModel.cs#L120 - - public static string GetLocalNamespace( - string? resxPath, - string? targetPath, - string projectPath, - string projectName, - string? rootNamespace - ) - { - try - { - if (resxPath is null) - { - return string.Empty; - } - - var resxFolder = Path.GetDirectoryName(resxPath); - var projectFolder = Path.GetDirectoryName(projectPath); - rootNamespace ??= string.Empty; - - if (resxFolder is null || projectFolder is null) - { - return string.Empty; - } - - var localNamespace = string.Empty; - - if (!string.IsNullOrWhiteSpace(targetPath)) - { - localNamespace = Path.GetDirectoryName(targetPath)! - .Trim(Path.DirectorySeparatorChar) - .Trim(Path.AltDirectorySeparatorChar) - .Replace(Path.DirectorySeparatorChar, '.') - .Replace(Path.AltDirectorySeparatorChar, '.'); - } - else if (resxFolder.StartsWith(projectFolder, StringComparison.OrdinalIgnoreCase)) - { - localNamespace = resxFolder - .Substring(projectFolder.Length) - .Trim(Path.DirectorySeparatorChar) - .Trim(Path.AltDirectorySeparatorChar) - .Replace(Path.DirectorySeparatorChar, '.') - .Replace(Path.AltDirectorySeparatorChar, '.'); - } - - if (string.IsNullOrEmpty(rootNamespace) && string.IsNullOrEmpty(localNamespace)) - { - // If local namespace is empty, e.g file is in root project folder, root namespace set to empty - // fallback to project name as a namespace - localNamespace = SanitizeNamespace(projectName); - } - else - { - localNamespace = (string.IsNullOrEmpty(localNamespace) - ? rootNamespace - : $"{rootNamespace}.{SanitizeNamespace(localNamespace, false)}") - .Trim('.'); - } - - return localNamespace; - } - catch (Exception) - { - return string.Empty; - } - } - - public static string GetClassNameFromPath(string resxFilePath) - { - // Fix issues with files that have names like xxx.aspx.resx - var className = resxFilePath; - while (className.Contains(".")) - { - className = Path.GetFileNameWithoutExtension(className); - } - - return className; - } - - public static string SanitizeNamespace(this string ns, bool sanitizeFirstChar = true) - { - if (string.IsNullOrEmpty(ns)) - return ns; - - // A namespace must contain only alphabetic characters, decimal digits, dots and underscores, and must begin with an alphabetic character or underscore (_) - // In case there are invalid chars we'll use same logic as Visual Studio and replace them with underscore (_) and append underscore (_) if project does not start with alphabetic or underscore (_) - - var sanitizedNs = Regex.Replace(ns, @"[^a-zA-Z0-9_\.]", "_"); - - // Handle folder containing multiple dots, e.g. 'test..test2' or starting, ending with dots - sanitizedNs = Regex.Replace(sanitizedNs, @"\.+", "."); - - if (sanitizeFirstChar) sanitizedNs = sanitizedNs.Trim('.'); - - // Handle namespace starting with digit - if (sanitizeFirstChar) return char.IsDigit(sanitizedNs[0]) ? $"_{sanitizedNs}" : sanitizedNs; - return sanitizedNs; - } - - public static string NamespaceNameCompliant(this string ns) => Regex.Replace(ns, @"[\.\-_]", ""); - - public static Location LocateMember(FallBackItem fallBackItem, GenFileOptions options) - { - var line = fallBackItem.Line; - return Location.Create( - filePath: options.GroupedFile.MainFile.File.Path, - textSpan: new TextSpan(), - lineSpan: new LinePositionSpan( - start: new LinePosition(line.LineNumber - 1, line.LinePosition - 1), - end: new LinePosition(line.LineNumber - 1, line.LinePosition - 1 + fallBackItem.Key.Length) - ) - ); - } + // Code from: https://github.com/dotnet/ResXResourceManager/blob/0ec11bae232151400a5a8ca7b9835ac063c516d0/src/ResXManager.Model/ResourceManager.cs#L267 + + private static bool IsValidLanguageName(string? languageName) + { + try + { + if (languageName.IsNullOrEmpty()) + { + return false; + } + + if (languageName!.StartsWith("qps-", StringComparison.Ordinal)) + { + return true; + } + + var dash = languageName.IndexOf('-'); + if (dash >= 4 || (dash == -1 && languageName.Length >= 4)) + { + return false; + } + + var culture = new CultureInfo(languageName); + + while (!culture.IsNeutralCulture) + { + culture = culture.Parent; + } + + return culture.LCID != 4096; + } + catch + { + return false; + } + } + + // Code from: https://github.com/dotnet/ResXResourceManager/blob/0ec11bae232151400a5a8ca7b9835ac063c516d0/src/ResXManager.Model/ProjectFileExtensions.cs#L77 + + public static string GetBaseName(string filePath) + { + var name = Path.GetFileNameWithoutExtension(filePath); + var innerExtension = Path.GetExtension(name); + var languageName = innerExtension.TrimStart('.'); + + return IsValidLanguageName(languageName) ? Path.GetFileNameWithoutExtension(name) : name; + } + + // Code from: https://github.com/dotnet/ResXResourceManager/blob/c8b5798d760f202a1842a74191e6010c6e8bbbc0/src/ResXManager.VSIX/Visuals/MoveToResourceViewModel.cs#L120 + + public static string GetLocalNamespace( + string? resxPath, + string? targetPath, + string projectPath, + string projectName, + string? rootNamespace + ) + { + try + { + if (resxPath is null) + { + return string.Empty; + } + + var resxFolder = Path.GetDirectoryName(resxPath); + var projectFolder = Path.GetDirectoryName(projectPath); + rootNamespace ??= string.Empty; + + if (resxFolder is null || projectFolder is null) + { + return string.Empty; + } + + var localNamespace = string.Empty; + + if (!string.IsNullOrWhiteSpace(targetPath)) + { + localNamespace = Path.GetDirectoryName(targetPath)! + .Trim(Path.DirectorySeparatorChar) + .Trim(Path.AltDirectorySeparatorChar) + .Replace(Path.DirectorySeparatorChar, '.') + .Replace(Path.AltDirectorySeparatorChar, '.'); + } + else if (resxFolder.StartsWith(projectFolder, StringComparison.OrdinalIgnoreCase)) + { + localNamespace = resxFolder + .Substring(projectFolder.Length) + .Trim(Path.DirectorySeparatorChar) + .Trim(Path.AltDirectorySeparatorChar) + .Replace(Path.DirectorySeparatorChar, '.') + .Replace(Path.AltDirectorySeparatorChar, '.'); + } + + if (string.IsNullOrEmpty(rootNamespace) && string.IsNullOrEmpty(localNamespace)) + { + // If local namespace is empty, e.g file is in root project folder, root namespace set to empty + // fallback to project name as a namespace + localNamespace = SanitizeNamespace(projectName); + } + else + { + localNamespace = (string.IsNullOrEmpty(localNamespace) + ? rootNamespace + : $"{rootNamespace}.{SanitizeNamespace(localNamespace, false)}") + .Trim('.'); + } + + return localNamespace; + } + catch (Exception) + { + return string.Empty; + } + } + + public static string GetClassNameFromPath(string resxFilePath) + { + // Fix issues with files that have names like xxx.aspx.resx + var className = resxFilePath; + while (className.Contains(".")) + { + className = Path.GetFileNameWithoutExtension(className); + } + + return className; + } + + public static string SanitizeNamespace(this string ns, bool sanitizeFirstChar = true) + { + if (string.IsNullOrEmpty(ns)) + return ns; + + // A namespace must contain only alphabetic characters, decimal digits, dots and underscores, and must begin with an alphabetic character or underscore (_) + // In case there are invalid chars we'll use same logic as Visual Studio and replace them with underscore (_) and append underscore (_) if project does not start with alphabetic or underscore (_) + + var sanitizedNs = Regex.Replace(ns, @"[^a-zA-Z0-9_\.]", "_"); + + // Handle folder containing multiple dots, e.g. 'test..test2' or starting, ending with dots + sanitizedNs = Regex.Replace(sanitizedNs, @"\.+", "."); + + if (sanitizeFirstChar) sanitizedNs = sanitizedNs.Trim('.'); + + // Handle namespace starting with digit + if (sanitizeFirstChar) return char.IsDigit(sanitizedNs[0]) ? $"_{sanitizedNs}" : sanitizedNs; + return sanitizedNs; + } + + public static string NamespaceNameCompliant(this string ns) => Regex.Replace(ns, @"[\.\-_]", ""); + + public static Location LocateMember(FallBackItem fallBackItem, GenFileOptions options) + { + var line = fallBackItem.Line; + return Location.Create( + filePath: options.GroupedFile.MainFile.File.Path, + textSpan: new TextSpan(), + lineSpan: new LinePositionSpan( + start: new LinePosition(line.LineNumber - 1, line.LinePosition - 1), + end: new LinePosition(line.LineNumber - 1, line.LinePosition - 1 + fallBackItem.Key.Length) + ) + ); + } } diff --git a/Aigamo.ResXGenerator/build/Aigamo.ResXGenerator.props b/Aigamo.ResXGenerator/build/Aigamo.ResXGenerator.props index 37c4f89..729bf1d 100644 --- a/Aigamo.ResXGenerator/build/Aigamo.ResXGenerator.props +++ b/Aigamo.ResXGenerator/build/Aigamo.ResXGenerator.props @@ -1,39 +1,39 @@ - - $(AdditionalFileItemNames);EmbeddedResource - + + $(AdditionalFileItemNames);EmbeddedResource + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DemoLocalization/DemoLocalization.csproj b/DemoLocalization/DemoLocalization.csproj index 13680ee..5e1f098 100644 --- a/DemoLocalization/DemoLocalization.csproj +++ b/DemoLocalization/DemoLocalization.csproj @@ -1,33 +1,33 @@  - - Exe - net8.0 - enable - enable - latest - false - true - StringLocalizer - + + Exe + net8.0 + enable + enable + latest + false + true + StringLocalizer + - + - - - - - - + + + + + + - - - + + + - - - $([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx - - + + + $([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx + + diff --git a/DemoLocalization/Langage.cs b/DemoLocalization/Langage.cs index f940ee5..cf21717 100644 --- a/DemoLocalization/Langage.cs +++ b/DemoLocalization/Langage.cs @@ -1,4 +1,5 @@ namespace DemoLocalization; + public enum Langage { English, diff --git a/DemoLocalization/Options.cs b/DemoLocalization/Options.cs index 522c604..ae7b681 100644 --- a/DemoLocalization/Options.cs +++ b/DemoLocalization/Options.cs @@ -1,13 +1,14 @@ using CommandLine; namespace DemoLocalization; + public class Options { [Option('l', "locale", Required = false, HelpText = """ - Set the langage of the application values: - - 0 -> English - - 1 -> Danish - - 2 -> French - """)] + Set the langage of the application values: + - 0 -> English + - 1 -> Danish + - 2 -> French + """)] public int Locale { get; set; } } diff --git a/DemoLocalization/Resource.da.resx b/DemoLocalization/Resource.da.resx index 87b88ae..f1729d5 100644 --- a/DemoLocalization/Resource.da.resx +++ b/DemoLocalization/Resource.da.resx @@ -1,123 +1,123 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Hej Verden - - \ No newline at end of file + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Hej Verden + + diff --git a/DemoLocalization/Resource.fr.resx b/DemoLocalization/Resource.fr.resx index c70ab5a..bb5e263 100644 --- a/DemoLocalization/Resource.fr.resx +++ b/DemoLocalization/Resource.fr.resx @@ -1,123 +1,123 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Bonjour monde - + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Bonjour monde + diff --git a/DemoLocalization/Resource.resx b/DemoLocalization/Resource.resx index 8bec740..e0cc03c 100644 --- a/DemoLocalization/Resource.resx +++ b/DemoLocalization/Resource.resx @@ -1,123 +1,123 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Hello World - - \ No newline at end of file + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Hello World + + diff --git a/README.md b/README.md index d07d4ed..5a80b71 100644 --- a/README.md +++ b/README.md @@ -16,60 +16,60 @@ Generated source from [ActivityEntrySortRuleNames.resx](https://github.com/VocaD ```cs // ------------------------------------------------------------------------------ -// -// This code was generated by a tool. +// +// This code was generated by a tool. // -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// // ------------------------------------------------------------------------------ #nullable enable namespace Resources { - using System.Globalization; - using System.Resources; - - public static class ActivityEntrySortRuleNames - { - private static ResourceManager? s_resourceManager; - public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", typeof(ActivityEntrySortRuleNames).Assembly); - public static CultureInfo? CultureInfo { get; set; } - - /// - /// Looks up a localized string similar to Oldest. - /// - public static string? CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo); - - /// - /// Looks up a localized string similar to Newest. - /// - public static string? CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo); - } + using System.Globalization; + using System.Resources; + + public static class ActivityEntrySortRuleNames + { + private static ResourceManager? s_resourceManager; + public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", typeof(ActivityEntrySortRuleNames).Assembly); + public static CultureInfo? CultureInfo { get; set; } + + /// + /// Looks up a localized string similar to Oldest. + /// + public static string? CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo); + + /// + /// Looks up a localized string similar to Newest. + /// + public static string? CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo); + } } ``` ## New in version 3 -- The generator now utilizes the IIncrementalGenerator API to instantly update the generated code, thus giving you instant intellisense. +- The generator now utilizes the IIncrementalGenerator API to instantly update the generated code, thus giving you instant intellisense. -- Added error handling for multiple members of same name, and members that have same name as class. These are clickable in visual studio to lead you to the source of the error, unlike before where they resulted in broken builds and you had to figure out why. +- Added error handling for multiple members of same name, and members that have same name as class. These are clickable in visual studio to lead you to the source of the error, unlike before where they resulted in broken builds and you had to figure out why. -- Namespace naming fixed for resx files in the top level folder. +- Namespace naming fixed for resx files in the top level folder. -- Resx files can now be named with multiple extensions, e.g. myresources.cshtml.resx and will result in class being called myresources. +- Resx files can now be named with multiple extensions, e.g. myresources.cshtml.resx and will result in class being called myresources. -- Added the ability to generate inner classes, partial outer classes and non-static members. Very useful if you want to ensure that only a particular class can use those resources instead of being spread around the codebase. +- Added the ability to generate inner classes, partial outer classes and non-static members. Very useful if you want to ensure that only a particular class can use those resources instead of being spread around the codebase. -- Use same 'Link' setting as msbuild uses to determine embedded file name. +- Use same 'Link' setting as msbuild uses to determine embedded file name. -- Can set a class postfix name +- Can set a class postfix name ## New in version 3.1 -- The generator can now generate code to lookup translations instead of using the 20 year old System.Resources.ResourceManager +- The generator can now generate code to lookup translations instead of using the 20 year old System.Resources.ResourceManager ## New in version 4 -- The generator now include support for IStringLocalizer and static method to register in IServiceCollection (no more need of "magic string" :blush: +- The generator now include support for IStringLocalizer and static method to register in IServiceCollection (no more need of "magic string" :blush: ```c# var local = provider.GetService(); local.Actor // return the value string @@ -82,7 +82,7 @@ var myViewModel = provider.GetService(); public class MyViewModel(IArtistCategoriesNames resources) : IMyViewModel { - public string Value => resources.Actor // retur also the value string + public string Value => resources.Actor // retur also the value string } ``` @@ -97,19 +97,19 @@ Because the dependency injection on somes frameworks like Blazor. Uses the IStri This paramerter is optional The default value is 'ResourceManager' to keep the actual behavior by default. AllowedValues : -- ResourceManager - - When this option choosen the generator will use the classic [System.Resources.ResourceManager](https://learn.microsoft.com/en-us/dotnet/api/system.resources.resourcemanager?view=net-9.0) to get resources string. -- CodeGeneration (You can chose this option to replace the GenerateCode option) - - When this option choosen the generator will generate code to get resources string. See [Generate Code](#Generate-Code) for more details)) +- ResourceManager + - When this option choosen the generator will use the classic [System.Resources.ResourceManager](https://learn.microsoft.com/en-us/dotnet/api/system.resources.resourcemanager?view=net-9.0) to get resources string. +- CodeGeneration (You can chose this option to replace the GenerateCode option) + - When this option choosen the generator will generate code to get resources string. See [Generate Code](#Generate-Code) for more details)) - StringLocalizer - - When this option choosen the generator will generate interface and class to use with [Microsoft.Extensions.Localization](https://docs.microsoft.com/en-us/dotnet/core/extensions/localization) `IStringLocalizer`. To see how to use it see [Using IStringLocalizer](#IStringLocalizer) + - When this option choosen the generator will generate interface and class to use with [Microsoft.Extensions.Localization](https://docs.microsoft.com/en-us/dotnet/core/extensions/localization) `IStringLocalizer`. To see how to use it see [Using IStringLocalizer](#IStringLocalizer) - SameAsOuter - - When this option choosen the generator will use the same generation type as the outer class if any. If no outer class exist it will fallback to 'ResourceManager'. + - When this option choosen the generator will use the same generation type as the outer class if any. If no outer class exist it will fallback to 'ResourceManager'. ```xml - - StringLocalizer - + + StringLocalizer + ``` @@ -117,7 +117,7 @@ or ```xml - + ``` @@ -125,7 +125,7 @@ If you want to apply this globally, use ```xml - StringLocalizer + StringLocalizer ``` @@ -137,9 +137,9 @@ Since version 2.0.0, ResXGenerator generates internal classes by default. You ca ```xml - - true - + + true + ``` @@ -147,7 +147,7 @@ or ```xml - + ``` @@ -155,7 +155,7 @@ If you want to apply this globally, use ```xml - true + true ``` @@ -165,7 +165,7 @@ Use cases: https://github.com/VocaDB/ResXFileCodeGenerator/issues/1. ```xml - true + true ``` @@ -187,9 +187,9 @@ To use generated resources with [Microsoft.Extensions.Localization](https://docs ```xml - - false - + + false + ``` @@ -197,7 +197,7 @@ or globally ```xml - false + false ``` @@ -209,9 +209,9 @@ To extend an existing class, you can make your classes partial. *Not suitable fo ```xml - - true - + + true + ``` @@ -219,7 +219,7 @@ or globally ```xml - true + true ``` @@ -229,9 +229,9 @@ In some rare cases it might be useful for the members to be non-static. *Not sui ```xml - - false - + + false + ``` @@ -239,7 +239,7 @@ or globally ```xml - false + false ``` @@ -252,17 +252,17 @@ This example configuration allows you to use Resources.MyResource in your model, ```xml - - Model - false - false - true - true - public - false - Resources - _Resources - + + Model + false + false + true + true + public + false + Resources + _Resources + ``` @@ -270,7 +270,7 @@ or just the postfix globally ```xml - Model + Model ``` @@ -280,18 +280,18 @@ If your resx files are organized along with code files, it can be quite useful t ```xml - - $([System.String]::Copy('%(FileName).cs')) - MyResources - private - EveryoneLikeMyNaming - false - false - true - - - $([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx - + + $([System.String]::Copy('%(FileName).cs')) + MyResources + private + EveryoneLikeMyNaming + false + false + true + + + $([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx + ``` @@ -299,12 +299,12 @@ or globally ```xml - MyResources - private - EveryoneLikeMyNaming - false - false - true + MyResources + private + EveryoneLikeMyNaming + false + false + true ``` @@ -313,39 +313,39 @@ This example would generate files like this: ```cs // ------------------------------------------------------------------------------ // -// This code was generated by a tool. +// This code was generated by a tool. // -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. // // ------------------------------------------------------------------------------ #nullable enable namespace Resources { - using System.Globalization; - using System.Resources; - - public partial class ActivityEntryModel - { - public MyResources EveryoneLikeMyNaming { get; } = new(); - - private class MyResources - { - private static ResourceManager? s_resourceManager; - public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.ActivityEntryModel", typeof(ActivityEntryModel).Assembly); - public CultureInfo? CultureInfo { get; set; } - - /// - /// Looks up a localized string similar to Oldest. - /// - public string? CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo); - - /// - /// Looks up a localized string similar to Newest. - /// - public string? CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo); - } - } + using System.Globalization; + using System.Resources; + + public partial class ActivityEntryModel + { + public MyResources EveryoneLikeMyNaming { get; } = new(); + + private class MyResources + { + private static ResourceManager? s_resourceManager; + public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.ActivityEntryModel", typeof(ActivityEntryModel).Assembly); + public CultureInfo? CultureInfo { get; set; } + + /// + /// Looks up a localized string similar to Oldest. + /// + public string? CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo); + + /// + /// Looks up a localized string similar to Newest. + /// + public string? CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo); + } + } } ``` @@ -353,11 +353,11 @@ namespace Resources By default inner classes are not generated, unless this setting is one of the following: -- Public -- Internal -- Private -- Protected -- SameAsOuter +- Public +- Internal +- Private +- Protected +- SameAsOuter Case is ignored, so you could use "private". @@ -365,9 +365,9 @@ It is also possible to use "NotGenerated" to override on a file if the global se ```xml - - private - + + private + ``` @@ -375,7 +375,7 @@ or globally ```xml - private + private ``` @@ -385,9 +385,9 @@ By default the inner class is named "Resources", which can be overridden with th ```xml - - MyResources - + + MyResources + ``` @@ -395,7 +395,7 @@ or globally ```xml - MyResources + MyResources ``` @@ -405,9 +405,9 @@ By default no instance is available of the class, but that can be made available ```xml - - EveryoneLikeMyNaming - + + EveryoneLikeMyNaming + ``` @@ -415,7 +415,7 @@ or globally ```xml - EveryoneLikeMyNaming + EveryoneLikeMyNaming ``` @@ -428,44 +428,44 @@ By default the ancient `System.Resources.ResourceManager` is used. Benefits of using `System.Resources.ResourceManager`: -- Supports custom `CultureInfo` -- Languages are only loaded the first time a language is referenced -- Only use memory for the languages used -- Can ship satellite dlls separately +- Supports custom `CultureInfo` +- Languages are only loaded the first time a language is referenced +- Only use memory for the languages used +- Can ship satellite dlls separately Disadvantages of using `System.Resources.ResourceManager` -- The satellite dlls are always lazy loaded, so cold start penalty is high -- Satellite dlls requires that you can scan the dir for which files are available, which can cause issues in some project types -- Loading a satellite dll takes way more memory than just loading the respective strings -- Build time for .resources -> satellite dll can be quite slow (~150msec per file) -- Linker optimization doesn't work, since it cannot know which resources are referenced +- The satellite dlls are always lazy loaded, so cold start penalty is high +- Satellite dlls requires that you can scan the dir for which files are available, which can cause issues in some project types +- Loading a satellite dll takes way more memory than just loading the respective strings +- Build time for .resources -> satellite dll can be quite slow (~150msec per file) +- Linker optimization doesn't work, since it cannot know which resources are referenced Benefits of using `GenerateCode` code generation: -- All languages are placed in the main dll, no more satellite dlls -- Lookup speed is ~600% faster (5ns vs 33ns) -- Zero allocations -- Very small code footprint (about 10 bytes per language, instead of including the entire `System.Resources`) -- Very fast build time -- Because all code is referencing the strings directly, the linker can see which strings are actually used and which are not. -- No cold start penalty -- Smaller combined size of dll (up to 50%, since it doesn't need to store the keys for every single language) +- All languages are placed in the main dll, no more satellite dlls +- Lookup speed is ~600% faster (5ns vs 33ns) +- Zero allocations +- Very small code footprint (about 10 bytes per language, instead of including the entire `System.Resources`) +- Very fast build time +- Because all code is referencing the strings directly, the linker can see which strings are actually used and which are not. +- No cold start penalty +- Smaller combined size of dll (up to 50%, since it doesn't need to store the keys for every single language) Disadvantages of using `GenerateCode` code generation -- Since `CultureInfo` are pre-computed, custom `CultureInfo` are not supported (or rather, they always return the default language) -- Cannot lookup "all" keys (unless using reflection) -- Main dll size increased since it contains all language strings (sometimes, the compiler can pack code strings much better than resource strings and it doesn't need to store the keys) +- Since `CultureInfo` are pre-computed, custom `CultureInfo` are not supported (or rather, they always return the default language) +- Cannot lookup "all" keys (unless using reflection) +- Main dll size increased since it contains all language strings (sometimes, the compiler can pack code strings much better than resource strings and it doesn't need to store the keys) Notice, it is required to set `GenerateResource` to false for all resx files to prevent the built-in resgen.exe from running. ```xml - - true - false - + + true + false + ``` @@ -473,12 +473,12 @@ or globally ```xml - true + true - - false - + + false + ``` @@ -486,9 +486,9 @@ If you get build error MSB3030, add this to your csproj to prevent it from tryin ```xml - - - + + + ``` @@ -500,14 +500,14 @@ Use-case: Linking `.resx` files from outside source (e.g. generated in a localiz ```xml - - Resources\%(FileName)%(Extension) - true - false - - - $([System.IO.Path]::GetFilenameWithoutExtension([System.String]::Copy('%(FileName)'))).resx - + + Resources\%(FileName)%(Extension) + true + false + + + $([System.IO.Path]::GetFilenameWithoutExtension([System.String]::Copy('%(FileName)'))).resx + ``` @@ -515,14 +515,14 @@ You can also use the `TargetPath` to just overwrite the namespace ```xml - - Resources\%(FileName)%(Extension) - true - false - - - $([System.IO.Path]::GetFilenameWithoutExtension([System.String]::Copy('%(FileName)'))).resx - + + Resources\%(FileName)%(Extension) + true + false + + + $([System.IO.Path]::GetFilenameWithoutExtension([System.String]::Copy('%(FileName)'))).resx + ``` @@ -530,9 +530,9 @@ It is also possible to set the namespace using the `CustomToolNamespace` setting ```xml - - MyNamespace.AllMyResourcesAreBelongToYouNamespace - + + MyNamespace.AllMyResourcesAreBelongToYouNamespace + ``` @@ -542,9 +542,9 @@ Individual resx files can also be excluded from being processed by setting the ` ```xml - - true - + + true + ``` @@ -552,7 +552,7 @@ Alternatively it can be set with the attribute `SkipFile="true"`. ```xml - + ``` @@ -571,9 +571,9 @@ Examples for a file ArtistCategoriesNames.resx : using ResXGenerator.Registration; builder.Services - .AddLogging(); - .AddLocalization() - .UsingResXGenerator(); //This will register all resx class in one line of code + .AddLogging(); + .AddLocalization() + .UsingResXGenerator(); //This will register all resx class in one line of code ``` you can also (if you prefer to load ressources by namespace using individual registrations). A class of registration is created by namespace. @@ -581,21 +581,21 @@ you can also (if you prefer to load ressources by namespace using individual reg using MyResourceAssembly; builder.Services - .AddLogging(); - .AddLocalization() - .UsingArtistCategoriesNamesResX(); //This will register all resx class of the namespace in one line of code + .AddLogging(); + .AddLocalization() + .UsingArtistCategoriesNamesResX(); //This will register all resx class of the namespace in one line of code ``` Now simply use the dependency injection to get your resources classes. All interface is in the same namespase as it's resource file. (or configured namespace) ## References -- [Introducing C# Source Generators | .NET Blog](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/) -- [microsoft/CsWin32: A source generator to add a user-defined set of Win32 P/Invoke methods and supporting types to a C# project.](https://github.com/microsoft/cswin32) -- [kenkendk/mdresxfilecodegenerator: Resx Designer Generator](https://github.com/kenkendk/mdresxfilecodegenerator) -- [dotnet/ResXResourceManager: Manage localization of all ResX-Based resources in one central place.](https://github.com/dotnet/ResXResourceManager) -- [roslyn/source-generators.cookbook.md at master · dotnet/roslyn](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.cookbook.md) -- [roslyn/Using Additional Files.md at master · dotnet/roslyn](https://github.com/dotnet/roslyn/blob/master/docs/analyzers/Using%20Additional%20Files.md) -- [ufcpp - YouTube](https://www.youtube.com/channel/UCY-z_9mau6X-Vr4gk2aWtMQ) -- [amis92/csharp-source-generators: A list of C# Source Generators (not necessarily awesome) and associated resources: articles, talks, demos.](https://github.com/amis92/csharp-source-generators) -- [A NuGet package workflow using GitHub Actions | by Andrew Craven | Medium](https://acraven.medium.com/a-nuget-package-workflow-using-github-actions-7da8c6557863) +- [Introducing C# Source Generators | .NET Blog](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/) +- [microsoft/CsWin32: A source generator to add a user-defined set of Win32 P/Invoke methods and supporting types to a C# project.](https://github.com/microsoft/cswin32) +- [kenkendk/mdresxfilecodegenerator: Resx Designer Generator](https://github.com/kenkendk/mdresxfilecodegenerator) +- [dotnet/ResXResourceManager: Manage localization of all ResX-Based resources in one central place.](https://github.com/dotnet/ResXResourceManager) +- [roslyn/source-generators.cookbook.md at master · dotnet/roslyn](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.cookbook.md) +- [roslyn/Using Additional Files.md at master · dotnet/roslyn](https://github.com/dotnet/roslyn/blob/master/docs/analyzers/Using%20Additional%20Files.md) +- [ufcpp - YouTube](https://www.youtube.com/channel/UCY-z_9mau6X-Vr4gk2aWtMQ) +- [amis92/csharp-source-generators: A list of C# Source Generators (not necessarily awesome) and associated resources: articles, talks, demos.](https://github.com/amis92/csharp-source-generators) +- [A NuGet package workflow using GitHub Actions | by Andrew Craven | Medium](https://acraven.medium.com/a-nuget-package-workflow-using-github-actions-7da8c6557863) From d6cdf957ff6f310854fa7a68d47c825a22e68a6a Mon Sep 17 00:00:00 2001 From: FranckSix Date: Mon, 22 Sep 2025 07:57:20 -0400 Subject: [PATCH 04/14] Add comment to the GenerationType enum Add comment to enum GenerationType --- Aigamo.ResXGenerator/Tools/GenerationType.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Aigamo.ResXGenerator/Tools/GenerationType.cs b/Aigamo.ResXGenerator/Tools/GenerationType.cs index 63b3d1c..c6d7688 100644 --- a/Aigamo.ResXGenerator/Tools/GenerationType.cs +++ b/Aigamo.ResXGenerator/Tools/GenerationType.cs @@ -1,9 +1,27 @@ namespace Aigamo.ResXGenerator.Tools; +/// +/// Specifies the strategy used for generating or retrieving localized resources. +/// +/// Use this enumeration to indicate how localization resources should be obtained or generated within an +/// application. The selected value determines whether resources are managed through a resource manager, generated as +/// code, accessed via a string localizer, or inherit the strategy from an outer context. public enum GenerationType { + /// + /// Specifies that a resource manager should be used to handle localization resources. + /// ResourceManager, + /// + /// Provides functionality to obtain resources programmatically. + /// CodeGeneration, + /// + /// Specifies that a string localizer should be used to manage localization resources. + /// StringLocalizer, + /// + /// Specifies that the generation type should be the same as the project option. + /// SameAsOuter } From 1632833ad6ee0b1255386a7ed39bf468f8cae80c Mon Sep 17 00:00:00 2001 From: FranckSix Date: Mon, 22 Sep 2025 08:17:06 -0400 Subject: [PATCH 05/14] Typo in commented code Correction of typo in documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a80b71..e4c9d11 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ var myViewModel = provider.GetService(); public class MyViewModel(IArtistCategoriesNames resources) : IMyViewModel { - public string Value => resources.Actor // retur also the value string + public string Value => resources.Actor // return also the value string } ``` From 38397f2fb856dc5ddbeb377ce7227ed116e98844 Mon Sep 17 00:00:00 2001 From: FranckSix Date: Mon, 22 Sep 2025 08:37:35 -0400 Subject: [PATCH 06/14] Add XML Documentation in GenerationType Report the README to XML comment on code to Intellisense purpose --- Aigamo.ResXGenerator/Tools/GenerationType.cs | 11 +++++++---- README.md | 8 ++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Aigamo.ResXGenerator/Tools/GenerationType.cs b/Aigamo.ResXGenerator/Tools/GenerationType.cs index c6d7688..65e004f 100644 --- a/Aigamo.ResXGenerator/Tools/GenerationType.cs +++ b/Aigamo.ResXGenerator/Tools/GenerationType.cs @@ -9,19 +9,22 @@ public enum GenerationType { /// - /// Specifies that a resource manager should be used to handle localization resources. + /// When this option chosen the generator will use the classic ResourceManager to get resources string. + /// See : ResourceManager. /// ResourceManager, /// - /// Provides functionality to obtain resources programmatically. + /// When this option chosen the generator will generate code to get resources string. See README.md (Generate Code (per file or globally)) for more details /// CodeGeneration, /// - /// Specifies that a string localizer should be used to manage localization resources. + /// When this option chosen the generator will generate interfaces and classes to use with + /// [Microsoft.Extensions.Localization] `IStringLocalizer<T>`. + /// To see how to use it see README.md (Using IStringLocalizer) /// StringLocalizer, /// - /// Specifies that the generation type should be the same as the project option. + /// When this option chosen the generator will use the same generation type as the outer class if any. If no outer class exist it will fall back to 'ResourceManager'. /// SameAsOuter } diff --git a/README.md b/README.md index e4c9d11..9fbd9a4 100644 --- a/README.md +++ b/README.md @@ -98,13 +98,13 @@ This paramerter is optional The default value is 'ResourceManager' to keep the a AllowedValues : - ResourceManager - - When this option choosen the generator will use the classic [System.Resources.ResourceManager](https://learn.microsoft.com/en-us/dotnet/api/system.resources.resourcemanager?view=net-9.0) to get resources string. + - When this option chosen the generator will use the classic [System.Resources.ResourceManager](https://learn.microsoft.com/en-us/dotnet/api/system.resources.resourcemanager?view=net-9.0) to get resources string. - CodeGeneration (You can chose this option to replace the GenerateCode option) - - When this option choosen the generator will generate code to get resources string. See [Generate Code](#Generate-Code) for more details)) + - When this option chosen the generator will generate code to get resources string. See [Generate Code](#Generate-Code) for more details)) - StringLocalizer - - When this option choosen the generator will generate interface and class to use with [Microsoft.Extensions.Localization](https://docs.microsoft.com/en-us/dotnet/core/extensions/localization) `IStringLocalizer`. To see how to use it see [Using IStringLocalizer](#IStringLocalizer) + - When this option chosen the generator will generate interface and class to use with [Microsoft.Extensions.Localization](https://docs.microsoft.com/en-us/dotnet/core/extensions/localization) `IStringLocalizer`. To see how to use it see [Using IStringLocalizer](#IStringLocalizer) - SameAsOuter - - When this option choosen the generator will use the same generation type as the outer class if any. If no outer class exist it will fallback to 'ResourceManager'. + - When this option chosen the generator will use the same generation type as the outer class if any. If no outer class exist it will fall back to 'ResourceManager'. ```xml From 1d13c38fe63fabdb68752499fc8489b474053778 Mon Sep 17 00:00:00 2001 From: FranckSix Date: Mon, 22 Sep 2025 08:50:57 -0400 Subject: [PATCH 07/14] Adding parenthesis for clarity In C#, the * and ^ operators have the same precedence, and evaluation is performed from left to right. So it's not strictly necessary, but I'll add it for clarity.In C#, the * and ^ operators have the same precedence, and evaluation is performed from left to right. So it's not strictly necessary, but I'll add it for clarity. --- Aigamo.ResXGenerator/Tools/GroupedAdditionalFile.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Aigamo.ResXGenerator/Tools/GroupedAdditionalFile.cs b/Aigamo.ResXGenerator/Tools/GroupedAdditionalFile.cs index 6b680d8..eb8fe4d 100644 --- a/Aigamo.ResXGenerator/Tools/GroupedAdditionalFile.cs +++ b/Aigamo.ResXGenerator/Tools/GroupedAdditionalFile.cs @@ -20,7 +20,7 @@ public override int GetHashCode() unchecked { var hashCode = MainFile.GetHashCode(); - SubFiles.ForEach(additionalText => hashCode = hashCode * 397 ^ additionalText.GetHashCode()); + SubFiles.ForEach(additionalText => hashCode = (hashCode * 397) ^ additionalText.GetHashCode()); return hashCode; } } From 931881533569242c02db60bea97b66059f36b2b2 Mon Sep 17 00:00:00 2001 From: FranckSix Date: Mon, 22 Sep 2025 10:55:54 -0400 Subject: [PATCH 08/14] Add an isolated test for CodeGeneration Also reverted the original comportment on Test2.resx --- .../Aigamo.ResXGenerator.Tests.csproj | 3 + .../IntegrationTests/Test2a.da-dk.resx | 107 ++++++++++++++++++ .../IntegrationTests/Test2a.da.resx | 107 ++++++++++++++++++ .../IntegrationTests/Test2a.en-us.resx | 107 ++++++++++++++++++ .../IntegrationTests/Test2a.resx | 107 ++++++++++++++++++ 5 files changed, 431 insertions(+) create mode 100644 Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.da-dk.resx create mode 100644 Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.da.resx create mode 100644 Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.en-us.resx create mode 100644 Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.resx diff --git a/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj b/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj index 670154f..6e39235 100644 --- a/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj +++ b/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj @@ -34,6 +34,9 @@ $([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx + true + + CodeGeneration diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.da-dk.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.da-dk.resx new file mode 100644 index 0000000..0de448d --- /dev/null +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.da-dk.resx @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + OldestDaDK + + + NewestDaDK + + diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.da.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.da.resx new file mode 100644 index 0000000..7d7fd5a --- /dev/null +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.da.resx @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + OldestDa + + + NewestDa + + diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.en-us.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.en-us.resx new file mode 100644 index 0000000..63413e6 --- /dev/null +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.en-us.resx @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + OldestEnUs + + + NewestEnUs + + diff --git a/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.resx b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.resx new file mode 100644 index 0000000..6d5b07a --- /dev/null +++ b/Aigamo.ResXGenerator.Tests/IntegrationTests/Test2a.resx @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Oldest + + + Newest + + From 1cb23f076cd32aaf64c34ee109bb886261e87e76 Mon Sep 17 00:00:00 2001 From: FranckSix Date: Tue, 23 Sep 2025 16:09:20 -0400 Subject: [PATCH 09/14] Correction of somes glitch after using it in real environement The global registration class generated even if no StringLocalizer was present Pass the message of exception instead of exception in case of fatal error Environement.NewLine not recommended for Analyser and Generator. I changed for a constant Adjust the Unshipped.md Add a QUICKSTART.md to have a global view of possibilities the option and simplify configuration for new and current users. --- .../Aigamo.ResXGenerator.csproj | 1 + .../AnalyzerReleases.Unshipped.md | 6 ++++-- .../Extensions/StringExtensions.cs | 4 ++-- .../Generators/LocalizerGenerator.cs | 4 ++-- .../LocalizerGlobalRegisterGenerator.cs | 4 ++-- .../Generators/LocalizerRegisterGenerator.cs | 2 +- Aigamo.ResXGenerator/QUICKSTART.md | 19 +++++++++++++++++++ Aigamo.ResXGenerator/SourceGenerator.cs | 13 +++++++------ Aigamo.ResXGenerator/Tools/Constants.cs | 1 + DemoLocalization/DemoLocalization.csproj | 2 ++ QUICKSTART.md | 19 +++++++++++++++++++ 11 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 Aigamo.ResXGenerator/QUICKSTART.md create mode 100644 QUICKSTART.md diff --git a/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj b/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj index 06ad302..14cb67d 100644 --- a/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj +++ b/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj @@ -48,6 +48,7 @@ + diff --git a/Aigamo.ResXGenerator/AnalyzerReleases.Unshipped.md b/Aigamo.ResXGenerator/AnalyzerReleases.Unshipped.md index 31179f0..4bfc7eb 100644 --- a/Aigamo.ResXGenerator/AnalyzerReleases.Unshipped.md +++ b/Aigamo.ResXGenerator/AnalyzerReleases.Unshipped.md @@ -2,5 +2,7 @@ Rule ID | Category | Severity | Notes --------|----------|----------|-------------------- -AigamoResXGenerator005|ResXGenerator|Warning|LocalizerGenerator -AigamoResXGenerator999|ResXGenerator|Error|SourceGenerator +AigamoResXGenerator004 | ResXGenerator | Warning | LocalizerGenerator +AigamoResXGenerator005 | ResXGenerator | Warning | LocalizerGenerator +AigamoResXGenerator006 | ResXGenerator | Warning | LocalizerGenerator +AigamoResXGenerator999 | ResXGenerator | Error | SourceGenerator diff --git a/Aigamo.ResXGenerator/Extensions/StringExtensions.cs b/Aigamo.ResXGenerator/Extensions/StringExtensions.cs index 28ea526..7757a36 100644 --- a/Aigamo.ResXGenerator/Extensions/StringExtensions.cs +++ b/Aigamo.ResXGenerator/Extensions/StringExtensions.cs @@ -11,12 +11,12 @@ internal static class StringExtensions public static string ToXmlCommentSafe(this string input, string indent) { var lines = HttpUtility.HtmlEncode(input.Trim()).GetCodeLines(); - return string.Join($"{Environment.NewLine}{indent}/// ", lines); + return string.Join($"{Constants.NewLine}{indent}/// ", lines); } public static string Indent(this string input, int level = 1) { var indent = new string('\t', level); - return string.Join($"{Environment.NewLine}{indent}", input.GetCodeLines()); + return string.Join($"{Constants.NewLine}{indent}", input.GetCodeLines()); } public static IEnumerable GetCodeLines(this string input) => RegexDefinitions.NewLine.Split(input); diff --git a/Aigamo.ResXGenerator/Generators/LocalizerGenerator.cs b/Aigamo.ResXGenerator/Generators/LocalizerGenerator.cs index b000844..75493db 100644 --- a/Aigamo.ResXGenerator/Generators/LocalizerGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/LocalizerGenerator.cs @@ -40,12 +40,12 @@ namespace {{Options.LocalNamespace}}; public interface I{{Options.ClassName}} { - {{string.Join(Environment.NewLine, fallback.Select(GenerateInterfaceMembers)).Indent()}} + {{string.Join(Constants.NewLine, fallback.Select(GenerateInterfaceMembers)).Indent()}} } {{Options.PublicClass.InterpolateCondition("public", "internal")}} class {{Options.ClassName}}(IStringLocalizer<{{Options.ClassName}}> stringLocalizer) : I{{Options.ClassName}} { - {{string.Join(Environment.NewLine, fallback.Select(GenerateMembers)).Indent()}} + {{string.Join(Constants.NewLine, fallback.Select(GenerateMembers)).Indent()}} } """; } diff --git a/Aigamo.ResXGenerator/Generators/LocalizerGlobalRegisterGenerator.cs b/Aigamo.ResXGenerator/Generators/LocalizerGlobalRegisterGenerator.cs index 49f49f8..78c62d9 100644 --- a/Aigamo.ResXGenerator/Generators/LocalizerGlobalRegisterGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/LocalizerGlobalRegisterGenerator.cs @@ -18,7 +18,7 @@ public override GeneratedOutput Generate(ImmutableArray optio {{Constants.AutoGeneratedHeader}} {{Options.All(f => f.NullForgivingOperator).InterpolateCondition("#nullable disable", "#nullable enable")}} using Microsoft.Extensions.DependencyInjection; - {{string.Join(Environment.NewLine, Options.Select(GenerateUsing))}} + {{string.Join(Constants.NewLine, Options.Select(GenerateUsing))}} namespace ResXGenerator.Registration; @@ -26,7 +26,7 @@ public static class ResXGeneratorRegistrationExtension { public static IServiceCollection UsingResXGenerator(this IServiceCollection services) { - {{string.Join(Environment.NewLine, Options.Select(GenerateRegisterCall)).Indent(2)}} + {{string.Join(Constants.NewLine, Options.Select(GenerateRegisterCall)).Indent(2)}} return services; } diff --git a/Aigamo.ResXGenerator/Generators/LocalizerRegisterGenerator.cs b/Aigamo.ResXGenerator/Generators/LocalizerRegisterGenerator.cs index 0aa3324..cb1c75e 100644 --- a/Aigamo.ResXGenerator/Generators/LocalizerRegisterGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/LocalizerRegisterGenerator.cs @@ -32,7 +32,7 @@ public static class {{options.SafeNamespaceName}}RegistrationExtensions { public static IServiceCollection {{options.NameOfUsingMethodRegistration}}(this IServiceCollection services) { - {{string.Join(Environment.NewLine, items.Select(GenerateRegistrationCalls)).Indent(2)}} + {{string.Join(Constants.NewLine, items.Select(GenerateRegistrationCalls)).Indent(2)}} return services; } } diff --git a/Aigamo.ResXGenerator/QUICKSTART.md b/Aigamo.ResXGenerator/QUICKSTART.md new file mode 100644 index 0000000..7036b05 --- /dev/null +++ b/Aigamo.ResXGenerator/QUICKSTART.md @@ -0,0 +1,19 @@ +# Options + +|Option|Global|Accepted values |Compatibilty | +|------|------|-------------------------|--------------| +|GenerationType|ResXGenerator_GenerationType|- **ResourceManager**
- CodeGeneration
- StringLocalizer
- SameAsOuter|All| +|PublicClass|ResXGenerator_PublicClass|- true
- **false**|All| +|StaticClass|ResXGenerator_StaticClass|- **true**
- false|All| +|PartialClass|ResXGenerator_PartialClass|- true
- **false**|All except StringLocalizer| +|StaticMembers|ResXGenerator_StaticMembers|- **true**
- false|All except StringLocalizer| +|NullForgivingOperators|ResXGenerator_NullForgivingOperators|- true
- **false**|All except StringLocalizer| +|InnerClassVisibility|ResXGenerator_InnerClassVisibility|-public
- internal
- private
- protected
- sameasouter
- **notgenerated**|All except StringLocalizer| +|InnerClassName|ResXGenerator_InnerClassName|Any valid C# identifier| +|InnerClassInstanceName|ResXGenerator_InnerClassInstanceName|Any valid C# identifier| +|ClassNamePostfix|ResXGenerator_ClassNamePostfix|Any valid C# identifier|All except StringLocalizer| +|GenerateCode*|ResXGenerator_GenerateCode|- true
- **false**|All except StringLocalizer| +|CustomToolNamespace|ResXGenerator_CustomToolNamespace|Any valid C# namespace|All except StringLocalizer| +|SkipFile| |- true
- **false**|All| + +\* *Could be replaced by the option GenerationType with the value CodeGeneration* diff --git a/Aigamo.ResXGenerator/SourceGenerator.cs b/Aigamo.ResXGenerator/SourceGenerator.cs index 6131455..6604371 100644 --- a/Aigamo.ResXGenerator/SourceGenerator.cs +++ b/Aigamo.ResXGenerator/SourceGenerator.cs @@ -68,7 +68,7 @@ private static void GenerateResXFiles(IncrementalGeneratorInitializationContext } catch (Exception e) { - ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"ResXManager({file.ClassName})", e)); + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"ResXManager({file.ClassName})", e.Message)); } }); } @@ -86,7 +86,7 @@ private static void GenerateCodeFiles(IncrementalGeneratorInitializationContext } catch (Exception e) { - ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"ResXManager({file.ClassName})", e)); + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"ResXManager({file.ClassName})", e.Message)); } }); @@ -110,7 +110,7 @@ private static void GenerateResXCombos(IncrementalGeneratorInitializationContext } catch (Exception e) { - ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, generator.GeneratedFileName, e)); + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, generator.GeneratedFileName, e.Message)); } }); } @@ -129,7 +129,7 @@ private static void GenerateLocalizerRegister(IncrementalGeneratorInitialization } catch (Exception e) { - ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, "Registration of localizers", e)); + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, "Registration of localizers", e.Message)); } }); @@ -155,7 +155,7 @@ private static void GenerateLocalizerResXClasses(IncrementalGeneratorInitializat } catch (Exception e) { - ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"Error while generating class for {file.ClassName}", e)); + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, $"Error while generating class for {file.ClassName}", e.Message)); } }); } @@ -167,6 +167,7 @@ private static void GenerateLocalizerGlobalRegister(IncrementalGeneratorInitiali context.RegisterSourceOutput(global, (ctx, gns) => { + if (gns.Length == 0) return; try { var output = globalGenerator.Generate(gns, ctx.CancellationToken); @@ -174,7 +175,7 @@ private static void GenerateLocalizerGlobalRegister(IncrementalGeneratorInitiali } catch (Exception e) { - ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, " Global localizer registration", e)); + ctx.ReportDiagnostic(Diagnostic.Create(Analyser.FatalError, Location.None, " Global localizer registration", e.Message)); } }); } diff --git a/Aigamo.ResXGenerator/Tools/Constants.cs b/Aigamo.ResXGenerator/Tools/Constants.cs index 1f93921..6cabec6 100644 --- a/Aigamo.ResXGenerator/Tools/Constants.cs +++ b/Aigamo.ResXGenerator/Tools/Constants.cs @@ -19,4 +19,5 @@ internal static class Constants public const string SResourceManagerVariable = "s_resourceManager"; public const string ResourceManagerVariable = "ResourceManager"; public const string CultureInfoVariable = "CultureInfo"; + public const string NewLine = "\n"; } diff --git a/DemoLocalization/DemoLocalization.csproj b/DemoLocalization/DemoLocalization.csproj index 5e1f098..fec0689 100644 --- a/DemoLocalization/DemoLocalization.csproj +++ b/DemoLocalization/DemoLocalization.csproj @@ -9,6 +9,8 @@ false true StringLocalizer + false + false diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..7036b05 --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,19 @@ +# Options + +|Option|Global|Accepted values |Compatibilty | +|------|------|-------------------------|--------------| +|GenerationType|ResXGenerator_GenerationType|- **ResourceManager**
- CodeGeneration
- StringLocalizer
- SameAsOuter|All| +|PublicClass|ResXGenerator_PublicClass|- true
- **false**|All| +|StaticClass|ResXGenerator_StaticClass|- **true**
- false|All| +|PartialClass|ResXGenerator_PartialClass|- true
- **false**|All except StringLocalizer| +|StaticMembers|ResXGenerator_StaticMembers|- **true**
- false|All except StringLocalizer| +|NullForgivingOperators|ResXGenerator_NullForgivingOperators|- true
- **false**|All except StringLocalizer| +|InnerClassVisibility|ResXGenerator_InnerClassVisibility|-public
- internal
- private
- protected
- sameasouter
- **notgenerated**|All except StringLocalizer| +|InnerClassName|ResXGenerator_InnerClassName|Any valid C# identifier| +|InnerClassInstanceName|ResXGenerator_InnerClassInstanceName|Any valid C# identifier| +|ClassNamePostfix|ResXGenerator_ClassNamePostfix|Any valid C# identifier|All except StringLocalizer| +|GenerateCode*|ResXGenerator_GenerateCode|- true
- **false**|All except StringLocalizer| +|CustomToolNamespace|ResXGenerator_CustomToolNamespace|Any valid C# namespace|All except StringLocalizer| +|SkipFile| |- true
- **false**|All| + +\* *Could be replaced by the option GenerationType with the value CodeGeneration* From 9dfae4b569b0fad1a7f46ae3e68c680a9c25c0ca Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Sun, 19 Oct 2025 09:50:05 +0900 Subject: [PATCH 10/14] Remove #nullable disable directives --- Aigamo.ResXGenerator/Generators/CodeGenerator.cs | 1 - Aigamo.ResXGenerator/Generators/ComboGenerator.cs | 1 - Aigamo.ResXGenerator/Generators/GeneratorBase.cs | 1 - Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs | 1 - 4 files changed, 4 deletions(-) diff --git a/Aigamo.ResXGenerator/Generators/CodeGenerator.cs b/Aigamo.ResXGenerator/Generators/CodeGenerator.cs index c8cd498..b44c924 100644 --- a/Aigamo.ResXGenerator/Generators/CodeGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/CodeGenerator.cs @@ -3,7 +3,6 @@ using Aigamo.ResXGenerator.Models; using Aigamo.ResXGenerator.Tools; using Microsoft.CodeAnalysis.CSharp; -#nullable disable namespace Aigamo.ResXGenerator.Generators; diff --git a/Aigamo.ResXGenerator/Generators/ComboGenerator.cs b/Aigamo.ResXGenerator/Generators/ComboGenerator.cs index aab4bcf..f603f9b 100644 --- a/Aigamo.ResXGenerator/Generators/ComboGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/ComboGenerator.cs @@ -2,7 +2,6 @@ using Aigamo.ResXGenerator.Extensions; using Aigamo.ResXGenerator.Models; using Aigamo.ResXGenerator.Tools; -#nullable disable namespace Aigamo.ResXGenerator.Generators; diff --git a/Aigamo.ResXGenerator/Generators/GeneratorBase.cs b/Aigamo.ResXGenerator/Generators/GeneratorBase.cs index 12fd0e6..03cd162 100644 --- a/Aigamo.ResXGenerator/Generators/GeneratorBase.cs +++ b/Aigamo.ResXGenerator/Generators/GeneratorBase.cs @@ -2,7 +2,6 @@ using Aigamo.ResXGenerator.Models; using Aigamo.ResXGenerator.Tools; using Microsoft.CodeAnalysis.Text; -#nullable disable namespace Aigamo.ResXGenerator.Generators; diff --git a/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs b/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs index 4f0e550..28ac873 100644 --- a/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs @@ -1,7 +1,6 @@ using Aigamo.ResXGenerator.Extensions; using Aigamo.ResXGenerator.Models; using Aigamo.ResXGenerator.Tools; -#nullable disable namespace Aigamo.ResXGenerator.Generators; From 2b142f4235fd308004ae47252a95deb6ad0b7ed8 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Sun, 19 Oct 2025 09:57:00 +0900 Subject: [PATCH 11/14] Restore AppendLineLF extension method --- .../Extensions/StringBuilderExtensions.cs | 17 +++++++ .../Generators/CodeGenerator.cs | 2 +- .../Generators/ComboGenerator.cs | 16 +++---- .../Generators/ResourceManagerGenerator.cs | 2 +- .../Tools/StringBuilderGeneratorHelper.cs | 48 +++++++++---------- 5 files changed, 51 insertions(+), 34 deletions(-) create mode 100644 Aigamo.ResXGenerator/Extensions/StringBuilderExtensions.cs diff --git a/Aigamo.ResXGenerator/Extensions/StringBuilderExtensions.cs b/Aigamo.ResXGenerator/Extensions/StringBuilderExtensions.cs new file mode 100644 index 0000000..cafeccc --- /dev/null +++ b/Aigamo.ResXGenerator/Extensions/StringBuilderExtensions.cs @@ -0,0 +1,17 @@ +using System.Text; + +namespace Aigamo.ResXGenerator.Extensions; + +internal static class StringBuilderExtensions +{ + public static void AppendLineLF(this StringBuilder builder) + { + builder.Append('\n'); + } + + public static void AppendLineLF(this StringBuilder builder, string value) + { + builder.Append(value); + builder.AppendLineLF(); + } +} diff --git a/Aigamo.ResXGenerator/Generators/CodeGenerator.cs b/Aigamo.ResXGenerator/Generators/CodeGenerator.cs index b44c924..6c95164 100644 --- a/Aigamo.ResXGenerator/Generators/CodeGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/CodeGenerator.cs @@ -76,7 +76,7 @@ private void GenerateCode(CancellationToken cancellationToken) Helper.Append(SymbolDisplay.FormatLiteral(langValue, true)); }); - Helper.AppendLine(");"); + Helper.AppendLineLF(");"); }); } } diff --git a/Aigamo.ResXGenerator/Generators/ComboGenerator.cs b/Aigamo.ResXGenerator/Generators/ComboGenerator.cs index f603f9b..ef4d56b 100644 --- a/Aigamo.ResXGenerator/Generators/ComboGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/ComboGenerator.cs @@ -39,8 +39,8 @@ public override GeneratedOutput Generate(CultureInfoCombo options, CancellationT Helper.AppendHeader("Aigamo.ResXGenerator"); - Helper.AppendLine("internal static partial class Helpers"); - Helper.AppendLine("{"); + Helper.AppendLineLF("internal static partial class Helpers"); + Helper.AppendLineLF("{"); Helper.Append("\tpublic static string GetString_"); var functionNamePostFix = Helper.AppendLanguages(definedLanguages); @@ -56,8 +56,8 @@ public override GeneratedOutput Generate(CultureInfoCombo options, CancellationT Helper.Append(") => "); Helper.Append(Constants.SystemGlobalization); - Helper.AppendLine(".CultureInfo.CurrentUICulture.LCID switch"); - Helper.AppendLine("\t{"); + Helper.AppendLineLF(".CultureInfo.CurrentUICulture.LCID switch"); + Helper.AppendLineLF("\t{"); var already = new HashSet(); definedLanguages.ForEach(ci => { @@ -68,12 +68,12 @@ public override GeneratedOutput Generate(CultureInfoCombo options, CancellationT already.Add(parent); return $"\t\t{parent} => {ci.Name.Replace('-', '_')},"; }) - .ForEach(l => Helper.AppendLine(l)); + .ForEach(l => Helper.AppendLineLF(l)); }); - Helper.AppendLine("\t\t_ => fallback"); - Helper.AppendLine("\t};"); - Helper.AppendLine("}"); + Helper.AppendLineLF("\t\t_ => fallback"); + Helper.AppendLineLF("\t};"); + Helper.AppendLineLF("}"); return Helper.GetOutput(GeneratedFileName, Validator); } diff --git a/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs b/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs index 28ac873..ee33668 100644 --- a/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs @@ -69,6 +69,6 @@ private void CreateMember(FallBackItem fallbackItem) Helper.Append(Constants.CultureInfoVariable); Helper.Append(")"); Helper.Append(Options.NullForgivingOperators ? "!" : string.Empty); - Helper.AppendLine(";"); + Helper.AppendLineLF(";"); } } diff --git a/Aigamo.ResXGenerator/Tools/StringBuilderGeneratorHelper.cs b/Aigamo.ResXGenerator/Tools/StringBuilderGeneratorHelper.cs index df992cd..6128e7d 100644 --- a/Aigamo.ResXGenerator/Tools/StringBuilderGeneratorHelper.cs +++ b/Aigamo.ResXGenerator/Tools/StringBuilderGeneratorHelper.cs @@ -21,7 +21,7 @@ public class StringBuilderGeneratorHelper public StringBuilderGeneratorHelper(GenFileOptions options) => ContainerClassName = options.ClassName; public void Append(string line) => Builder.Append(line); - public void AppendLine(string line) => Builder.AppendLine(line); + public void AppendLineLF(string line) => Builder.AppendLineLF(line); public void AppendFormat(string format, params object[] args) => Builder.AppendFormat(format, args); public string AppendLanguages(IReadOnlyList languages) { @@ -32,11 +32,11 @@ public string AppendLanguages(IReadOnlyList languages) public void AppendHeader(string @namespace) { - Builder.AppendLine(Constants.AutoGeneratedHeader); - Builder.AppendLine("#nullable enable"); + Builder.AppendLineLF(Constants.AutoGeneratedHeader); + Builder.AppendLineLF("#nullable enable"); Builder.Append("namespace "); Builder.Append(@namespace); - Builder.AppendLine(";"); + Builder.AppendLineLF(";"); } public void AppendInnerClass(GenFileOptions options, IntegrityValidator validator) @@ -53,8 +53,8 @@ public void AppendInnerClass(GenFileOptions options, IntegrityValidator validato Builder.Append(ContainerClassName); Builder.Append(" "); Builder.Append(options.InnerClassInstanceName); - Builder.AppendLine(" { get; } = new();"); - Builder.AppendLine(); + Builder.AppendLineLF(" { get; } = new();"); + Builder.AppendLineLF(); } Builder.Append(Indent); @@ -62,9 +62,9 @@ public void AppendInnerClass(GenFileOptions options, IntegrityValidator validato Builder.Append(options.StaticClass ? " static" : string.Empty); Builder.Append(options.PartialClass ? " partial class " : " class "); - Builder.AppendLine(ContainerClassName); + Builder.AppendLineLF(ContainerClassName); Builder.Append(Indent); - Builder.AppendLine("{"); + Builder.AppendLineLF("{"); Indent += "\t"; } @@ -74,16 +74,16 @@ public void AppendClassHeader(GenFileOptions options) Builder.Append(options.PublicClass ? "public" : "internal"); Builder.Append(options.StaticClass ? " static" : string.Empty); Builder.Append(options.PartialClass ? " partial class " : " class "); - Builder.AppendLine(options.ClassName); - Builder.AppendLine("{"); + Builder.AppendLineLF(options.ClassName); + Builder.AppendLineLF("{"); } public void AppendClassFooter(GenFileOptions options) { if (options.InnerClassVisibility != InnerClassVisibility.NotGenerated) - Builder.AppendLine("\t}"); + Builder.AppendLineLF("\t}"); - Builder.AppendLine("}"); + Builder.AppendLineLF("}"); } public (bool valid, bool resourceAccessByName) GenerateMember(FallBackItem fallbackItem, GenFileOptions options, IntegrityValidator validator) @@ -105,18 +105,18 @@ public void AppendClassFooter(GenFileOptions options) if (!validator.ValidateMember(fallbackItem, options, ContainerClassName)) return (false, resourceAccessByName); - Builder.AppendLine(); + Builder.AppendLineLF(); Builder.Append(Indent); - Builder.AppendLine("/// "); + Builder.AppendLineLF("/// "); Builder.Append(Indent); Builder.Append("/// Looks up a localized string similar to "); Builder.Append(fallbackItem.Value.ToXmlCommentSafe(Indent)); - Builder.AppendLine("."); + Builder.AppendLineLF("."); Builder.Append(Indent); - Builder.AppendLine("/// "); + Builder.AppendLineLF("/// "); Builder.Append(Indent); Builder.Append("public "); @@ -132,19 +132,19 @@ public void AppendResourceManagerUsings() { Builder.Append("using "); Builder.Append(Constants.SystemGlobalization); - Builder.AppendLine(";"); + Builder.AppendLineLF(";"); Builder.Append("using "); Builder.Append(Constants.SystemResources); - Builder.AppendLine(";"); + Builder.AppendLineLF(";"); - Builder.AppendLine(); + Builder.AppendLineLF(); } public void AppendCodeUsings() { - Builder.AppendLine("using static Aigamo.ResXGenerator.Helpers;"); - Builder.AppendLine(); + Builder.AppendLineLF("using static Aigamo.ResXGenerator.Helpers;"); + Builder.AppendLineLF(); } public void GenerateResourceManagerMembers(GenFileOptions options) @@ -154,7 +154,7 @@ public void GenerateResourceManagerMembers(GenFileOptions options) Builder.Append(nameof(ResourceManager)); Builder.Append("? "); Builder.Append(Constants.SResourceManagerVariable); - Builder.AppendLine(";"); + Builder.AppendLineLF(";"); Builder.Append(Indent); Builder.Append("public static "); @@ -169,7 +169,7 @@ public void GenerateResourceManagerMembers(GenFileOptions options) Builder.Append(options.EmbeddedFilename); Builder.Append("\", typeof("); Builder.Append(ContainerClassName); - Builder.AppendLine(").Assembly);"); + Builder.AppendLineLF(").Assembly);"); Builder.Append(Indent); Builder.Append("public "); @@ -177,7 +177,7 @@ public void GenerateResourceManagerMembers(GenFileOptions options) Builder.Append(nameof(CultureInfo)); Builder.Append("? "); Builder.Append(Constants.CultureInfoVariable); - Builder.AppendLine(" { get; set; }"); + Builder.AppendLineLF(" { get; set; }"); } private static string GetInnerClassVisibility(GenFileOptions options) From b5d2cd92ee99b42c44e24c8dd1c242b63b452943 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Sun, 19 Oct 2025 10:12:29 +0900 Subject: [PATCH 12/14] Simplify file content null check --- Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs b/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs index ee33668..f7a9041 100644 --- a/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs +++ b/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs @@ -14,13 +14,13 @@ public override GeneratedOutput Generate(GenFileOptions options, CancellationTok Helper = new StringBuilderGeneratorHelper(options); - Content = Options.GroupedFile.MainFile.File.GetText(cancellationToken); - if (Content is null) + if (Options.GroupedFile.MainFile.File.GetText(cancellationToken) is not { } content) { GeneratedFileName = Options.GroupedFile.MainFile.File.Path; Helper.Append("//ERROR reading file:"); return Helper.GetOutput(GeneratedFileName, Validator); } + Content = content; GeneratedFileName = $"{Options.LocalNamespace}.{Options.ClassName}.g.cs"; From b2cf6999e7c897ace2a1106c5982f112648211a2 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Sun, 19 Oct 2025 10:17:35 +0900 Subject: [PATCH 13/14] Add XML documentation comments --- .../Extensions/StringBuilderExtensions.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Aigamo.ResXGenerator/Extensions/StringBuilderExtensions.cs b/Aigamo.ResXGenerator/Extensions/StringBuilderExtensions.cs index cafeccc..551b12a 100644 --- a/Aigamo.ResXGenerator/Extensions/StringBuilderExtensions.cs +++ b/Aigamo.ResXGenerator/Extensions/StringBuilderExtensions.cs @@ -2,13 +2,35 @@ namespace Aigamo.ResXGenerator.Extensions; +/// +/// Provides extension methods for to append lines using LF ('\n') +/// regardless of the platform's default line ending (CRLF on Windows, LF on Unix/Mac). +/// internal static class StringBuilderExtensions { + /// + /// Appends a line feed character ('\n') to the end of the . + /// + /// The to append to. + /// + /// This method always appends a LF character instead of the platform-dependent . + /// Use this to ensure consistent line endings across Windows, Mac, and Linux. + /// public static void AppendLineLF(this StringBuilder builder) { builder.Append('\n'); } + /// + /// Appends the specified string followed by a line feed character ('\n') + /// to the end of the . + /// + /// The to append to. + /// The string to append before the line feed. + /// + /// This method always appends a LF character instead of the platform-dependent . + /// Use this to ensure consistent line endings across Windows, Mac, and Linux. + /// public static void AppendLineLF(this StringBuilder builder, string value) { builder.Append(value); From a3c62224b76778b56efe604460712bb557137aa2 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Sun, 19 Oct 2025 10:23:19 +0900 Subject: [PATCH 14/14] Update README.md --- README.md | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9fbd9a4..d2ce3de 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,10 @@ namespace Resources - The generator can now generate code to lookup translations instead of using the 20 year old System.Resources.ResourceManager -## New in version 4 -- The generator now include support for IStringLocalizer and static method to register in IServiceCollection (no more need of "magic string" :blush: +## New in version 4.3 + +- The generator now includes support for `IStringLocalizer` and static methods to register resources in `IServiceCollection` (no more need for "magic strings"). + ```c# var local = provider.GetService(); local.Actor // return the value string @@ -92,19 +94,20 @@ public class MyViewModel(IArtistCategoriesNames resources) : IMyViewModel ### GenerationType (per file or globally) Use cases: https://github.com/ycanardeau/ResXGenerator/issues/6. -Because the dependency injection on somes frameworks like Blazor. Uses the IStringLocalizer to get resources string. And to keep actual functionality. It now possible to choose the gerneration type by setting 'GenerationType'. +Some frameworks, like Blazor, use `IStringLocalizer` to get resource strings via dependency injection. To support this while keeping existing functionality, it is now possible to choose the generation type by setting the `GenerationType` parameter. -This paramerter is optional The default value is 'ResourceManager' to keep the actual behavior by default. +This parameter is optional. The default value is `ResourceManager` to preserve the existing behavior. AllowedValues : - ResourceManager - - When this option chosen the generator will use the classic [System.Resources.ResourceManager](https://learn.microsoft.com/en-us/dotnet/api/system.resources.resourcemanager?view=net-9.0) to get resources string. + - When this option is chosen, the generator will use the classic [System.Resources.ResourceManager](https://learn.microsoft.com/en-us/dotnet/api/system.resources.resourcemanager?view=net-9.0) to get resource strings. - CodeGeneration (You can chose this option to replace the GenerateCode option) - - When this option chosen the generator will generate code to get resources string. See [Generate Code](#Generate-Code) for more details)) + - When this option is chosen, the generator will generate code to access resource strings. See [Generate Code](#Generate-Code) for more details. - StringLocalizer - - When this option chosen the generator will generate interface and class to use with [Microsoft.Extensions.Localization](https://docs.microsoft.com/en-us/dotnet/core/extensions/localization) `IStringLocalizer`. To see how to use it see [Using IStringLocalizer](#IStringLocalizer) + - When this option is chosen, the generator will create interfaces and classes compatible with [Microsoft.Extensions.Localization](https://docs.microsoft.com/en-us/dotnet/core/extensions/localization) `IStringLocalizer`. + See [Using IStringLocalizer](#IStringLocalizer) for usage examples. - SameAsOuter - - When this option chosen the generator will use the same generation type as the outer class if any. If no outer class exist it will fall back to 'ResourceManager'. + - When this option is chosen, the generator will use the same generation type as the outer class, if any. If no outer class exists, it falls back to `ResourceManager`. ```xml @@ -559,12 +562,14 @@ Alternatively it can be set with the attribute `SkipFile="true"`. ## Using IStringLocalizer -To enablethe génération of interface and classes for your resource yo need to set de GenerationType to [StringLocalizer](#GenerationType). Note to use this you need to ensure you reference nuget on your project. +To enable the generation of interfaces and classes for your resources, you need to set the `GenerationType` to [StringLocalizer](#GenerationType). +Note: to use this, you must ensure that your project references the following NuGet packages: + - [Microsoft.Extensions.DependencyInjection](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection) - [Microsoft.Extensions.Localization](https://www.nuget.org/packages/Microsoft.Extensions.Localization) - [Microsoft.Extensions.Logging](https://www.nuget.org/packages/Microsoft.Extensions.Logging) -Now yous can register singletons for resources, simply using extension methods. +You can now register singletons for resources using extension methods. Examples for a file ArtistCategoriesNames.resx : ```c# @@ -576,7 +581,8 @@ builder.Services .UsingResXGenerator(); //This will register all resx class in one line of code ``` -you can also (if you prefer to load ressources by namespace using individual registrations). A class of registration is created by namespace. +you can also (if you prefer to load resources by namespace using individual registrations). A class of registration is created by namespace. + ```c# using MyResourceAssembly; @@ -586,7 +592,8 @@ builder.Services .UsingArtistCategoriesNamesResX(); //This will register all resx class of the namespace in one line of code ``` -Now simply use the dependency injection to get your resources classes. All interface is in the same namespase as it's resource file. (or configured namespace) +You can now use dependency injection to get your resource classes. +All interfaces are in the same namespace as their corresponding resource file (or the configured namespace). ## References