Skip to content

Commit 92ad9f8

Browse files
Copilotandrewlock
andauthored
Fix code generation for enum members using C# reserved keywords (#168)
* Initial plan * Initial exploration - understanding codebase structure Co-authored-by: andrewlock <18755388+andrewlock@users.noreply.github.com> * Fix enum member name escaping for C# keywords - Added EscapeIdentifier helper to prepend @ for C# keywords - Updated all usages of member.Key to use EscapeIdentifier - Added integration test for EnumWithReservedKeywords - Added unit test CanGenerateEnumWithReservedKeywords Co-authored-by: andrewlock <18755388+andrewlock@users.noreply.github.com> * Reenable .NET 10 testing * Refactor source gen helper to reduce allocation --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: andrewlock <18755388+andrewlock@users.noreply.github.com> Co-authored-by: Andrew Lock <andrewlock.net@gmail.com>
1 parent 41b1841 commit 92ad9f8

File tree

5 files changed

+815
-24
lines changed

5 files changed

+815
-24
lines changed

src/NetEscapades.EnumGenerators/SourceGenerationHelper.cs

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ public static string ToStringFast(this
111111
"""
112112
113113
114-
""").Append(fullyQualifiedName).Append('.').Append(member.Key)
115-
.Append(" => nameof(").Append(fullyQualifiedName).Append('.').Append(member.Key).Append("),");
114+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key)
115+
.Append(" => nameof(").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append("),");
116116
}
117117
}
118118

@@ -191,7 +191,7 @@ private static string ToStringFastWithMetadata(this
191191
"""
192192
193193
194-
""").Append(fullyQualifiedName).Append('.').Append(member.Key)
194+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key)
195195
.Append(" => ");
196196

197197
if (member.Value.GetMetadataName(metadataSource) is { } dn)
@@ -200,7 +200,7 @@ private static string ToStringFastWithMetadata(this
200200
}
201201
else
202202
{
203-
sb.Append("nameof(").Append(fullyQualifiedName).Append('.').Append(member.Key).Append("),");
203+
sb.Append("nameof(").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append("),");
204204
}
205205
}
206206
}
@@ -325,7 +325,7 @@ public static bool IsDefined(
325325
"""
326326
327327
328-
""").Append(fullyQualifiedName).Append('.').Append(member.Key)
328+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key)
329329
.Append(" => true,");
330330
}
331331
}
@@ -356,7 +356,7 @@ public static bool IsDefined(string name)
356356
"""
357357
358358
nameof(
359-
""").Append(fullyQualifiedName).Append('.').Append(member.Key).Append(") => true,");
359+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append(") => true,");
360360
}
361361

