@alloy-js/csharp 0.23.0-dev.8 → 0.24.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/dev/src/components/method/method.test.js +64 -0
  3. package/dist/dev/src/components/method/method.test.js.map +1 -1
  4. package/dist/dev/src/components/namespace/namespace.js +2 -1
  5. package/dist/dev/src/components/namespace/namespace.js.map +1 -1
  6. package/dist/dev/src/components/namespace/namespace.test.js +13 -10
  7. package/dist/dev/src/components/namespace/namespace.test.js.map +1 -1
  8. package/dist/dev/src/components/namespace.ref.test.js +54 -42
  9. package/dist/dev/src/components/namespace.ref.test.js.map +1 -1
  10. package/dist/dev/src/components/property/property.js +101 -24
  11. package/dist/dev/src/components/property/property.js.map +1 -1
  12. package/dist/dev/src/components/property/property.test.js +140 -0
  13. package/dist/dev/src/components/property/property.test.js.map +1 -1
  14. package/dist/dev/src/components/source-file/source-file.js +12 -11
  15. package/dist/dev/src/components/source-file/source-file.js.map +1 -1
  16. package/dist/dev/src/identifier-utils.js +45 -0
  17. package/dist/dev/src/identifier-utils.js.map +1 -0
  18. package/dist/dev/src/index.js +2 -0
  19. package/dist/dev/src/index.js.map +1 -1
  20. package/dist/dev/src/keywords.js +39 -0
  21. package/dist/dev/src/keywords.js.map +1 -0
  22. package/dist/dev/src/name-policy.js +29 -6
  23. package/dist/dev/src/name-policy.js.map +1 -1
  24. package/dist/dev/src/name-policy.test.js +167 -0
  25. package/dist/dev/src/name-policy.test.js.map +1 -0
  26. package/dist/src/components/method/method.test.js +48 -0
  27. package/dist/src/components/method/method.test.js.map +1 -1
  28. package/dist/src/components/namespace/namespace.js +2 -1
  29. package/dist/src/components/namespace/namespace.js.map +1 -1
  30. package/dist/src/components/namespace/namespace.test.js +6 -3
  31. package/dist/src/components/namespace/namespace.test.js.map +1 -1
  32. package/dist/src/components/namespace.ref.test.js +24 -12
  33. package/dist/src/components/namespace.ref.test.js.map +1 -1
  34. package/dist/src/components/property/property.d.ts +42 -4
  35. package/dist/src/components/property/property.d.ts.map +1 -1
  36. package/dist/src/components/property/property.js +64 -11
  37. package/dist/src/components/property/property.js.map +1 -1
  38. package/dist/src/components/property/property.test.js +104 -0
  39. package/dist/src/components/property/property.test.js.map +1 -1
  40. package/dist/src/components/source-file/source-file.d.ts.map +1 -1
  41. package/dist/src/components/source-file/source-file.js +3 -2
  42. package/dist/src/components/source-file/source-file.js.map +1 -1
  43. package/dist/src/identifier-utils.d.ts +22 -0
  44. package/dist/src/identifier-utils.d.ts.map +1 -0
  45. package/dist/src/identifier-utils.js +45 -0
  46. package/dist/src/identifier-utils.js.map +1 -0
  47. package/dist/src/index.d.ts +2 -0
  48. package/dist/src/index.d.ts.map +1 -1
  49. package/dist/src/index.js +2 -0
  50. package/dist/src/index.js.map +1 -1
  51. package/dist/src/keywords.d.ts +32 -0
  52. package/dist/src/keywords.d.ts.map +1 -0
  53. package/dist/src/keywords.js +39 -0
  54. package/dist/src/keywords.js.map +1 -0
  55. package/dist/src/name-policy.d.ts +7 -0
  56. package/dist/src/name-policy.d.ts.map +1 -1
  57. package/dist/src/name-policy.js +29 -6
  58. package/dist/src/name-policy.js.map +1 -1
  59. package/dist/src/name-policy.test.d.ts +2 -0
  60. package/dist/src/name-policy.test.d.ts.map +1 -0
  61. package/dist/src/name-policy.test.js +167 -0
  62. package/dist/src/name-policy.test.js.map +1 -0
  63. package/dist/tsconfig.tsbuildinfo +1 -1
  64. package/docs/api/components/Property.md +30 -30
  65. package/docs/api/contexts/csharp-context.md +21 -0
  66. package/docs/api/contexts/index.md +3 -0
  67. package/docs/api/functions/createCSharpNamePolicy.md +4 -0
  68. package/docs/api/functions/index.md +5 -1
  69. package/docs/api/functions/isCSharpContextualKeyword.md +20 -0
  70. package/docs/api/functions/isCSharpKeyword.md +22 -0
  71. package/docs/api/functions/isValidCSharpIdentifier.md +20 -0
  72. package/docs/api/functions/sanitizeCSharpIdentifier.md +24 -0
  73. package/docs/api/index.md +3 -2
  74. package/docs/api/variables/csharpKeywords.md +11 -0
  75. package/docs/api/variables/index.md +1 -0
  76. package/package.json +6 -6
  77. package/src/components/method/method.test.tsx +36 -0
  78. package/src/components/namespace/namespace.test.tsx +6 -3
  79. package/src/components/namespace/namespace.tsx +2 -2
  80. package/src/components/namespace.ref.test.tsx +24 -12
  81. package/src/components/property/property.test.tsx +89 -0
  82. package/src/components/property/property.tsx +111 -16
  83. package/src/components/source-file/source-file.tsx +1 -4
  84. package/src/identifier-utils.ts +45 -0
  85. package/src/index.ts +2 -0
  86. package/src/keywords.ts +162 -0
  87. package/src/name-policy.test.ts +210 -0
  88. package/src/name-policy.ts +30 -6
  89. package/temp/api.json +253 -7
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Block,
2
3
  Children,
