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)