362362
sb.Append(
@@ -456,7 +456,7 @@ public static bool IsDefined(in global::System.ReadOnlySpan<char> name)
456456
457457
global::System.ReadOnlySpan<char> current when global::System.MemoryExtensions.Equals(current, nameof(
458458
""").Append(fullyQualifiedName).Append('.')
459-
.Append(member.Key)
459+
.AppendIdentifier(member.Key)
460460
.Append("), global::System.StringComparison.Ordinal) => true,");
461461
}
462462

@@ -825,7 +825,7 @@ private static bool TryParseIgnoreCase(
825825
"""
826826
, global::System.StringComparison.OrdinalIgnoreCase):
827827
value =
828-
""").Append(fullyQualifiedName).Append('.').Append(member.Key).Append(
828+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append(
829829
"""
830830
;
831831
return true;
@@ -856,11 +856,11 @@ private static bool TryParseIgnoreCase(
856856
"""
857857
858858
case string s when s.Equals(nameof(
859-
""").Append(fullyQualifiedName).Append('.').Append(member.Key).Append(
859+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append(
860860
"""
861861
), global::System.StringComparison.OrdinalIgnoreCase):
862862
value =
863-
""").Append(fullyQualifiedName).Append('.').Append(member.Key).Append(
863+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append(
864864
"""
865865
;
866866
return true;
@@ -924,7 +924,7 @@ private static bool TryParseWithCase(
924924
"""
925925
:
926926
value =
927-
""").Append(fullyQualifiedName).Append('.').Append(member.Key).Append(
927+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append(
928928
"""
929929
;
930930
return true;
@@ -955,11 +955,11 @@ private static bool TryParseWithCase(
955955
"""
956956
957957
case nameof(
958-
""").Append(fullyQualifiedName).Append('.').Append(member.Key).Append(
958+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append(
959959
"""
960960
):
961961
value =
962-
""").Append(fullyQualifiedName).Append('.').Append(member.Key).Append(
962+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append(
963963
"""
964964
;
965965
return true;
@@ -1263,7 +1263,7 @@ private static bool TryParseIgnoreCase(
12631263
"""
12641264
, global::System.StringComparison.OrdinalIgnoreCase):
12651265
result =
1266-
""").Append(fullyQualifiedName).Append('.').Append(member.Key).Append(
1266+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append(
12671267
"""
12681268
;
12691269
return true;
@@ -1295,11 +1295,11 @@ private static bool TryParseIgnoreCase(
12951295
12961296
case global::System.ReadOnlySpan<char> current when global::System.MemoryExtensions.Equals(current, nameof(
12971297
""").Append(fullyQualifiedName).Append('.')
1298-
.Append(member.Key).Append(
1298+
.AppendIdentifier(member.Key).Append(
12991299
"""
13001300
), global::System.StringComparison.OrdinalIgnoreCase):
13011301
result =
1302-
""").Append(fullyQualifiedName).Append('.').Append(member.Key).Append(
1302+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append(
13031303
"""
13041304
;
13051305
return true;
@@ -1366,7 +1366,7 @@ private static bool TryParseWithCase(
13661366
"""
13671367
, global::System.StringComparison.Ordinal):
13681368
result =
1369-
""").Append(fullyQualifiedName).Append('.').Append(member.Key).Append(
1369+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append(
13701370
"""
13711371
;
13721372
return true;
@@ -1398,11 +1398,11 @@ private static bool TryParseWithCase(
13981398
13991399
case global::System.ReadOnlySpan<char> current when global::System.MemoryExtensions.Equals(current, nameof(
14001400
""").Append(fullyQualifiedName).Append('.')
1401-
.Append(member.Key).Append(
1401+
.AppendIdentifier(member.Key).Append(
14021402
"""
14031403
), global::System.StringComparison.Ordinal):
14041404
result =
1405-
""").Append(fullyQualifiedName).Append('.').Append(member.Key).Append(
1405+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append(
14061406
"""
14071407
;
14081408
return true;
@@ -1429,7 +1429,7 @@ private static bool TryParseWithCase(
14291429
#endif
14301430
""");
14311431

1432-
var orderedNames = GetNamesOrderedByValue(enumToGenerate);
1432+
var orderedNames = GetNamesOrderedByValue(in enumToGenerate);
14331433
sb.Append(
14341434
"""
14351435
@@ -1461,7 +1461,7 @@ [] GetValues()
14611461
"""
14621462
14631463
1464-
""").Append(fullyQualifiedName).Append('.').Append(member.Key).Append(',');
1464+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append(',');
14651465
}
14661466

14671467
sb.Append(
@@ -1502,7 +1502,7 @@ [] GetValuesAsUnderlyingType()
15021502
"""
15031503
15041504
(
1505-
""").Append(enumToGenerate.UnderlyingType).Append(") ").Append(fullyQualifiedName).Append('.').Append(member.Key).Append(',');
1505+
""").Append(enumToGenerate.UnderlyingType).Append(") ").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append(',');
15061506
}
15071507

15081508
sb.Append(
@@ -1541,7 +1541,7 @@ public static string[] GetNames()
15411541
"""
15421542
15431543
nameof(
1544-
""").Append(fullyQualifiedName).Append('.').Append(member.Key).Append("),");
1544+
""").Append(fullyQualifiedName).Append('.').AppendIdentifier(member.Key).Append("),");
15451545
}
15461546

15471547
sb.Append(
@@ -1590,7 +1590,7 @@ public static string[] GetNames()
15901590
return (content, filename);
15911591
}
15921592

1593-
private static List<(string Key, EnumValueOption Value)> GetNamesOrderedByValue(EnumToGenerate enumToGenerate)
1593+
private static List<(string Key, EnumValueOption Value)> GetNamesOrderedByValue(in EnumToGenerate enumToGenerate)
15941594
{
15951595
// We order by underlying value, keeping the order of names with the same value, as they were defined
15961596
return enumToGenerate.Names
@@ -1600,4 +1600,14 @@ public static string[] GetNames()
16001600
.Select(tuple => tuple.name)
16011601
.ToList();
16021602
}
1603+
1604+
private static StringBuilder AppendIdentifier(this StringBuilder sb, string identifier)
1605+
{
1606+
if (SyntaxFacts.GetKeywordKind(identifier) != SyntaxKind.None)
1607+
{
1608+
sb.Append('@');
1609+
}
1610+
1611+
return sb.Append(identifier);
1612+
}
16031613
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using System;
2+
using Xunit;
3+
4+
#if INTEGRATION_TESTS
5+
namespace NetEscapades.EnumGenerators.IntegrationTests;
6+
#elif NETSTANDARD_INTEGRATION_TESTS
7+
namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests;
8+
#elif INTERCEPTOR_TESTS
9+
namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests;
10+
#elif NUGET_INTEGRATION_TESTS
11+
namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests;
12+
#elif NUGET_INTERCEPTOR_TESTS
13+
namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests;
14+
#else
15+
#error Unknown integration tests
16+
#endif
17+
18+
public class EnumWithReservedKeywordsExtensionsTests : ExtensionTests<EnumWithReservedKeywords, int, EnumWithReservedKeywordsExtensionsTests>, ITestData<EnumWithReservedKeywords>
19+
{
20+
public TheoryData<EnumWithReservedKeywords> ValidEnumValues() => new()
21+
{
22+
EnumWithReservedKeywords.number,
23+
EnumWithReservedKeywords.@string,
24+
EnumWithReservedKeywords.date,
25+
EnumWithReservedKeywords.@class,
26+
};
27+
28+
public TheoryData<string> ValuesToParse() => new()
29+
{
30+
"number",
31+
"string",
32+
"date",
33+
"class",
34+
"NUMBER",
35+
"STRING",
36+
"1",
37+
"2",
38+
"-1",
39+
};
40+
41+
protected override string[] GetNames() => EnumWithReservedKeywordsExtensions.GetNames();
42+
protected override EnumWithReservedKeywords[] GetValues() => EnumWithReservedKeywordsExtensions.GetValues();
43+
protected override int[] GetValuesAsUnderlyingType() => EnumWithReservedKeywordsExtensions.GetValuesAsUnderlyingType();
44+
protected override int AsUnderlyingValue(EnumWithReservedKeywords value) => value.AsUnderlyingType();
45+
46+
protected override string ToStringFast(EnumWithReservedKeywords value) => value.ToStringFast();
47+
protected override string ToStringFast(EnumWithReservedKeywords value, bool withMetadata) => value.ToStringFast(withMetadata);
48+
protected override bool IsDefined(EnumWithReservedKeywords value) => EnumWithReservedKeywordsExtensions.IsDefined(value);
49+
protected override bool IsDefined(string name, bool allowMatchingMetadataAttribute) => EnumWithReservedKeywordsExtensions.IsDefined(name, allowMatchingMetadataAttribute);
50+
#if READONLYSPAN
51+
protected override bool IsDefined(in ReadOnlySpan<char> name, bool allowMatchingMetadataAttribute = false) => EnumWithReservedKeywordsExtensions.IsDefined(name, allowMatchingMetadataAttribute);
52+
#endif
53+
protected override bool TryParse(string name, out EnumWithReservedKeywords parsed, bool ignoreCase, bool allowMatchingMetadataAttribute)
54+
=> EnumWithReservedKeywordsExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute);
55+
#if READONLYSPAN
56+
protected override bool TryParse(in ReadOnlySpan<char> name, out EnumWithReservedKeywords parsed, bool ignoreCase, bool allowMatchingMetadataAttribute)
57+
=> EnumWithReservedKeywordsExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute);
58+
#endif
59+
protected override EnumWithReservedKeywords Parse(string name, bool ignoreCase, bool allowMatchingMetadataAttribute)
60+
=> EnumWithReservedKeywordsExtensions.Parse(name, ignoreCase);
61+
#if READONLYSPAN
62+
protected override EnumWithReservedKeywords Parse(in ReadOnlySpan<char> name, bool ignoreCase, bool allowMatchingMetadataAttribute)
63+
=> EnumWithReservedKeywordsExtensions.Parse(name, ignoreCase);
64+
#endif
65+
}

tests/NetEscapades.EnumGenerators.IntegrationTests/Enums.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,17 @@ public enum EnumWithRepeatedValuesWithDisplayNames
187187
Second = 1,
188188
[EnumMember(Value = "Repeated")] Third = 0, // Repeated value with display name
189189
}
190+
191+
[EnumExtensions(MetadataSource = MetadataSource.DescriptionAttribute)]
192+
public enum EnumWithReservedKeywords
193+
{
194+
[Description("number")]
195+
number,
196+
[Description("string")]
197+
@string,
198+
[Description("date")]
199+
date,
200+
[Description("class")]
201+
@class,
202+
}
190203
}

tests/NetEscapades.EnumGenerators.Tests/EnumGeneratorTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,33 @@ public enum MyEnum
694694
return Verifier.Verify(output, Settings());
695695
}
696696

697+
[Fact]
698+
public Task CanGenerateEnumWithReservedKeywords()
699+
{
700+
const string input =
701+
"""
702+
using System.ComponentModel;
703+
using NetEscapades.EnumGenerators;
704+
705+
[EnumExtensions(MetadataSource = MetadataSource.DescriptionAttribute)]
706+
public enum EnumWithReservedKeywords
707+
{
708+
[Description("number")]
709+
number,
710+
[Description("string")]
711+
@string,
712+
[Description("date")]
713+
date,
714+
[Description("class")]
715+
@class,
716+
}
717+
""";
718+
var (diagnostics, output) = TestHelpers.GetGeneratedOutput(Generators(), new(input));
719+
720+
Assert.Empty(diagnostics);
721+
return Verifier.Verify(output, Settings());
722+
}
723+
697724
[Fact]
698725
public void DoesNotGenerateWithoutAttribute()
699726
{

0 commit comments

Comments
 (0)