3
4
  createSymbolSlot,
4
5
  List,
@@ -54,11 +55,49 @@ export interface PropertyProps extends AccessModifiers, PropertyModifiers {
54
55
  /** Property type */
55
56
  type: Children;
56
57
 
57
- /** If property should have a getter */
58
- get?: boolean;
58
+ /**
59
+ * If property should have a getter. Pass `true` for an auto-property getter (`get;`),
60
+ * or pass children for a getter with a body.
61
+ *
62
+ * @example auto-property
63
+ * ```tsx
64
+ * <Property name="Name" type="string" get set />
65
+ * ```
66
+ * Produces: `string Name { get; set; }`
67
+ *
68
+ * @example with body
69
+ * ```tsx
70
+ * <Property name="Name" type="string" get={<>return _name;</>} set />
71
+ * ```
72
+ * Produces:
73
+ * ```csharp
74
+ * string Name
75
+ * {
76
+ * get { return _name; }
77
+ * set;
78
+ * }
79
+ * ```
80
+ */
81
+ get?: boolean | Children;
59
82
 
60
- /** If property should have a setter */
61
- set?: boolean;
83
+ /**
84
+ * If property should have a setter. Pass `true` for an auto-property setter (`set;`),
85
+ * or pass children for a setter with a body.
86
+ *
87
+ * @example with body
88
+ * ```tsx
89
+ * <Property name="Value" type="int" get set={<>_value = value;</>} />
90
+ * ```
91
+ * Produces:
92
+ * ```csharp
93
+ * int Value
94
+ * {
95
+ * get;
96
+ * set { _value = value; }
97
+ * }
98
+ * ```
99
+ */
100
+ set?: boolean | Children;
62
101
 
63
102
  /** If property should only be set on the type creation */
64
103
  init?: boolean;
@@ -133,14 +172,42 @@ export function Property(props: PropertyProps) {
133
172
  `Cannot use 'init' and 'set' together on property '${name}'`,
134
173
  );
135
174
  }
