diff --git a/Catglobe.ResXFileCodeGenerator.Tests/GeneratorTests.cs b/Catglobe.ResXFileCodeGenerator.Tests/GeneratorTests.cs index c52e8d8..c81ef11 100644 --- a/Catglobe.ResXFileCodeGenerator.Tests/GeneratorTests.cs +++ b/Catglobe.ResXFileCodeGenerator.Tests/GeneratorTests.cs @@ -75,6 +75,78 @@ public class GeneratorTests "; + private const string TextWithUnsupportedChar = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + SystemName + +"; + + private static void Generate( IGenerator generator, bool publicClass = true, @@ -82,7 +154,7 @@ private static void Generate( bool partial = false, bool nullForgivingOperators = false, bool staticMembers = true - ) + ) { var expected = $@"// ------------------------------------------------------------------------------ // @@ -200,11 +272,96 @@ namespace Resources; InnerClassName = innerClassName, InnerClassVisibility = innerClassVisibility, InnerClassInstanceName = innerClassInstanceName - } + } ); ErrorsAndWarnings.Should().BeNullOrEmpty(); SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); } + + private static void GenerateKeys( + IGenerator generator, + bool publicClass = true, + bool staticClass = false, + bool partial = false, + bool nullForgivingOperators = false, + bool staticMembers = true + ) + { + var expected = $@"// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#nullable enable +namespace Resources; +using System.Globalization; +using System.Resources; + +{(publicClass ? "public" : "internal")}{(staticClass ? " static" : string.Empty)}{(partial ? " partial" : string.Empty)} class ActivityEntrySortRuleNames +{{ + private static ResourceManager? s_resourceManager; + public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager(""Catglobe.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)}; + + /// + /// Looks up a localized string similar to SystemName. + /// + public{(staticMembers ? " static" : string.Empty)} string{(nullForgivingOperators ? string.Empty : "?")} Sys_Name => ResourceManager.GetString(""Sys.Name"", CultureInfo){(nullForgivingOperators ? "!" : string.Empty)}; + public{(staticClass ? " static" : string.Empty)} class Keys + {{ + + /// + /// Name of resource CreateDate. + /// + public const string CreateDate = nameof(CreateDate); + + /// + /// Name of resource CreateDateDescending. + /// + public const string CreateDateDescending = nameof(CreateDateDescending); + + /// + /// Name of resource Sys.Name. + /// + public const string Sys_Name = ""Sys.Name""; + }} +}} +"; + var (_, sourceCode, errorsAndWarnings) = generator.Generate( + options: new FileOptions() + { + LocalNamespace = "Catglobe.Web.App_GlobalResources", + EmbeddedFilename = "Catglobe.Web.App_GlobalResources.ActivityEntrySortRuleNames", + CustomToolNamespace = "Resources", + ClassName = "ActivityEntrySortRuleNames", + PublicClass = publicClass, + NullForgivingOperators = nullForgivingOperators, + GroupedFile = new GroupedAdditionalFile( + mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", TextWithUnsupportedChar), NewGuid()), + subFiles: Array.Empty() + ), + StaticClass = staticClass, + PartialClass = partial, + StaticMembers = staticMembers, + KeyGeneration = true + } + ); + errorsAndWarnings.Should().BeNullOrEmpty(); + sourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings()); + } [Fact] public void Generate_StringBuilder_Public() @@ -573,6 +730,14 @@ public void Generate_StringBuilder_Name_DuplicatedataGivesWarning() errs[0].Location.GetLineSpan().StartLinePosition.Line.Should().Be(5); } + [Fact] + public void Generate_StringBuilder_KeyGeneration() + { + var generator = new StringBuilderGenerator(); + GenerateKeys(generator); + GenerateKeys(generator, true, nullForgivingOperators: true); + } + [Fact] public void Generate_StringBuilder_Name_MemberSameAsFileGivesWarning() { diff --git a/Catglobe.ResXFileCodeGenerator/Catglobe.ResXFileCodeGenerator.csproj b/Catglobe.ResXFileCodeGenerator/Catglobe.ResXFileCodeGenerator.csproj index fbd6380..9899bb5 100644 --- a/Catglobe.ResXFileCodeGenerator/Catglobe.ResXFileCodeGenerator.csproj +++ b/Catglobe.ResXFileCodeGenerator/Catglobe.ResXFileCodeGenerator.csproj @@ -19,6 +19,7 @@ frombuild enable true + true diff --git a/Catglobe.ResXFileCodeGenerator/FileOptions.cs b/Catglobe.ResXFileCodeGenerator/FileOptions.cs index 3f54d08..da878bf 100644 --- a/Catglobe.ResXFileCodeGenerator/FileOptions.cs +++ b/Catglobe.ResXFileCodeGenerator/FileOptions.cs @@ -17,7 +17,8 @@ public readonly record struct FileOptions public bool UseResManager { get; init; } public string EmbeddedFilename { get; init; } public bool IsValid { get; init; } - + public bool KeyGeneration { get; init; } + public FileOptions( GroupedAdditionalFile groupedFile, AnalyzerConfigOptions options, @@ -129,7 +130,16 @@ GlobalOptions globalOptions } IsValid = globalOptions.IsValid; - } + + KeyGeneration = globalOptions.KeyGeneration; + if ( + options.TryGetValue("build_metadata.EmbeddedResource.KeyGeneration", out var keyGenerationSwitch) && + keyGenerationSwitch is { Length: > 0 } + ) + { + KeyGeneration= keyGenerationSwitch.Equals("true", StringComparison.OrdinalIgnoreCase); + } + } public static FileOptions Select( GroupedAdditionalFile file, diff --git a/Catglobe.ResXFileCodeGenerator/GlobalOptions.cs b/Catglobe.ResXFileCodeGenerator/GlobalOptions.cs index 8d81244..dda19e3 100644 --- a/Catglobe.ResXFileCodeGenerator/GlobalOptions.cs +++ b/Catglobe.ResXFileCodeGenerator/GlobalOptions.cs @@ -16,6 +16,7 @@ public sealed record GlobalOptions // this must be a record or implement IEquata public string ClassNamePostfix { get; } public bool UseResManager { get; } public bool IsValid { get; } + public bool KeyGeneration { get; } public GlobalOptions(AnalyzerConfigOptions options) { @@ -104,6 +105,10 @@ public GlobalOptions(AnalyzerConfigOptions options) { UseResManager = true; } + + KeyGeneration = options.TryGetValue("build_property.ResXFileCodeGenerator_KeyGeneration", out var keyGenerationSwitch) && + keyGenerationSwitch is { Length: > 0 } && + keyGenerationSwitch.Equals("true", StringComparison.OrdinalIgnoreCase); } public static GlobalOptions Select(AnalyzerConfigOptionsProvider provider, CancellationToken token) diff --git a/Catglobe.ResXFileCodeGenerator/StringBuilderGenerator.KeyGeneration.cs b/Catglobe.ResXFileCodeGenerator/StringBuilderGenerator.KeyGeneration.cs new file mode 100644 index 0000000..9d74e3a --- /dev/null +++ b/Catglobe.ResXFileCodeGenerator/StringBuilderGenerator.KeyGeneration.cs @@ -0,0 +1,149 @@ +using System.Web; + +namespace Catglobe.ResXFileCodeGenerator; + +public sealed partial class StringBuilderGenerator : IGenerator +{ + private static void CreateMemberKey( + string indent, + StringBuilder builder, + FileOptions options, + string name, + string value, + IXmlLineInfo line, + HashSet alreadyAddedMembers, + List errorsAndWarnings, + string containerclassname + ) + { + if (!GenerateMemberKey(indent, builder, options, name, value, line, alreadyAddedMembers, errorsAndWarnings, containerclassname, out var resourceAccessByName)) + { + return; + } + + if (resourceAccessByName) + { + // => name + builder.Append(" = nameof("); + builder.Append(name); + builder.Append(")"); + } + else + { + // => "name" + // replace " with \" + builder.Append(" = \""); + builder.Append(name.Replace(@"""", @"\""")); + builder.Append("\""); + } + + builder.AppendLineLF(";"); + } + + private static bool GenerateMemberKey( + 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("/// Name of resource "); + builder.Append(name); + builder.AppendLineLF("."); + + builder.Append(indent); + builder.AppendLineLF("/// "); + + builder.Append(indent); + builder.Append("public const string "); + builder.Append(memberName); + return true; + } + + private void KeyGeneration( + FileOptions options, + SourceText content, + string indent, + string containerClassName, + StringBuilder builder, + List errorsAndWarnings, + CancellationToken cancellationToken + ) + { + var members = ReadResxFile(content); + if (members is null) + { + return; + } + + var alreadyAddedMembers = new HashSet() { Constants.CultureInfoVariable }; + foreach (var (key, value, line) in members) + { + cancellationToken.ThrowIfCancellationRequested(); + CreateMemberKey( + indent, + builder, + options, + key, + value, + line, + alreadyAddedMembers, + errorsAndWarnings, + containerClassName + ); + } + } +} diff --git a/Catglobe.ResXFileCodeGenerator/StringBuilderGenerator.cs b/Catglobe.ResXFileCodeGenerator/StringBuilderGenerator.cs index 901d61f..6c10589 100644 --- a/Catglobe.ResXFileCodeGenerator/StringBuilderGenerator.cs +++ b/Catglobe.ResXFileCodeGenerator/StringBuilderGenerator.cs @@ -5,242 +5,261 @@ namespace Catglobe.ResXFileCodeGenerator; 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: "CatglobeResXFileCodeGenerator001", - title: "Duplicate member", - messageFormat: "Ignored added member '{0}'", - category: "ResXFileCodeGenerator", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); - - private static readonly DiagnosticDescriptor s_memberSameAsClassWarning = new( - id: "CatglobeResXFileCodeGenerator002", - title: "Member same name as class", - messageFormat: "Ignored member '{0}' has same name as class", - category: "ResXFileCodeGenerator", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); - - private static readonly DiagnosticDescriptor s_memberWithStaticError = new( - id: "CatglobeResXFileCodeGenerator003", - title: "Incompatible settings", - messageFormat: "Cannot have static members/class with an class instance", - category: "ResXFileCodeGenerator", - 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.UseResManager) - 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.UseResManager) - 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; - } + 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: "CatglobeResXFileCodeGenerator001", + title: "Duplicate member", + messageFormat: "Ignored added member '{0}'", + category: "ResXFileCodeGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + private static readonly DiagnosticDescriptor s_memberSameAsClassWarning = new( + id: "CatglobeResXFileCodeGenerator002", + title: "Member same name as class", + messageFormat: "Ignored member '{0}' has same name as class", + category: "ResXFileCodeGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + private static readonly DiagnosticDescriptor s_memberWithStaticError = new( + id: "CatglobeResXFileCodeGenerator003", + title: "Incompatible settings", + messageFormat: "Cannot have static members/class with an class instance", + category: "ResXFileCodeGenerator", + 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.UseResManager) + 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.UseResManager) + GenerateCode(options, content, indent, containerClassName, builder, errorsAndWarnings, cancellationToken); + else + GenerateResourceManager(options, content, indent, containerClassName, builder, errorsAndWarnings, cancellationToken); + + if (options.KeyGeneration) + { + containerClassName = "Keys"; + builder.Append(indent); + builder.Append("public"); + + builder.Append(options.StaticClass ? " static" : string.Empty); + builder.Append(options.PartialClass ? " partial class " : " class "); + + builder.AppendLineLF(containerClassName); + builder.Append(indent); + builder.AppendLineLF("{"); + + indent += " "; + KeyGeneration(options, content, indent, "Keys", builder, errorsAndWarnings, + cancellationToken); + builder.AppendLineLF(" }"); + } + + 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", "\n" + indent + "/// "))); // Replace environment.NewLine to work around with RS1035 + 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/Catglobe.ResXFileCodeGenerator/Utilities.cs b/Catglobe.ResXFileCodeGenerator/Utilities.cs index f841229..d7b6076 100644 --- a/Catglobe.ResXFileCodeGenerator/Utilities.cs +++ b/Catglobe.ResXFileCodeGenerator/Utilities.cs @@ -13,7 +13,7 @@ private static bool IsValidLanguageName(string? languageName) return false; } - if (languageName.StartsWith("qps-", StringComparison.Ordinal)) + if (languageName!.StartsWith("qps-", StringComparison.Ordinal)) { return true; } diff --git a/Catglobe.ResXFileCodeGenerator/build/Catglobe.ResXFileCodeGenerator.props b/Catglobe.ResXFileCodeGenerator/build/Catglobe.ResXFileCodeGenerator.props index cef226e..661c5d7 100644 --- a/Catglobe.ResXFileCodeGenerator/build/Catglobe.ResXFileCodeGenerator.props +++ b/Catglobe.ResXFileCodeGenerator/build/Catglobe.ResXFileCodeGenerator.props @@ -19,6 +19,7 @@ + @@ -31,6 +32,7 @@ + diff --git a/README.md b/README.md index d9fb883..d7d2b13 100644 --- a/README.md +++ b/README.md @@ -470,6 +470,156 @@ It is also possible to set the namespace using the `CustomToolNamespace` setting ``` +## Key generation + +This function will extract all keys from the resx file and generate a class with all the keys as constants. + +```xml + + + true + + +``` +or +```xml + + + +``` + +Source resx file +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + SystemName + + +``` + + +Generation result +```C# +// ------------------------------------------------------------------------------ +// +// 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; + +public class ActivityEntrySortRuleNames +{ + private static ResourceManager? s_resourceManager; + public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("Catglobe.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)!; + + /// + /// Looks up a localized string similar to SystemName. + /// + public static string Sys_Name => ResourceManager.GetString("Sys.Name", CultureInfo)!; + public class Keys + { + + /// + /// Name of resource CreateDate. + /// + public const string CreateDate = nameof(CreateDate); + + /// + /// Name of resource CreateDateDescending. + /// + public const string CreateDateDescending = nameof(CreateDateDescending); + + /// + /// Name of resource Sys.Name. + /// + public const string Sys_Name = "Sys.Name"; + } +} +``` + + ## 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)