diff --git a/.editorconfig b/.editorconfig index 5cecb19..cae39eb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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..5bc9efc 100644 --- a/Aigamo.ResXGenerator.Tests/AdditionalTextStub.cs +++ b/Aigamo.ResXGenerator.Tests/AdditionalTextStub.cs @@ -3,17 +3,11 @@ 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 AdditionalTextStub(string path, string? text = null) - { - _text = text is null ? null : SourceText.From(text); - Path = path; - } + public override string Path { get; } = path; 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 0027e0e..6e39235 100644 --- a/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj +++ b/Aigamo.ResXGenerator.Tests/Aigamo.ResXGenerator.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -8,7 +8,7 @@ enable - + @@ -17,6 +17,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -35,9 +36,15 @@ true + + CodeGeneration + true + + StringLocalizer + diff --git a/Aigamo.ResXGenerator.Tests/CodeGenTests.cs b/Aigamo.ResXGenerator.Tests/CodeGenTests.cs index d313ad1..61f055f 100644 --- a/Aigamo.ResXGenerator.Tests/CodeGenTests.cs +++ b/Aigamo.ResXGenerator.Tests/CodeGenTests.cs @@ -1,216 +1,14 @@ -using FluentAssertions; +using Aigamo.ResXGenerator.Generators; +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 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 static void Generate( - IGenerator generator, + IResXGenerator generator, bool publicClass = true, bool staticClass = true, bool partial = false, @@ -218,46 +16,50 @@ 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", CustomToolNamespace = "Resources", ClassName = "ActivityEntrySortRuleNames", GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(string.Empty, Text), NewGuid()), - subFiles: new[] - { - new AdditionalTextWithHash(new AdditionalTextStub("test.da.rex", TextDa), NewGuid()), - new AdditionalTextWithHash(new AdditionalTextStub("test.da-dk.rex", TextDaDk), NewGuid()), - } + 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, @@ -265,18 +67,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 CodeGenerator(); Generate(generator); - Generate(generator, true, nullForgivingOperators: true); + Generate(generator, nullForgivingOperators: true); } } diff --git a/Aigamo.ResXGenerator.Tests/CodeResXTestsHelpers.cs b/Aigamo.ResXGenerator.Tests/CodeResXTestsHelpers.cs new file mode 100644 index 0000000..55ea124 --- /dev/null +++ b/Aigamo.ResXGenerator.Tests/CodeResXTestsHelpers.cs @@ -0,0 +1,235 @@ +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..21266c8 --- /dev/null +++ b/Aigamo.ResXGenerator.Tests/GeneratorLocalizerTests.cs @@ -0,0 +1,299 @@ +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 39168eb..358983a 100644 --- a/Aigamo.ResXGenerator.Tests/GeneratorTests.cs +++ b/Aigamo.ResXGenerator.Tests/GeneratorTests.cs @@ -1,82 +1,15 @@ -using FluentAssertions; +using Aigamo.ResXGenerator.Generators; +using Aigamo.ResXGenerator.Tools; +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( - IGenerator generator, + IResXGenerator generator, bool publicClass = true, bool staticClass = true, bool partial = false, @@ -84,46 +17,50 @@ 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 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() + 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: Array.Empty() + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetText()), Guid.NewGuid()), + subFiles: [] ), PublicClass = publicClass, NullForgivingOperators = nullForgivingOperators, @@ -132,12 +69,12 @@ namespace Resources; StaticMembers = staticMembers } ); - ErrorsAndWarnings.Should().BeNullOrEmpty(); - SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + result.ErrorsAndWarnings.Should().BeNullOrEmpty(); + result.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); } private static void GenerateInner( - IGenerator generator, + IResXGenerator generator, bool publicClass = true, bool staticClass = false, bool partial = false, @@ -148,41 +85,56 @@ private static void GenerateInner( 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() + 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", @@ -191,8 +143,8 @@ namespace Resources; PublicClass = publicClass, NullForgivingOperators = nullForgivingOperators, GroupedFile = new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", Text), NewGuid()), - subFiles: Array.Empty() + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetText()), Guid.NewGuid()), + subFiles: [] ), StaticClass = staticClass, PartialClass = partial, @@ -202,22 +154,22 @@ namespace Resources; InnerClassInstanceName = innerClassInstanceName } ); - 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); } [Fact] public void Generate_StringBuilder_NonStatic() { - var generator = new StringBuilderGenerator(); + var generator = new ResourceManagerGenerator(); Generate(generator, staticClass: false); Generate(generator, staticClass: false, nullForgivingOperators: true); } @@ -225,7 +177,7 @@ public void Generate_StringBuilder_NonStatic() [Fact] public void Generate_StringBuilder_Internal() { - var generator = new StringBuilderGenerator(); + var generator = new ResourceManagerGenerator(); Generate(generator, false); Generate(generator, false, nullForgivingOperators: true); } @@ -233,7 +185,7 @@ public void Generate_StringBuilder_Internal() [Fact] public void Generate_StringBuilder_Partial() { - var generator = new StringBuilderGenerator(); + var generator = new ResourceManagerGenerator(); Generate(generator, partial: true); Generate(generator, partial: true, nullForgivingOperators: true); } @@ -241,7 +193,7 @@ public void Generate_StringBuilder_Partial() [Fact] public void Generate_StringBuilder_NonStaticMembers() { - var generator = new StringBuilderGenerator(); + var generator = new ResourceManagerGenerator(); Generate(generator, staticMembers: false); Generate(generator, staticMembers: false, nullForgivingOperators: true); } @@ -249,223 +201,86 @@ public void Generate_StringBuilder_NonStaticMembers() [Fact] public void Generate_StringBuilder_Inner() { - var generator = new StringBuilderGenerator(); + var generator = new ResourceManagerGenerator(); GenerateInner(generator); } [Fact] public void Generate_StringBuilder_InnerInstance() { - var generator = new StringBuilderGenerator(); + var generator = new ResourceManagerGenerator(); 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() + 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: Array.Empty() + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetTextWithNewline()), Guid.NewGuid()), + subFiles: [] ), PublicClass = true, NullForgivingOperators = false, @@ -473,55 +288,62 @@ public static class CommonMessages StaticMembers = true } ); - 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_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() + 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: Array.Empty() + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), + subFiles: [] ), ClassName = "CommonMessages", PublicClass = true, @@ -530,32 +352,22 @@ public static class CommonMessages StaticMembers = true } ); - 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_Name_DuplicatedataGivesWarning() + public void Generate_StringBuilder_Name_DuplicateDataGivesWarning() { - var text = @" - - - Works. - - - Doeesnt Work. - -"; - - var generator = new StringBuilderGenerator(); - var (_, _, ErrorsAndWarnings) = generator.Generate( - options: new FileOptions() + 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: Array.Empty() + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", CodeResXTestsHelpers.GetDaTextWithDuplicates()), Guid.NewGuid()), + subFiles: [] ), CustomToolNamespace = null, ClassName = "CommonMessages", @@ -564,10 +376,10 @@ public void Generate_StringBuilder_Name_DuplicatedataGivesWarning() StaticClass = true } ); - var errs = ErrorsAndWarnings.ToList(); + 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); @@ -576,22 +388,25 @@ public void Generate_StringBuilder_Name_DuplicatedataGivesWarning() [Fact] public void Generate_StringBuilder_Name_MemberSameAsFileGivesWarning() { - var text = @" - - - Works. - -"; - - var generator = new StringBuilderGenerator(); - var (_, _, ErrorsAndWarnings) = generator.Generate( - options: new FileOptions() + 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: Array.Empty() + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), + subFiles: [] ), CustomToolNamespace = null, ClassName = "CommonMessages", @@ -600,26 +415,12 @@ public void Generate_StringBuilder_Name_MemberSameAsFileGivesWarning() StaticClass = true } ); - var errs = ErrorsAndWarnings.ToList(); + 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 86e0a09..48b88ef 100644 --- a/Aigamo.ResXGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs +++ b/Aigamo.ResXGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs @@ -1,7 +1,8 @@ using System.Xml; +using Aigamo.ResXGenerator.Generators; +using Aigamo.ResXGenerator.Tools; using FluentAssertions; using Xunit; -using static System.Guid; namespace Aigamo.ResXGenerator.Tests.GithubIssues.Issue3; @@ -10,106 +11,113 @@ 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. - -"; + 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(); + /// + /// 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 FileOptions() + 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: Array.Empty() + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), + subFiles: [] ), ClassName = "CommonMessages", PublicClass = true, @@ -125,82 +133,85 @@ public static class CommonMessages [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 + 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: Array.Empty() + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), Guid.NewGuid()), + subFiles: [] ), PublicClass = true, NullForgivingOperators = false, diff --git a/Aigamo.ResXGenerator.Tests/GlobalRegisterTests.cs b/Aigamo.ResXGenerator.Tests/GlobalRegisterTests.cs new file mode 100644 index 0000000..3d38e57 --- /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 7e73e96..2f43bc4 100644 --- a/Aigamo.ResXGenerator.Tests/GroupResxFilesTests.cs +++ b/Aigamo.ResXGenerator.Tests/GroupResxFilesTests.cs @@ -1,6 +1,6 @@ -using FluentAssertions; +using Aigamo.ResXGenerator.Tools; +using FluentAssertions; using Xunit; -using static System.Guid; namespace Aigamo.ResXGenerator.Tests; @@ -10,21 +10,19 @@ 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")), - 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")), - }); + @"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"), 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")), - new[] - { + @"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); } @@ -33,21 +31,18 @@ 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")), - 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")), - }); + @"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"), 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")), - 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")), - } + @"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"), 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); } @@ -56,21 +51,18 @@ 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")), - 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")), - }); + @"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"), 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")), - 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")), - } + @"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"), 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); } @@ -79,179 +71,172 @@ 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")), - 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")), - }); + @"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"), 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")), - new[] - { + @"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); } 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(x => 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 GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("00000000-0000-0000-0000-000000000002")), - subFiles: new[] - { - 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( + 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"), 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 GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.resx"), Parse("00000000-0000-0000-0000-000000000005")), - subFiles: new[] - { - 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( + 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"), 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 GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\Identity\Pages\Login.resx"), Parse("00000000-0000-0000-0000-000000000008")), - subFiles: new[] - { - 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( + 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"), 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 GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.resx"), Parse("00000000-0000-0000-0000-000000000011")), - subFiles: new[] - { - 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( + 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"), 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 GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.resx"), Parse("00000000-0000-0000-0000-000000000026")), - subFiles: new[] - { - 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 GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation.resx"), Parse("00000000-0000-0000-0000-000000000033")), - subFiles: new[] - { - new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation.da.resx"), Parse("00000000-0000-0000-0000-000000000032")) - } + new( + 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"), 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"), Guid.Parse("00000000-0000-0000-0000-000000000033")), + subFiles: + [ + new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation.da.resx"), Guid.Parse("00000000-0000-0000-0000-000000000032")) + ] ), - new GroupedAdditionalFile( - mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation2.resx"), Parse("00000000-0000-0000-0000-000000000034")), - subFiles: Array.Empty() + new( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation2.resx"), Guid.Parse("00000000-0000-0000-0000-000000000034")), + 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), Guid.NewGuid())).OrderBy(_ => Guid.NewGuid()).ToArray()).ToArray()).ToList(); var expected = new List { - new CultureInfoCombo(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 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([ + 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"), Guid.NewGuid())]), + new([]), + new([ + 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); result.Should().BeEquivalentTo(expected); diff --git a/Aigamo.ResXGenerator.Tests/HelperGeneratorTests.cs b/Aigamo.ResXGenerator.Tests/HelperGeneratorTests.cs index 7a38837..1dc87fc 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,71 @@ 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/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/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 + + 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 new file mode 100644 index 0000000..0de448d --- /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..7d7fd5a --- /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..63413e6 --- /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..ceb62be --- /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/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 new file mode 100644 index 0000000..71b4009 --- /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/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 87c9c33..a2f7e43 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; @@ -38,6 +39,8 @@ public void GlobalDefaults() 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] @@ -52,7 +55,7 @@ public void GlobalSettings_CanReadAll() MSBuildProjectName = "project1", ResXGenerator_InnerClassName = "test1", ResXGenerator_InnerClassInstanceName = "test2", - ResXGenerator_ClassNamePostfix= "test3", + ResXGenerator_ClassNamePostfix = "test3", ResXGenerator_InnerClassVisibility = "public", ResXGenerator_NullForgivingOperators = "true", ResXGenerator_StaticClass = "false", @@ -60,6 +63,7 @@ public void GlobalSettings_CanReadAll() ResXGenerator_GenerateCode = "true", ResXGenerator_PublicClass = "true", ResXGenerator_PartialClass = "true", + ResXGenerator_GenerationType = "ResourceManager" }, fileOptions: null! ), @@ -79,15 +83,16 @@ public void GlobalSettings_CanReadAll() 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 = FileOptions.Select( + var fileOptions = GenFileOptions.Select( file: new GroupedAdditionalFile( mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), - subFiles: Array.Empty() + subFiles: [] ), options: new AnalyzerConfigOptionsProviderStub( globalOptions: null!, @@ -110,6 +115,7 @@ public void FileDefaults() fileOptions.ClassName.Should().Be("Path1"); fileOptions.SkipFile.Should().Be(false); fileOptions.IsValid.Should().Be(true); + fileOptions.GenerationType.Should().Be(GenerationType.ResourceManager); } [Theory] @@ -131,10 +137,10 @@ 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() + subFiles: [] ), options: new AnalyzerConfigOptionsProviderStub( globalOptions: null!, @@ -168,15 +174,16 @@ string expectedEmbeddedFilename 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 = FileOptions.Select( + var fileOptions = GenFileOptions.Select( file: new GroupedAdditionalFile( mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()), - subFiles: Array.Empty() + subFiles: [] ), options: new AnalyzerConfigOptionsProviderStub( globalOptions: null!, @@ -191,16 +198,17 @@ 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() + subFiles: [] ), options: new AnalyzerConfigOptionsProviderStub( globalOptions: null!, fileOptions: new AnalyzerConfigOptionsStub { - RootNamespace = "namespace1", MSBuildProjectFullPath = "project1.csproj", + RootNamespace = "namespace1", + MSBuildProjectFullPath = "project1.csproj", CustomToolNamespace = "ns1", InnerClassName = "test1", InnerClassInstanceName = "test2", @@ -211,6 +219,7 @@ public void FileSettings_CanReadAll() PublicClass = "true", PartialClass = "true", GenerateCode = "true", + GenerationType = "ResourceManager" } ), globalOptions: s_globalOptions @@ -229,6 +238,7 @@ public void FileSettings_CanReadAll() 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] @@ -243,7 +253,7 @@ public void FileSettings_RespectsGlobalDefaults() MSBuildProjectName = "project1", ResXGenerator_InnerClassName = "test1", ResXGenerator_InnerClassInstanceName = "test2", - ResXGenerator_ClassNamePostfix= "test3", + ResXGenerator_ClassNamePostfix = "test3", ResXGenerator_InnerClassVisibility = "public", ResXGenerator_NullForgivingOperators = "true", ResXGenerator_StaticClass = "false", @@ -255,10 +265,10 @@ 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() + subFiles: [] ), options: new AnalyzerConfigOptionsProviderStub( globalOptions: null!, @@ -285,10 +295,10 @@ 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() + subFiles: [] ), options: new AnalyzerConfigOptionsProviderStub( globalOptions: null!, @@ -307,6 +317,52 @@ public void FileSettings_CanSkipIndividualFile() 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 + ); + + globalOptions.RootNamespace.Should().Be("namespace1"); + globalOptions.GenerationType.Should().Be(GenerationType.StringLocalizer); + globalOptions.IsValid.Should().Be(true); + } + private class AnalyzerConfigOptionsStub : AnalyzerConfigOptions { @@ -325,6 +381,7 @@ private class AnalyzerConfigOptionsStub : AnalyzerConfigOptions 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; } @@ -337,14 +394,16 @@ private class AnalyzerConfigOptionsStub : AnalyzerConfigOptions 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 public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) { - string? GetVal() => - key switch + string? GetVal() + { + return key switch { "build_property.MSBuildProjectFullPath" => MSBuildProjectFullPath, "build_property.MSBuildProjectName" => MSBuildProjectName, @@ -359,6 +418,7 @@ public override bool TryGetValue(string key, [NotNullWhen(true)] out string? val "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, @@ -372,29 +432,24 @@ public override bool TryGetValue(string key, [NotNullWhen(true)] out string? val "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; } } - private class AnalyzerConfigOptionsProviderStub : AnalyzerConfigOptionsProvider + private class AnalyzerConfigOptionsProviderStub(AnalyzerConfigOptions globalOptions, AnalyzerConfigOptions fileOptions) : AnalyzerConfigOptionsProvider { - private readonly AnalyzerConfigOptions _fileOptions; + private readonly AnalyzerConfigOptions _fileOptions = fileOptions; - public override AnalyzerConfigOptions GlobalOptions { get; } - - public AnalyzerConfigOptionsProviderStub(AnalyzerConfigOptions globalOptions, AnalyzerConfigOptions fileOptions) - { - _fileOptions = fileOptions; - GlobalOptions = globalOptions; - } + 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..5e3f05f 100644 --- a/Aigamo.ResXGenerator.Tests/UtilitiesTests.cs +++ b/Aigamo.ResXGenerator.Tests/UtilitiesTests.cs @@ -1,15 +1,10 @@ -using System.Diagnostics.CodeAnalysis; -using FluentAssertions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; +using FluentAssertions; using Xunit; namespace Aigamo.ResXGenerator.Tests; public class UtilitiesTests { - - [Theory] [InlineData("Valid", "Valid")] [InlineData("_Valid", "_Valid")] @@ -23,19 +18,26 @@ public class UtilitiesTests [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); - } + 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) + public void SanitizeNamespaceWithoutFirstCharRules(string input, string expected) => input.SanitizeNamespace(false).Should().Be(expected); + + [Fact] + public void GetLocalNamespace_ShouldNotGenerateIllegalNamespace() { - Utilities.SanitizeNamespace(input, false).Should().Be(expected); + 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.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/AdditionalTextWithHash.cs b/Aigamo.ResXGenerator/AdditionalTextWithHash.cs deleted file mode 100644 index 5899180..0000000 --- a/Aigamo.ResXGenerator/AdditionalTextWithHash.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace Aigamo.ResXGenerator; - -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 override int GetHashCode() - { - unchecked - { - return (File.GetHashCode() * 397) ^ Hash.GetHashCode(); - } - } - - public override string ToString() - { - return $"{nameof(File)}: {File?.Path}, {nameof(Hash)}: {Hash}"; - } -} diff --git a/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj b/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj index 64b24e3..14cb67d 100644 --- a/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj +++ b/Aigamo.ResXGenerator/Aigamo.ResXGenerator.csproj @@ -1,53 +1,54 @@ - - 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 new file mode 100644 index 0000000..658ed8d --- /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..4bfc7eb 100644 --- a/Aigamo.ResXGenerator/AnalyzerReleases.Unshipped.md +++ b/Aigamo.ResXGenerator/AnalyzerReleases.Unshipped.md @@ -1,9 +1,8 @@ -; 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 +--------|----------|----------|-------------------- +AigamoResXGenerator004 | ResXGenerator | Warning | LocalizerGenerator +AigamoResXGenerator005 | ResXGenerator | Warning | LocalizerGenerator +AigamoResXGenerator006 | ResXGenerator | Warning | LocalizerGenerator +AigamoResXGenerator999 | ResXGenerator | Error | SourceGenerator 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/BoolExtensions.cs b/Aigamo.ResXGenerator/Extensions/BoolExtensions.cs new file mode 100644 index 0000000..5c3abe8 --- /dev/null +++ b/Aigamo.ResXGenerator/Extensions/BoolExtensions.cs @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..6bda930 --- /dev/null +++ b/Aigamo.ResXGenerator/Extensions/EnumerableExtensions.cs @@ -0,0 +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); + } +} diff --git a/Aigamo.ResXGenerator/Extensions/StringBuilderExtensions.cs b/Aigamo.ResXGenerator/Extensions/StringBuilderExtensions.cs new file mode 100644 index 0000000..551b12a --- /dev/null +++ b/Aigamo.ResXGenerator/Extensions/StringBuilderExtensions.cs @@ -0,0 +1,39 @@ +using System.Text; + +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); + builder.AppendLineLF(); + } +} diff --git a/Aigamo.ResXGenerator/Extensions/StringExtensions.cs b/Aigamo.ResXGenerator/Extensions/StringExtensions.cs new file mode 100644 index 0000000..7757a36 --- /dev/null +++ b/Aigamo.ResXGenerator/Extensions/StringExtensions.cs @@ -0,0 +1,23 @@ +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) => input.ToXmlCommentSafe(string.Empty); + public static string ToXmlCommentSafe(this string input, string indent) + { + var lines = HttpUtility.HtmlEncode(input.Trim()).GetCodeLines(); + 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($"{Constants.NewLine}{indent}", input.GetCodeLines()); + } + + 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..6c95164 --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/CodeGenerator.cs @@ -0,0 +1,82 @@ +using System.Collections.Immutable; +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; +using Microsoft.CodeAnalysis.CSharp; + +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.AppendLineLF(");"); + }); + } +} diff --git a/Aigamo.ResXGenerator/Generators/ComboGenerator.cs b/Aigamo.ResXGenerator/Generators/ComboGenerator.cs new file mode 100644 index 0000000..ef4d56b --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/ComboGenerator.cs @@ -0,0 +1,82 @@ +using System.Globalization; +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; + +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; } + + /// + /// 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) + { + Init(options); + Helper = new StringBuilderGeneratorHelper(); + + var definedLanguages = Options.GetDefinedLanguages(); + + Helper.AppendHeader("Aigamo.ResXGenerator"); + + Helper.AppendLineLF("internal static partial class Helpers"); + Helper.AppendLineLF("{"); + + 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); + + Helper.Append(") => "); + Helper.Append(Constants.SystemGlobalization); + Helper.AppendLineLF(".CultureInfo.CurrentUICulture.LCID switch"); + Helper.AppendLineLF("\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.AppendLineLF(l)); + }); + + Helper.AppendLineLF("\t\t_ => fallback"); + Helper.AppendLineLF("\t};"); + Helper.AppendLineLF("}"); + + 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/GeneratorBase.cs b/Aigamo.ResXGenerator/Generators/GeneratorBase.cs new file mode 100644 index 0000000..03cd162 --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/GeneratorBase.cs @@ -0,0 +1,39 @@ +using System.Xml.Linq; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; +using Microsoft.CodeAnalysis.Text; + +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..75493db --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/LocalizerGenerator.cs @@ -0,0 +1,64 @@ +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(Constants.NewLine, fallback.Select(GenerateInterfaceMembers)).Indent()}} + } + + {{Options.PublicClass.InterpolateCondition("public", "internal")}} class {{Options.ClassName}}(IStringLocalizer<{{Options.ClassName}}> stringLocalizer) : I{{Options.ClassName}} + { + {{string.Join(Constants.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 new file mode 100644 index 0000000..78c62d9 --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/LocalizerGlobalRegisterGenerator.cs @@ -0,0 +1,42 @@ +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(Constants.NewLine, Options.Select(GenerateUsing))}} + + namespace ResXGenerator.Registration; + + public static class ResXGeneratorRegistrationExtension + { + public static IServiceCollection UsingResXGenerator(this IServiceCollection services) + { + {{string.Join(Constants.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..cb1c75e --- /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(Constants.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 new file mode 100644 index 0000000..f7a9041 --- /dev/null +++ b/Aigamo.ResXGenerator/Generators/ResourceManagerGenerator.cs @@ -0,0 +1,74 @@ +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; + +namespace Aigamo.ResXGenerator.Generators; + +public sealed class ResourceManagerGenerator : GeneratorBase, IResXGenerator +{ + private StringBuilderGeneratorHelper Helper { get; set; } + + public override GeneratedOutput Generate(GenFileOptions options, CancellationToken cancellationToken = default) + { + Init(options); + + Helper = new StringBuilderGeneratorHelper(options); + + 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"; + + 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.AppendLineLF(";"); + } +} 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/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..01c078f --- /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/NullableAttributes.cs b/Aigamo.ResXGenerator/NullableAttributes.cs deleted file mode 100644 index f4729e0..0000000 --- a/Aigamo.ResXGenerator/NullableAttributes.cs +++ /dev/null @@ -1,208 +0,0 @@ -// https://github.com/dotnet/runtime/blob/527f9ae88a0ee216b44d556f9bdc84037fe0ebda/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs - -#pragma warning disable -#define INTERNAL_NULLABLE_ATTRIBUTES - -// 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 -{ -#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 -#else - internal -#endif - 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)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - 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)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - 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)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - 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)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - 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)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - 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)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - 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)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - internal -#endif - 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)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - 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; } - } -#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 SYSTEM_PRIVATE_CORELIB - public -#else - 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)] -#if SYSTEM_PRIVATE_CORELIB - public -#else - 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; } - } -#endif -} diff --git a/Aigamo.ResXGenerator/Properties/launchSettings.json b/Aigamo.ResXGenerator/Properties/launchSettings.json index aacbe6d..d04f985 100644 --- a/Aigamo.ResXGenerator/Properties/launchSettings.json +++ b/Aigamo.ResXGenerator/Properties/launchSettings.json @@ -3,6 +3,9 @@ "Debug": { "commandName": "DebugRoslynComponent", "targetProject": "..\\Aigamo.ResXGenerator.Tests\\Aigamo.ResXGenerator.Tests.csproj" + }, + "Profil 1": { + "commandName": "Project" } } } 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 370a69a..6604371 100644 --- a/Aigamo.ResXGenerator/SourceGenerator.cs +++ b/Aigamo.ResXGenerator/SourceGenerator.cs @@ -1,12 +1,15 @@ -using Microsoft.CodeAnalysis; +using System.Collections.Immutable; +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Generators; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; +using Microsoft.CodeAnalysis; namespace Aigamo.ResXGenerator; [Generator] public class SourceGenerator : IIncrementalGenerator { - private static readonly IGenerator s_generator = new StringBuilderGenerator(); - public void Initialize(IncrementalGeneratorInitializationContext context) { var globalOptions = context.AnalyzerConfigOptionsProvider.Select(GlobalOptions.Select); @@ -15,41 +18,165 @@ public void Initialize(IncrementalGeneratorInitializationContext context) 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 + var monitor = allResxFiles + .Collect() + .SelectMany(static (x, _) => GroupResxFiles.Group(x)) .Combine(globalOptions) .Combine(context.AnalyzerConfigOptionsProvider) - .Select(static (x, _) => FileOptions.Select( - file: x.Left.Left, - options: x.Right, - globalOptions: x.Left.Right - )) - .Where(static x => x.IsValid && !x.SkipFile); + .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) => { - var (generatedFileName, sourceCode, errorsAndWarnings) = - s_generator.Generate(file, ctx.CancellationToken); - foreach (var sourceErrorsAndWarning in errorsAndWarnings) + try { - ctx.ReportDiagnostic(sourceErrorsAndWarning); + 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.Message)); + } + }); + } - ctx.AddSource(generatedFileName, sourceCode); + 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.Message)); + } }); - var detectAllCombosOfResx = monitor.Collect().SelectMany((x, _) => GroupResxFiles.DetectChildCombos(x)); + 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) => { - var (generatedFileName, sourceCode, errorsAndWarnings) = - s_generator.Generate(combo, ctx.CancellationToken); - foreach (var sourceErrorsAndWarning in errorsAndWarnings) + 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.Message)); + } + }); + } + + private static void GenerateLocalizerRegister(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) + { + var generator = new LocalizerRegisterGenerator(); + + context.RegisterSourceOutput(inputs, (ctx, ns) => + { + try { - ctx.ReportDiagnostic(sourceErrorsAndWarning); + 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.Message)); + } + }); - ctx.AddSource(generatedFileName, sourceCode); + 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.Message)); + } + }); + } + + private static void GenerateLocalizerGlobalRegister(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider inputs) + { + var globalGenerator = new LocalizerGlobalRegisterGenerator(); + var global = inputs.Collect(); + + context.RegisterSourceOutput(global, (ctx, gns) => + { + if (gns.Length == 0) return; + 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.Message)); + } }); } } 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/Tools/AdditionalTextWithHash.cs b/Aigamo.ResXGenerator/Tools/AdditionalTextWithHash.cs new file mode 100644 index 0000000..ab5a040 --- /dev/null +++ b/Aigamo.ResXGenerator/Tools/AdditionalTextWithHash.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; + +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 override int GetHashCode() + { + unchecked + { + return File.GetHashCode() * 397 ^ Hash.GetHashCode(); + } + } + + 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 new file mode 100644 index 0000000..6cabec6 --- /dev/null +++ b/Aigamo.ResXGenerator/Tools/Constants.cs @@ -0,0 +1,23 @@ +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"; + public const string NewLine = "\n"; +} diff --git a/Aigamo.ResXGenerator/CultureInfoCombo.cs b/Aigamo.ResXGenerator/Tools/CultureInfoCombo.cs similarity index 63% rename from Aigamo.ResXGenerator/CultureInfoCombo.cs rename to Aigamo.ResXGenerator/Tools/CultureInfoCombo.cs index dac55b8..abcc33a 100644 --- a/Aigamo.ResXGenerator/CultureInfoCombo.cs +++ b/Aigamo.ResXGenerator/Tools/CultureInfoCombo.cs @@ -1,4 +1,6 @@ using System.Globalization; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; namespace Aigamo.ResXGenerator; @@ -14,21 +16,19 @@ 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;} + 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)) - .ToList() ?? new List<(string Name, int LCID, AdditionalTextWithHash FileWithHash)>(); + .Select(x => new ComboItem(x.Item2.Name.Replace('-', '_'), x.Item2.LCID, x.File)) + .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()); - } + 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/FileOptions.cs b/Aigamo.ResXGenerator/Tools/GenFileOptions.cs similarity index 90% rename from Aigamo.ResXGenerator/FileOptions.cs rename to Aigamo.ResXGenerator/Tools/GenFileOptions.cs index 5747032..8dced47 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; } @@ -17,11 +17,12 @@ public readonly record struct FileOptions 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; } - public FileOptions( + public GenFileOptions( GroupedAdditionalFile groupedFile, AnalyzerConfigOptions options, GlobalOptions globalOptions @@ -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 } @@ -142,13 +153,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/Tools/GenerationType.cs b/Aigamo.ResXGenerator/Tools/GenerationType.cs new file mode 100644 index 0000000..65e004f --- /dev/null +++ b/Aigamo.ResXGenerator/Tools/GenerationType.cs @@ -0,0 +1,30 @@ +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 +{ + /// + /// When this option chosen the generator will use the classic ResourceManager to get resources string. + /// See : ResourceManager. + /// + ResourceManager, + /// + /// 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, + /// + /// 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, + /// + /// 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/Aigamo.ResXGenerator/GlobalOptions.cs b/Aigamo.ResXGenerator/Tools/GlobalOptions.cs similarity index 90% rename from Aigamo.ResXGenerator/GlobalOptions.cs rename to Aigamo.ResXGenerator/Tools/GlobalOptions.cs index 5a7f6c4..ee8c300 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; @@ -17,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) @@ -38,6 +40,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 @@ -97,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/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..eb8fe4d 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..c29430c --- /dev/null +++ b/Aigamo.ResXGenerator/Tools/IGenerator.cs @@ -0,0 +1,14 @@ +using System.Collections.Immutable; +using Aigamo.ResXGenerator.Models; + +namespace Aigamo.ResXGenerator.Tools; + +public interface IGenerator +{ + GeneratedOutput Generate(T options, CancellationToken cancellationToken = default); +} + +public interface IResXGenerator : IGenerator; +public interface IComboGenerator : IGenerator; +public interface ILocalRegisterGenerator : IGenerator; +public interface IGlobalRegisterGenerator : IGenerator>; 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/Tools/IntegrityValidator.cs b/Aigamo.ResXGenerator/Tools/IntegrityValidator.cs new file mode 100644 index 0000000..ac52baa --- /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/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/Tools/NullableAttributes.cs b/Aigamo.ResXGenerator/Tools/NullableAttributes.cs new file mode 100644 index 0000000..bb4264d --- /dev/null +++ b/Aigamo.ResXGenerator/Tools/NullableAttributes.cs @@ -0,0 +1,213 @@ +// https://github.com/dotnet/runtime/blob/527f9ae88a0ee216b44d556f9bdc84037fe0ebda/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs + +#pragma warning disable +#define INTERNAL_NULLABLE_ATTRIBUTES + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +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. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + 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; } + } +#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 SYSTEM_PRIVATE_CORELIB + public +#else + 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + 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; } + } +#endif +} diff --git a/Aigamo.ResXGenerator/Tools/RegexDefinitions.cs b/Aigamo.ResXGenerator/Tools/RegexDefinitions.cs new file mode 100644 index 0000000..240a9ee --- /dev/null +++ b/Aigamo.ResXGenerator/Tools/RegexDefinitions.cs @@ -0,0 +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 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..6128e7d --- /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; } = "\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 AppendLineLF(string line) => Builder.AppendLineLF(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.AppendLineLF(Constants.AutoGeneratedHeader); + Builder.AppendLineLF("#nullable enable"); + Builder.Append("namespace "); + Builder.Append(@namespace); + Builder.AppendLineLF(";"); + } + + 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.AppendLineLF(" { get; } = new();"); + Builder.AppendLineLF(); + } + + Builder.Append(Indent); + Builder.Append(GetInnerClassVisibility(options)); + Builder.Append(options.StaticClass ? " static" : string.Empty); + Builder.Append(options.PartialClass ? " partial class " : " class "); + + Builder.AppendLineLF(ContainerClassName); + Builder.Append(Indent); + Builder.AppendLineLF("{"); + + 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.AppendLineLF(options.ClassName); + Builder.AppendLineLF("{"); + } + + public void AppendClassFooter(GenFileOptions options) + { + if (options.InnerClassVisibility != InnerClassVisibility.NotGenerated) + Builder.AppendLineLF("\t}"); + + Builder.AppendLineLF("}"); + } + + 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.AppendLineLF(); + + Builder.Append(Indent); + Builder.AppendLineLF("/// "); + + Builder.Append(Indent); + Builder.Append("/// Looks up a localized string similar to "); + Builder.Append(fallbackItem.Value.ToXmlCommentSafe(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, resourceAccessByName); + } + + public void AppendResourceManagerUsings() + { + Builder.Append("using "); + Builder.Append(Constants.SystemGlobalization); + Builder.AppendLineLF(";"); + + Builder.Append("using "); + Builder.Append(Constants.SystemResources); + Builder.AppendLineLF(";"); + + Builder.AppendLineLF(); + } + + public void AppendCodeUsings() + { + Builder.AppendLineLF("using static Aigamo.ResXGenerator.Helpers;"); + Builder.AppendLineLF(); + } + + public void GenerateResourceManagerMembers(GenFileOptions options) + { + Builder.Append(Indent); + Builder.Append("private static "); + Builder.Append(nameof(ResourceManager)); + Builder.Append("? "); + Builder.Append(Constants.SResourceManagerVariable); + Builder.AppendLineLF(";"); + + 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.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; }"); + } + + 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/Utilities.cs b/Aigamo.ResXGenerator/Tools/Utilities.cs similarity index 75% rename from Aigamo.ResXGenerator/Utilities.cs rename to Aigamo.ResXGenerator/Tools/Utilities.cs index 0103dd7..d18eb2c 100644 --- a/Aigamo.ResXGenerator/Utilities.cs +++ b/Aigamo.ResXGenerator/Tools/Utilities.cs @@ -1,6 +1,10 @@ using System.Globalization; using System.Text.RegularExpressions; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using Aigamo.ResXGenerator.Extensions; +using Aigamo.ResXGenerator.Models; +using Aigamo.ResXGenerator.Tools; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; namespace Aigamo.ResXGenerator; @@ -17,7 +21,7 @@ private static bool IsValidLanguageName(string? languageName) return false; } - if (languageName.StartsWith("qps-", StringComparison.Ordinal)) + if (languageName!.StartsWith("qps-", StringComparison.Ordinal)) { return true; } @@ -84,7 +88,7 @@ public static string GetLocalNamespace( if (!string.IsNullOrWhiteSpace(targetPath)) { - localNamespace = Path.GetDirectoryName(targetPath) + localNamespace = Path.GetDirectoryName(targetPath)! .Trim(Path.DirectorySeparatorChar) .Trim(Path.AltDirectorySeparatorChar) .Replace(Path.DirectorySeparatorChar, '.') @@ -134,31 +138,38 @@ public static string GetClassNameFromPath(string resxFilePath) return className; } - public static string SanitizeNamespace(string ns, bool sanitizeFirstChar = true) + 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_\.]", "_"); + 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, @"\.+", "."); + sanitizedNs = Regex.Replace(sanitizedNs, @"\.+", "."); - if (sanitizeFirstChar) - { - sanitizedNs = sanitizedNs.Trim('.'); - } + if (sanitizeFirstChar) sanitizedNs = sanitizedNs.Trim('.'); - return sanitizeFirstChar - // Handle namespace starting with digit - ? char.IsDigit(sanitizedNs[0]) ? $"_{sanitizedNs}" : sanitizedNs - : sanitizedNs; + // 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..729bf1d 100644 --- a/Aigamo.ResXGenerator/build/Aigamo.ResXGenerator.props +++ b/Aigamo.ResXGenerator/build/Aigamo.ResXGenerator.props @@ -1,37 +1,39 @@ - - $(AdditionalFileItemNames);EmbeddedResource - + + $(AdditionalFileItemNames);EmbeddedResource + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DemoLocalization/DemoLocalization.csproj b/DemoLocalization/DemoLocalization.csproj new file mode 100644 index 0000000..fec0689 --- /dev/null +++ b/DemoLocalization/DemoLocalization.csproj @@ -0,0 +1,35 @@ + + + + Exe + net8.0 + enable + enable + latest + false + true + StringLocalizer + false + false + + + + + + + + + + + + + + + + + + $([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx + + + + diff --git a/DemoLocalization/Langage.cs b/DemoLocalization/Langage.cs new file mode 100644 index 0000000..cf21717 --- /dev/null +++ b/DemoLocalization/Langage.cs @@ -0,0 +1,8 @@ +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..ae7b681 --- /dev/null +++ b/DemoLocalization/Options.cs @@ -0,0 +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 + """)] + 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..f1729d5 --- /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 + + diff --git a/DemoLocalization/Resource.fr.resx b/DemoLocalization/Resource.fr.resx new file mode 100644 index 0000000..bb5e263 --- /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..e0cc03c --- /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 + + 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* diff --git a/README.md b/README.md index 00584a7..d2ce3de 100644 --- a/README.md +++ b/README.md @@ -16,60 +16,122 @@ 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.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 +``` + +or + +```c# +var myViewModel = provider.GetService(); + +public class MyViewModel(IArtistCategoriesNames resources) : IMyViewModel +{ + public string Value => resources.Actor // return also the value string +} +``` ## Options + +### GenerationType (per file or globally) +Use cases: https://github.com/ycanardeau/ResXGenerator/issues/6. + +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 parameter is optional. The default value is `ResourceManager` to preserve the existing behavior. + +AllowedValues : +- ResourceManager + - 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 is chosen, the generator will generate code to access resource strings. See [Generate Code](#Generate-Code) for more details. +- StringLocalizer + - 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 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 + + + 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. @@ -78,9 +140,9 @@ Since version 2.0.0, ResXGenerator generates internal classes by default. You ca ```xml - - true - + + true + ``` @@ -88,7 +150,7 @@ or ```xml - + ``` @@ -96,7 +158,7 @@ If you want to apply this globally, use ```xml - true + true ``` @@ -106,7 +168,7 @@ Use cases: https://github.com/VocaDB/ResXFileCodeGenerator/issues/1. ```xml - true + true ``` @@ -128,9 +190,9 @@ To use generated resources with [Microsoft.Extensions.Localization](https://docs ```xml - - false - + + false + ``` @@ -138,7 +200,7 @@ or globally ```xml - false + false ``` @@ -146,13 +208,13 @@ 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 - - true - + + true + ``` @@ -160,19 +222,19 @@ or globally ```xml - true + true ``` ### 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 - - false - + + false + ``` @@ -180,7 +242,7 @@ or globally ```xml - false + false ``` @@ -193,17 +255,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 + ``` @@ -211,28 +273,28 @@ or just the postfix globally ```xml - Model + Model ``` ## 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 - - $([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 + ``` @@ -240,12 +302,12 @@ or globally ```xml - MyResources - private - EveryoneLikeMyNaming - false - false - true + MyResources + private + EveryoneLikeMyNaming + false + false + true ``` @@ -254,39 +316,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); + } + } } ``` @@ -294,11 +356,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". @@ -306,9 +368,9 @@ It is also possible to use "NotGenerated" to override on a file if the global se ```xml - - private - + + private + ``` @@ -316,7 +378,7 @@ or globally ```xml - private + private ``` @@ -326,9 +388,9 @@ By default the inner class is named "Resources", which can be overridden with th ```xml - - MyResources - + + MyResources + ``` @@ -336,7 +398,7 @@ or globally ```xml - MyResources + MyResources ``` @@ -346,9 +408,9 @@ By default no instance is available of the class, but that can be made available ```xml - - EveryoneLikeMyNaming - + + EveryoneLikeMyNaming + ``` @@ -356,56 +418,57 @@ or globally ```xml - EveryoneLikeMyNaming + EveryoneLikeMyNaming ``` 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. 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 + ``` @@ -413,12 +476,12 @@ or globally ```xml - true + true - - false - + + false + ``` @@ -426,9 +489,9 @@ If you get build error MSB3030, add this to your csproj to prevent it from tryin ```xml - - - + + + ``` @@ -440,14 +503,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 + ``` @@ -455,14 +518,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 + ``` @@ -470,9 +533,9 @@ It is also possible to set the namespace using the `CustomToolNamespace` setting ```xml - - MyNamespace.AllMyResourcesAreBelongToYouNamespace - + + MyNamespace.AllMyResourcesAreBelongToYouNamespace + ``` @@ -482,9 +545,9 @@ Individual resx files can also be excluded from being processed by setting the ` ```xml - - true - + + true + ``` @@ -496,14 +559,50 @@ Alternatively it can be set with the attribute `SkipFile="true"`.
``` + +## Using IStringLocalizer + +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) + +You can now register singletons for resources 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 resources 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 +``` + +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 -- [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)