136
- // note that scope wraps the method decl so that the params get the correct scope
175
+
176
+ const hasAccessorBody =
177
+ (props.get && props.get !== true) || (props.set && props.set !== true);
178
+
137
179
  return (
138
180
  <MemberDeclaration symbol={propertySymbol}>
139
181
  <DocWhen doc={props.doc} />
140
182
  <AttributeList attributes={props.attributes} endline />
141
183
  {modifiers}
142
184
  <TypeSlot>{props.type}</TypeSlot>
143
- {props.nullable && "?"} <MemberName /> {"{ "}
185
+ {props.nullable && "?"} <MemberName />
186
+ {hasAccessorBody ?
187
+ <AccessorBlock get={props.get} set={props.set} init={props.init} />
188
+ : <AutoAccessors
189
+ get={props.get}
190
+ set={props.set}
191
+ init={props.init}
192
+ initializer={props.initializer}
193
+ />
194
+ }
195
+ </MemberDeclaration>
196
+ );
197
+ }
198
+
199
+ interface AutoAccessorsProps {
200
+ get?: boolean | Children;
201
+ set?: boolean | Children;
202
+ init?: boolean;
203
+ initializer?: Children;
204
+ }
205
+
206
+ function AutoAccessors(props: AutoAccessorsProps) {
207
+ return (
208
+ <group>
209
+ {" "}
210
+ {"{ "}
144
211
  <List joiner=" ">
145
212
  {props.get && "get;"}
146
213
  {props.set && "set;"}
@@ -148,20 +215,48 @@ export function Property(props: PropertyProps) {
148
215
  </List>
149
216
  {" }"}
150
217
  {props.initializer && (
151
- <PropertyInitializer>{props.initializer}</PropertyInitializer>
218
+ <>
219
+ {" ="}
220
+ <indent>
221
+ <line />
222
+ {props.initializer};
223
+ </indent>
224
+ </>
152
225
  )}
153
- </MemberDeclaration>
226
+ </group>
227
+ );
228
+ }
229
+
230
+ interface AccessorBlockProps {
231
+ get?: boolean | Children;
232
+ set?: boolean | Children;
233
+ init?: boolean;
234
+ }
235
+
236
+ function AccessorBlock(props: AccessorBlockProps) {
237
+ return (
238
+ <Block newline>
239
+ <List hardline>
240
+ {props.get && <Accessor keyword="get" body={props.get} />}
241
+ {props.set && <Accessor keyword="set" body={props.set} />}
242
+ {props.init && "init;"}
243
+ </List>
244
+ </Block>
154
245
  );
155
246
  }
156
247
 
157
- function PropertyInitializer(props: { children: Children }) {
248
+ interface AccessorProps {
249
+ keyword: string;
250
+ body: boolean | Children;
251
+ }
252
+
253
+ function Accessor(props: AccessorProps) {
254
+ if (props.body === true) {
255
+ return <>{props.keyword};</>;
256
+ }
158
257
  return (
159
- <group>
160
- {" ="}
161
- <indent>
162
- <line />
163
- {props.children};
164
- </indent>
165
- </group>
258
+ <>
259
+ {props.keyword} <Block inline>{props.body}</Block>
260
+ </>
166
261
  );
167
262
  }
@@ -105,10 +105,7 @@ export function SourceFile(props: SourceFileProps) {
105
105
  : <>
106
106
  namespace <NamespaceName symbol={nsSymbol} />
107
107
  {sourceFileScope.hasBlockNamespace ?
108
- <>
109
- {" "}
110
- <Block>{content}</Block>
111
- </>
108
+ <Block newline>{content}</Block>
112
109
  : <>
113
110
  ;<hbr />
114
111
  <hbr />
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Checks whether the provided name is a valid C# identifier (without `@` prefix).
3
+ * Does not account for keyword conflicts — use {@link isCSharpKeyword} for that.
4
+ *
5
+ * @param name - The name to validate.
6
+ * @returns true if the name matches C# identifier rules (letter or underscore start, word chars after).
7
+ */
8
+ export function isValidCSharpIdentifier(name: string): boolean {
9
+ return /^[A-Za-z_]\w*$/.test(name);
10
+ }
11
+
12
+ /**
13
+ * Transforms an arbitrary string into a valid C# identifier by replacing
14
+ * invalid characters. The result may still be a C# keyword — callers
15
+ * should combine with keyword escaping if needed.
16
+ *
17
+ * - If the first character is not a letter or underscore, a `_` prefix is added.
18
+ * - Subsequent non-word characters are replaced with `_`.
19
+ * - Empty strings become `_`.
20
+ *
21
+ * @param name - The string to sanitize.
22
+ * @returns A string that satisfies C# identifier character rules.
23
+ */
24
+ export function sanitizeCSharpIdentifier(name: string): string {
25
+ if (name.length === 0) return "_";
26
+
27
+ const chars: string[] = [];
28
+ for (let i = 0; i < name.length; i++) {
29
+ const ch = name[i];
30
+ if (i === 0) {
31
+ if (/[A-Za-z_]/.test(ch)) {
32
+ chars.push(ch);
33
+ } else {
34
+ chars.push("_");
35
+ // Keep the original char if it's a word char (e.g., digit)
36
+ if (/\w/.test(ch)) {
37
+ chars.push(ch);
38
+ }
39
+ }
40
+ } else {
41
+ chars.push(/\w/.test(ch) ? ch : "_");
42
+ }
43
+ }
44
+ return chars.join("");
45
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@ export * from "./access.jsx";
2
2
  export * from "./components/index.js";
3
3
  export * from "./contexts/format-options.js";
4
4
  export * from "./create-library.js";
5
+ export * from "./identifier-utils.js";
6
+ export * from "./keywords.js";
5
7
  export * from "./modifiers.js";
6
8
  export * from "./name-policy.js";
7
9
  export * from "./scopes/index.js";
@@ -0,0 +1,162 @@
1
+ /**
2
+ * C# reserved keywords that cannot be used as identifiers without `@` prefix.
3
+ * These are case-sensitive in C#.
4
+ *
5
+ * @see https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/
6
+ */
7
+ export const csharpKeywords: ReadonlySet<string> = new Set([
8
+ "abstract",
9
+ "as",
10
+ "base",
11
+ "bool",
12
+ "break",
13
+ "byte",
14
+ "case",
15
+ "catch",
16
+ "char",
17
+ "checked",
18
+ "class",
19
+ "const",
20
+ "continue",
21
+ "decimal",
22
+ "default",
23
+ "delegate",
24
+ "do",
25
+ "double",
26
+ "else",
27
+ "enum",
28
+ "event",
29
+ "explicit",
30
+ "extern",
31
+ "false",
32
+ "finally",
33
+ "fixed",
34
+ "float",
35
+ "for",
36
+ "foreach",
37
+ "goto",
38
+ "if",
39
+ "implicit",
40
+ "in",
41
+ "int",
42
+ "interface",
43
+ "internal",
44
+ "is",
45
+ "lock",
46
+ "long",
47
+ "namespace",
48
+ "new",
49
+ "null",
50
+ "object",
51
+ "operator",
52
+ "out",
53
+ "override",
54
+ "params",
55
+ "private",
56
+ "protected",
57
+ "public",
58
+ "readonly",
59
+ "ref",
60
+ "return",
61
+ "sbyte",
62
+ "sealed",
63
+ "short",
64
+ "sizeof",
65
+ "stackalloc",
66
+ "static",
67
+ "string",
68
+ "struct",
69
+ "switch",
70
+ "this",
71
+ "throw",
72
+ "true",
73
+ "try",
74
+ "typeof",
75
+ "uint",
76
+ "ulong",
77
+ "unchecked",
78
+ "unsafe",
79
+ "ushort",
80
+ "using",
81
+ "virtual",
82
+ "void",
83
+ "volatile",
84
+ "while",
85
+ ]);
86
+
87
+ /**
88
+ * C# contextual keywords that are reserved in certain contexts.
89
+ * While not always reserved, treating them as keywords in generated code
90
+ * avoids subtle context-dependent issues.
91
+ *
92
+ * @see https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/#contextual-keywords
93
+ */
94
+ export const csharpContextualKeywords: ReadonlySet<string> = new Set([
95
+ "add",
96
+ "allows",
97
+ "alias",
98
+ "and",
99
+ "ascending",
100
+ "args",
101
+ "async",
102
+ "await",
103
+ "by",
104
+ "descending",
105
+ "dynamic",
106
+ "equals",
107
+ "field",
108
+ "file",
109
+ "from",
110
+ "get",
111
+ "global",
112
+ "group",
113
+ "init",
114
+ "into",
115
+ "join",
116
+ "let",
117
+ "managed",
118
+ "nameof",
119
+ "nint",
120
+ "not",
121
+ "notnull",
122
+ "nuint",
123
+ "on",
124
+ "or",
125
+ "orderby",
126
+ "partial",
127
+ "record",
128
+ "remove",
129
+ "required",
130
+ "scoped",
131
+ "select",
132
+ "set",
133
+ "unmanaged",
134
+ "value",
135
+ "var",
136
+ "when",
137
+ "where",
138
+ "with",
139
+ "yield",
140
+ ]);
141
+
142
+ /**
143
+ * Returns true if the given name is a C# reserved keyword.
144
+ * The check is case-sensitive, matching C# language semantics.
145
+ *
146
+ * Note: this only checks reserved keywords, not contextual keywords.
147
+ * Contextual keywords are only reserved in specific language contexts
148
+ * and are valid identifiers elsewhere (e.g., `value` is valid as a parameter name).
149
+ * Use {@link isCSharpContextualKeyword} to check contextual keywords separately.
150
+ */
151
+ export function isCSharpKeyword(name: string): boolean {
152
+ return csharpKeywords.has(name);
153
+ }
154
+
155
+ /**
156
+ * Returns true if the given name is a C# contextual keyword.
157
+ * Contextual keywords are only reserved in specific language contexts
158
+ * and are generally valid as identifiers.
159
+ */
160
+ export function isCSharpContextualKeyword(name: string): boolean {
161
+ return csharpContextualKeywords.has(name);
162
+ }
@@ -0,0 +1,210 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ isValidCSharpIdentifier,
4
+ sanitizeCSharpIdentifier,
5
+ } from "./identifier-utils.js";
6
+ import {
7
+ csharpContextualKeywords,
8
+ csharpKeywords,
9
+ isCSharpContextualKeyword,
10
+ isCSharpKeyword,
11
+ } from "./keywords.js";
12
+ import { createCSharpNamePolicy } from "./name-policy.js";
13
+
14
+ describe("isCSharpKeyword", () => {
15
+ it("recognizes reserved keywords", () => {
16
+ expect(isCSharpKeyword("class")).toBe(true);
17
+ expect(isCSharpKeyword("interface")).toBe(true);
18
+ expect(isCSharpKeyword("string")).toBe(true);
19
+ expect(isCSharpKeyword("int")).toBe(true);
20
+ expect(isCSharpKeyword("void")).toBe(true);
21
+ expect(isCSharpKeyword("return")).toBe(true);
22
+ });
23
+
24
+ it("does not match contextual keywords", () => {
25
+ expect(isCSharpKeyword("async")).toBe(false);
26
+ expect(isCSharpKeyword("value")).toBe(false);
27
+ expect(isCSharpKeyword("var")).toBe(false);
28
+ expect(isCSharpKeyword("record")).toBe(false);
29
+ });
30
+
31
+ it("is case-sensitive — PascalCase versions are not keywords", () => {
32
+ expect(isCSharpKeyword("Class")).toBe(false);
33
+ expect(isCSharpKeyword("String")).toBe(false);
34
+ expect(isCSharpKeyword("Int")).toBe(false);
35
+ expect(isCSharpKeyword("Void")).toBe(false);
36
+ });
37
+
38
+ it("rejects non-keywords", () => {
39
+ expect(isCSharpKeyword("MyClass")).toBe(false);
40
+ expect(isCSharpKeyword("foo")).toBe(false);
41
+ });
42
+
43
+ it("keyword sets are non-empty", () => {
44
+ expect(csharpKeywords.size).toBeGreaterThan(70);
45
+ expect(csharpContextualKeywords.size).toBeGreaterThan(30);
46
+ });
47
+ });
48
+
49
+ describe("isCSharpContextualKeyword", () => {
50
+ it("recognizes contextual keywords", () => {
51
+ expect(isCSharpContextualKeyword("async")).toBe(true);
52
+ expect(isCSharpContextualKeyword("await")).toBe(true);
53
+ expect(isCSharpContextualKeyword("value")).toBe(true);
54
+ expect(isCSharpContextualKeyword("var")).toBe(true);
55
+ expect(isCSharpContextualKeyword("record")).toBe(true);
56
+ expect(isCSharpContextualKeyword("required")).toBe(true);
57
+ });
58
+
59
+ it("does not match reserved keywords", () => {
60
+ expect(isCSharpContextualKeyword("class")).toBe(false);
61
+ expect(isCSharpContextualKeyword("int")).toBe(false);
62
+ });
63
+ });
64
+
65
+ describe("isValidCSharpIdentifier", () => {
66
+ it("accepts valid identifiers", () => {
67
+ expect(isValidCSharpIdentifier("MyClass")).toBe(true);
68
+ expect(isValidCSharpIdentifier("_private")).toBe(true);
69
+ expect(isValidCSharpIdentifier("name123")).toBe(true);
70
+ expect(isValidCSharpIdentifier("a")).toBe(true);
71
+ expect(isValidCSharpIdentifier("_")).toBe(true);
72
+ });
73
+
74
+ it("rejects invalid identifiers", () => {
75
+ expect(isValidCSharpIdentifier("123start")).toBe(false);
76
+ expect(isValidCSharpIdentifier("has-dash")).toBe(false);
77
+ expect(isValidCSharpIdentifier("has space")).toBe(false);
78
+ expect(isValidCSharpIdentifier("")).toBe(false);
79
+ expect(isValidCSharpIdentifier("foo.bar")).toBe(false);
80
+ });
81
+
82
+ it("does not check for keywords (only character rules)", () => {
83
+ // "class" has valid characters, even though it's a keyword
84
+ expect(isValidCSharpIdentifier("class")).toBe(true);
85
+ });
86
+ });
87
+
88
+ describe("sanitizeCSharpIdentifier", () => {
89
+ it("passes through valid identifiers unchanged", () => {
90
+ expect(sanitizeCSharpIdentifier("ValidName")).toBe("ValidName");
91
+ expect(sanitizeCSharpIdentifier("_private")).toBe("_private");
92
+ expect(sanitizeCSharpIdentifier("name123")).toBe("name123");
93
+ });
94
+
95
+ it("prefixes underscore when first char is a digit", () => {
96
+ expect(sanitizeCSharpIdentifier("1foo")).toBe("_1foo");
97
+ });
98
+
99
+ it("replaces non-word characters with underscore", () => {
100
+ expect(sanitizeCSharpIdentifier("foo-bar")).toBe("foo_bar");
101
+ expect(sanitizeCSharpIdentifier("has space")).toBe("has_space");
102
+ expect(sanitizeCSharpIdentifier("a.b.c")).toBe("a_b_c");
103
+ });
104
+
105
+ it("handles leading special characters", () => {
106
+ expect(sanitizeCSharpIdentifier("$foo")).toBe("_foo");
107
+ expect(sanitizeCSharpIdentifier("-bar")).toBe("_bar");
108
+ });
109
+
110
+ it("handles empty string", () => {
111
+ expect(sanitizeCSharpIdentifier("")).toBe("_");
112
+ });
113
+ });
114
+
115
+ describe("createCSharpNamePolicy keyword escaping", () => {
116
+ const policy = createCSharpNamePolicy();
117
+
118
+ describe("PascalCase elements avoid most keyword conflicts naturally", () => {
119
+ it("class element: 'string' becomes 'String' (not a keyword, no escape)", () => {
120
+ expect(policy.getName("string", "class")).toBe("String");
121
+ });
122
+
123
+ it("class element: 'class' becomes 'Class' (not a keyword, no escape)", () => {
124
+ expect(policy.getName("class", "class")).toBe("Class");
125
+ });
126
+
127
+ it("class-property element: 'value' becomes 'Value' (not a keyword, no escape)", () => {
128
+ expect(policy.getName("value", "class-property")).toBe("Value");
129
+ });
130
+ });
131
+
132
+ describe("camelCase elements may hit keywords and get @-escaped", () => {
133
+ it("parameter: 'string' stays 'string' → gets @-escaped", () => {
134
+ expect(policy.getName("string", "parameter")).toBe("@string");
135
+ });
136
+
137
+ it("variable: 'int' stays 'int' → gets @-escaped", () => {
138
+ expect(policy.getName("int", "variable")).toBe("@int");
139
+ });
140
+
141
+ it("parameter: 'return' stays 'return' → gets @-escaped", () => {
142
+ expect(policy.getName("return", "parameter")).toBe("@return");
143
+ });
144
+
145
+ it("variable: 'value' stays 'value' — contextual keyword, not escaped", () => {
146
+ expect(policy.getName("value", "variable")).toBe("value");
147
+ });
148
+
149
+ it("parameter: 'async' stays 'async' — contextual keyword, not escaped", () => {
150
+ expect(policy.getName("async", "parameter")).toBe("async");
151
+ });
152
+ });
153
+
154
+ describe("non-keyword names pass through normally", () => {
155
+ it("parameter: 'myParam' stays 'myParam'", () => {
156
+ expect(policy.getName("myParam", "parameter")).toBe("myParam");
157
+ });
158
+
159
+ it("class: 'my-model' becomes 'MyModel'", () => {
160
+ expect(policy.getName("my-model", "class")).toBe("MyModel");
161
+ });
162
+
163
+ it("parameter: 'some-param' becomes 'someParam'", () => {
164
+ expect(policy.getName("some-param", "parameter")).toBe("someParam");
165
+ });
166
+ });
167
+
168
+ describe("namespace handling", () => {
169
+ it("applies PascalCase", () => {
170
+ expect(policy.getName("my-service", "namespace")).toBe("MyService");
171
+ });
172
+
173
+ it("single segment works", () => {
174
+ expect(policy.getName("system", "namespace")).toBe("System");
175
+ });
176
+
177
+ it("escapes keyword segments", () => {
178
+ // "namespace" as a namespace segment → PascalCase → "Namespace" → not a keyword
179
+ expect(policy.getName("namespace", "namespace")).toBe("Namespace");
180
+ });
181
+ });
182
+
183
+ describe("constant and private member escaping", () => {
184
+ it("constant: 'class' becomes 'CLASS' (not a keyword)", () => {
185
+ expect(policy.getName("class", "constant")).toBe("CLASS");
186
+ });
187
+
188
+ it("class-member-private: 'value' becomes '_value' (not a keyword)", () => {
189
+ expect(policy.getName("value", "class-member-private")).toBe("_value");
190
+ });
191
+ });
192
+
193
+ describe("identifier sanitization", () => {
194
+ it("sanitizes names starting with digits", () => {
195
+ expect(policy.getName("123foo", "class")).toBe("_123foo");
196
+ });
197
+
198
+ it("sanitizes names starting with digits for camelCase elements", () => {
199
+ expect(policy.getName("123param", "parameter")).toBe("_123param");
200
+ });
201
+
202
+ it("sanitizes empty string", () => {
203
+ expect(policy.getName("", "class")).toBe("_");
204
+ });
205
+
206
+ it("sanitizes namespace starting with digits", () => {
207
+ expect(policy.getName("123service", "namespace")).toBe("_123service");
208
+ });
209
+ });
210
+ });
@@ -1,5 +1,7 @@
1
1
  import * as core from "@alloy-js/core";
2
2
  import * as changecase from "change-case";
3
+ import { sanitizeCSharpIdentifier } from "./identifier-utils.js";
4
+ import { isCSharpKeyword } from "./keywords.js";
3
5
 
4
6
  // the context in which the name policy should be applied
5
7
  export type CSharpElements =
@@ -20,10 +22,27 @@ export type CSharpElements =
20
22
  | "type-parameter"
21
23
  | "namespace";
22
24
 
23
- // creates the C# naming policy
25
+ /**
26
+ * Prefixes the name with `@` if it is a C# keyword.
27
+ * This is the idiomatic C# way to use reserved words as identifiers.
28
+ * The check is case-sensitive, matching C# language semantics.
29
+ */
30
+ function escapeIfKeyword(name: string): string {
31
+ return isCSharpKeyword(name) ? `@${name}` : name;
32
+ }
33
+
34
+ /**
35
+ * Creates the C# naming policy with case conversion and keyword escaping.
36
+ *
37
+ * After applying the appropriate case conversion for each element kind,
38
+ * the resulting name is checked against C# reserved and contextual keywords.
39
+ * If it matches (case-sensitively), the name is prefixed with `@`.
40
+ */
24
41
  export function createCSharpNamePolicy(): core.NamePolicy<CSharpElements> {
25
42
  return core.createNamePolicy((name, element) => {
43
+ let result: string;
26
44
  switch (element) {
45
+ case "namespace":
27
46
  case "class":
28
47
  case "struct":
29
48
  case "enum":
@@ -34,15 +53,20 @@ export function createCSharpNamePolicy(): core.NamePolicy<CSharpElements> {
34
53
  case "class-method":
35
54
  case "type-parameter":
36
55
  case "class-property":
37
- case "namespace":
38
- return changecase.pascalCase(name);
56
+ result = changecase.pascalCase(name);
57
+ break;
39
58
  case "constant":
40
- return changecase.constantCase(name);
59
+ result = changecase.constantCase(name);
60
+ break;
41
61
  case "class-member-private":
42
- return `_${changecase.camelCase(name)}`;
62
+ result = `_${changecase.camelCase(name)}`;
63
+ break;
43
64
  default:
44
- return changecase.camelCase(name);
65
+ result = changecase.camelCase(name);
66
+ break;
45
67
  }
68
+
69
+ return escapeIfKeyword(sanitizeCSharpIdentifier(result));
46
70
  });
47
71
  }
48
72