-
Notifications
You must be signed in to change notification settings - Fork 0
fix: Accept format specifiers #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,10 @@ | ||
| ### New Rules | ||
|
|
||
| | Rule ID | Category | Severity | Notes | | ||
|
Check warning on line 3 in src/Darp.Utils.ResxSourceGenerator/AnalyzerReleases.Unshipped.md
|
||
| |-------------|---------------|----------|--------------------------------------| | ||
| | DarpResX001 | Globalization | Warning | Empty resource file | | ||
| | DarpResX002 | Globalization | Warning | Invalid Key in resource file | | ||
| | DarpResX003 | Globalization | Warning | Missing Value in resource file | | ||
| | DarpResX004 | Globalization | Warning | Duplicate Key in resource file | | ||
| | DarpResX005 | Globalization | Warning | Missing translation for specific Key | | ||
| | DarpResX006 | Globalization | Warning | Mixed format argument styles | | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,7 +3,6 @@ namespace Darp.Utils.ResxSourceGenerator; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Diagnostics.CodeAnalysis; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Globalization; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Text; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Text.RegularExpressions; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Xml; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Xml.Linq; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Microsoft.CodeAnalysis; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -65,6 +64,16 @@ internal static class BuildHelper | |||||||||||||||||||||||||||||||||||||||||||||||||||
| isEnabledByDefault: true | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static readonly DiagnosticDescriptor MixedFormatArgumentsWarning = new( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: "DarpResX006", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Mixed format argument styles", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| messageFormat: "Entry with key '{0}' mixes named and numbered format items and will not get a format method", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| category: "Globalization", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| defaultSeverity: DiagnosticSeverity.Warning, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| helpLinkUri: HelpLinkUri, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| isEnabledByDefault: true | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| public static bool TryGenerateSource( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ResourceCollection resourceCollection, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| in List<Diagnostic> diagnostics, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -97,6 +106,7 @@ out var namespaceEnd | |||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| string? getStringMethod = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| string? formatHelperMethods = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (resourceInformation.Settings.EmitFormatMethods) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| getStringMethod += $$$$""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -106,11 +116,115 @@ out var namespaceEnd | |||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (formatterNames == null) return value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} for (var i = 0; i < formatterNames.Length; i++) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} value = value.Replace($"{{{formatterNames[i]}}}", $"{{{i}}}"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} value = ReplaceNamedFormatItem(value, formatterNames[i], i); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} return value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| """; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| formatHelperMethods += $$$$""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}}private static string ReplaceNamedFormatItem(string value, string formatterName, int index) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}}{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} global::System.Text.StringBuilder? builder = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} var appendFrom = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} for (var i = 0; i < value.Length; i++) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (value[i] != '{') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (i + 1 < value.Length && value[i + 1] == '{') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} i++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} var nameStart = i + 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (!IsMatchAt(value, nameStart, formatterName)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} var suffixStart = nameStart + formatterName.Length; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (suffixStart >= value.Length) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} var suffixEnd = suffixStart; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} while (suffixEnd < value.Length && value[suffixEnd] != '}') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (value[suffixEnd] == '{') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} suffixEnd = -1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} suffixEnd++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (suffixEnd < 0 || suffixEnd >= value.Length) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (!IsValidFormatSuffix(value, suffixStart, suffixEnd)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} builder ??= new global::System.Text.StringBuilder(value.Length); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} builder.Append(value, appendFrom, i - appendFrom); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} builder.Append('{').Append(index); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} builder.Append(value, suffixStart, suffixEnd - suffixStart); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} builder.Append('}'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} appendFrom = suffixEnd + 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} i = suffixEnd; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (builder == null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} return value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} builder.Append(value, appendFrom, value.Length - appendFrom); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} return builder.ToString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}}private static bool IsMatchAt(string value, int start, string formatterName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}}{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (start + formatterName.Length > value.Length) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} for (var i = 0; i < formatterName.Length; i++) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (value[start + i] != formatterName[i]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}}private static bool IsValidFormatSuffix(string value, int start, int end) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}}{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (start == end) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (value[start] == ':') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (value[start] != ',') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} var i = start + 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} while (i < end && char.IsWhiteSpace(value[i])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} i++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (i < end && value[i] == '-') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} i++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} var digitStart = i; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} while (i < end && char.IsDigit(value[i])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} i++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (i == digitStart) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} if (i == end) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}} return value[i] == ':'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{{{memberIndent}}}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| """; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var defaultClass = $$""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -159,7 +273,7 @@ out var namespaceEnd | |||||||||||||||||||||||||||||||||||||||||||||||||||
| {{memberIndent}}{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{keysMembers}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{memberIndent}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{classIndent}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{formatHelperMethods}}{{classIndent}}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var debugInformation = resourceCollection.GenerateDebugInformation(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var result = $""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -277,6 +391,18 @@ CancellationToken cancellationToken | |||||||||||||||||||||||||||||||||||||||||||||||||||
| var resourceString = new ResourceString(propertyIdentifier, value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (resourceString.HasArguments) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (resourceString.HasMixedArguments) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| diagnostics.Add( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Diagnostic.Create( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| descriptor: MixedFormatArgumentsWarning, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| location: Location.Create(resourceInformation.ResourceFile.Path, default, default), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| messageArgs: [name] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+394
to
+405
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win Don’t This skips key generation for the resource after the property has already been emitted, so the generated property can reference a missing Proposed fix if (resourceString.HasMixedArguments)
{
diagnostics.Add(
Diagnostic.Create(
descriptor: MixedFormatArgumentsWarning,
location: Location.Create(resourceInformation.ResourceFile.Path, default, default),
messageArgs: [name]
)
);
- continue;
}
-
- RenderFormatMethod(memberIndent, membersBuilder, resourceString);
+ else
+ {
+ RenderFormatMethod(memberIndent, membersBuilder, resourceString);
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| RenderFormatMethod(memberIndent, membersBuilder, resourceString); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -574,41 +700,25 @@ public static string GetIdentifierFromResourceName(string name) | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly struct ResourceString | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static readonly Regex NamedParameterMatcher = new( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| @"\{([a-z]\w*)\}", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| RegexOptions.IgnoreCase | RegexOptions.Compiled | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static readonly Regex NumberParameterMatcher = new(@"\{(\d+)\}", RegexOptions.Compiled); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly IReadOnlyList<string> _arguments; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| public ResourceString(string identifier, string value) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Identifier = identifier; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Value = value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| MatchCollection match = NamedParameterMatcher.Matches(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| UsingNamedArgs = match.Count > 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!UsingNamedArgs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| match = NumberParameterMatcher.Matches(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| IEnumerable<string> arguments = match.Cast<Match>().Select(m => m.Groups[1].Value).Distinct(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!UsingNamedArgs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| arguments = arguments.OrderBy(Convert.ToInt32); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| _arguments = arguments.ToList(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _arguments = ResourceFormatHelper.GetArguments(value, out var usingNamedArgs, out var hasMixedArguments); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| UsingNamedArgs = usingNamedArgs; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| HasMixedArguments = hasMixedArguments; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| public string Identifier { get; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| public string Value { get; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| public bool UsingNamedArgs { get; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| public bool HasArguments => _arguments.Count > 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| public bool HasArguments => _arguments.Count > 0 || HasMixedArguments; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| public bool HasMixedArguments { get; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| public string GetArgumentNames() => string.Join(", ", _arguments.Select(a => "\"" + a + "\"")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| namespace Darp.Utils.ResxSourceGenerator; | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Globalization; | ||
| using System.Linq; | ||
|
|
||
| internal static class ResourceFormatHelper | ||
| { | ||
| public static IReadOnlyList<string> GetArguments(string value, out bool usingNamedArgs) | ||
| { | ||
| return GetArguments(value, out usingNamedArgs, out _); | ||
| } | ||
|
|
||
| public static IReadOnlyList<string> GetArguments(string value, out bool usingNamedArgs, out bool hasMixedArguments) | ||
| { | ||
| var namedArguments = new List<string>(); | ||
| var numberedArguments = new List<string>(); | ||
|
|
||
| for (var i = 0; i < value.Length; i++) | ||
| { | ||
| if (value[i] != '{') | ||
| continue; | ||
| if (i + 1 < value.Length && value[i + 1] == '{') | ||
| { | ||
| i++; | ||
| continue; | ||
| } | ||
|
|
||
| if (!TryReadFormatItem(value, i, out var argument, out var isNamed, out var end)) | ||
| continue; | ||
|
|
||
| List<string> arguments = isNamed ? namedArguments : numberedArguments; | ||
| if (!arguments.Contains(argument)) | ||
| { | ||
| arguments.Add(argument); | ||
| } | ||
|
|
||
| i = end; | ||
| } | ||
|
|
||
| hasMixedArguments = namedArguments.Count > 0 && numberedArguments.Count > 0; | ||
| if (hasMixedArguments) | ||
| { | ||
| usingNamedArgs = false; | ||
| return []; | ||
| } | ||
|
|
||
| usingNamedArgs = namedArguments.Count > 0; | ||
| if (usingNamedArgs) | ||
| { | ||
| return namedArguments; | ||
| } | ||
|
|
||
| if (numberedArguments.Count == 0) | ||
| { | ||
| return numberedArguments; | ||
| } | ||
|
|
||
| var maxArgumentIndex = numberedArguments.Select(x => Convert.ToInt32(x, CultureInfo.InvariantCulture)).Max(); | ||
| return Enumerable.Range(0, maxArgumentIndex + 1).Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList(); | ||
|
Comment on lines
+60
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Guard oversized numeric format indexes before building the contiguous range.
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| private static bool TryReadFormatItem( | ||
| string value, | ||
| int openBrace, | ||
| out string argument, | ||
| out bool isNamed, | ||
| out int closeBrace | ||
| ) | ||
| { | ||
| argument = ""; | ||
| isNamed = false; | ||
| closeBrace = -1; | ||
|
|
||
| var argumentStart = openBrace + 1; | ||
| if (argumentStart >= value.Length) | ||
| return false; | ||
|
|
||
| var argumentEnd = argumentStart; | ||
| if (char.IsDigit(value[argumentEnd])) | ||
| { | ||
| while (argumentEnd < value.Length && char.IsDigit(value[argumentEnd])) | ||
| { | ||
| argumentEnd++; | ||
| } | ||
| } | ||
| else if (IsIdentifierStart(value[argumentEnd])) | ||
| { | ||
| isNamed = true; | ||
| while (argumentEnd < value.Length && IsIdentifierPart(value[argumentEnd])) | ||
| { | ||
| argumentEnd++; | ||
| } | ||
| } | ||
| else | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| closeBrace = argumentEnd; | ||
| while (closeBrace < value.Length && value[closeBrace] != '}') | ||
| { | ||
| if (value[closeBrace] == '{') | ||
| { | ||
| closeBrace = -1; | ||
| break; | ||
| } | ||
|
|
||
| closeBrace++; | ||
| } | ||
|
|
||
| if (closeBrace < 0 || closeBrace >= value.Length) | ||
| return false; | ||
| if (!IsValidFormatSuffix(value, argumentEnd, closeBrace)) | ||
| return false; | ||
|
|
||
| argument = value.Substring(argumentStart, argumentEnd - argumentStart); | ||
| return true; | ||
| } | ||
|
|
||
| private static bool IsValidFormatSuffix(string value, int start, int end) | ||
| { | ||
| if (start == end) | ||
| return true; | ||
| if (value[start] == ':') | ||
| return true; | ||
| if (value[start] != ',') | ||
| return false; | ||
|
|
||
| var i = start + 1; | ||
| while (i < end && char.IsWhiteSpace(value[i])) | ||
| { | ||
| i++; | ||
| } | ||
| if (i < end && value[i] == '-') | ||
| { | ||
| i++; | ||
| } | ||
|
|
||
| var digitStart = i; | ||
| while (i < end && char.IsDigit(value[i])) | ||
| { | ||
| i++; | ||
| } | ||
|
|
||
| if (i == digitStart) | ||
| return false; | ||
| if (i == end) | ||
| return true; | ||
| return value[i] == ':'; | ||
| } | ||
|
|
||
| private static bool IsIdentifierStart(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z'; | ||
|
|
||
| private static bool IsIdentifierPart(char c) => IsIdentifierStart(c) || c is (>= '0' and <= '9') or '_'; | ||
|
Comment on lines
+154
to
+156
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For named placeholders that contain a valid non-ASCII C# identifier character after the first ASCII letter, such as Useful? React with 👍 / 👎. |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a resource value mixes numbered and named items (for example
{0} {name}) withEmitFormatMethodsenabled, thiscontinueexits the entire resource loop after the property has already been emitted but before the correspondingKeysconstant is appended. The warning says only the format method should be omitted, but the generated property still referencesKeys.@..., so this case turns into a compilation error instead of a usable string property plus diagnostic.Useful? React with 👍 / 👎.