@alloy-js/csharp 0.23.0-dev.7 → 0.23.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 (102) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/dev/src/components/constructor/constructor.js +69 -9
  3. package/dist/dev/src/components/constructor/constructor.js.map +1 -1
  4. package/dist/dev/src/components/constructor/constructor.test.js +117 -0
  5. package/dist/dev/src/components/constructor/constructor.test.js.map +1 -1
  6. package/dist/dev/src/components/method/method.test.js +64 -0
  7. package/dist/dev/src/components/method/method.test.js.map +1 -1
  8. package/dist/dev/src/components/namespace/namespace.js +2 -1
  9. package/dist/dev/src/components/namespace/namespace.js.map +1 -1
  10. package/dist/dev/src/components/namespace/namespace.test.js +13 -10
  11. package/dist/dev/src/components/namespace/namespace.test.js.map +1 -1
  12. package/dist/dev/src/components/namespace.ref.test.js +54 -42
  13. package/dist/dev/src/components/namespace.ref.test.js.map +1 -1
  14. package/dist/dev/src/components/property/property.js +101 -24
  15. package/dist/dev/src/components/property/property.js.map +1 -1
  16. package/dist/dev/src/components/property/property.test.js +140 -0
  17. package/dist/dev/src/components/property/property.test.js.map +1 -1
  18. package/dist/dev/src/components/source-file/source-file.js +12 -11
  19. package/dist/dev/src/components/source-file/source-file.js.map +1 -1
  20. package/dist/dev/src/identifier-utils.js +45 -0
  21. package/dist/dev/src/identifier-utils.js.map +1 -0
  22. package/dist/dev/src/index.js +2 -0
  23. package/dist/dev/src/index.js.map +1 -1
  24. package/dist/dev/src/keywords.js +39 -0
  25. package/dist/dev/src/keywords.js.map +1 -0
  26. package/dist/dev/src/name-policy.js +29 -6
  27. package/dist/dev/src/name-policy.js.map +1 -1
  28. package/dist/dev/src/name-policy.test.js +167 -0
  29. package/dist/dev/src/name-policy.test.js.map +1 -0
  30. package/dist/src/components/constructor/constructor.d.ts +32 -0
  31. package/dist/src/components/constructor/constructor.d.ts.map +1 -1
  32. package/dist/src/components/constructor/constructor.js +35 -3
  33. package/dist/src/components/constructor/constructor.js.map +1 -1
  34. package/dist/src/components/constructor/constructor.test.js +81 -0
  35. package/dist/src/components/constructor/constructor.test.js.map +1 -1
  36. package/dist/src/components/method/method.test.js +48 -0
  37. package/dist/src/components/method/method.test.js.map +1 -1
  38. package/dist/src/components/namespace/namespace.js +2 -1
  39. package/dist/src/components/namespace/namespace.js.map +1 -1
  40. package/dist/src/components/namespace/namespace.test.js +6 -3
  41. package/dist/src/components/namespace/namespace.test.js.map +1 -1
  42. package/dist/src/components/namespace.ref.test.js +24 -12
  43. package/dist/src/components/namespace.ref.test.js.map +1 -1
  44. package/dist/src/components/property/property.d.ts +42 -4
  45. package/dist/src/components/property/property.d.ts.map +1 -1
  46. package/dist/src/components/property/property.js +64 -11
  47. package/dist/src/components/property/property.js.map +1 -1
  48. package/dist/src/components/property/property.test.js +104 -0
  49. package/dist/src/components/property/property.test.js.map +1 -1
  50. package/dist/src/components/source-file/source-file.d.ts.map +1 -1
  51. package/dist/src/components/source-file/source-file.js +3 -2
  52. package/dist/src/components/source-file/source-file.js.map +1 -1
  53. package/dist/src/identifier-utils.d.ts +22 -0
  54. package/dist/src/identifier-utils.d.ts.map +1 -0
  55. package/dist/src/identifier-utils.js +45 -0
  56. package/dist/src/identifier-utils.js.map +1 -0
  57. package/dist/src/index.d.ts +2 -0
  58. package/dist/src/index.d.ts.map +1 -1
  59. package/dist/src/index.js +2 -0
  60. package/dist/src/index.js.map +1 -1
  61. package/dist/src/keywords.d.ts +32 -0
  62. package/dist/src/keywords.d.ts.map +1 -0
  63. package/dist/src/keywords.js +39 -0
  64. package/dist/src/keywords.js.map +1 -0
  65. package/dist/src/name-policy.d.ts +7 -0
  66. package/dist/src/name-policy.d.ts.map +1 -1
  67. package/dist/src/name-policy.js +29 -6
  68. package/dist/src/name-policy.js.map +1 -1
  69. package/dist/src/name-policy.test.d.ts +2 -0
  70. package/dist/src/name-policy.test.d.ts.map +1 -0
  71. package/dist/src/name-policy.test.js +167 -0
  72. package/dist/src/name-policy.test.js.map +1 -0
  73. package/dist/tsconfig.tsbuildinfo +1 -1
  74. package/docs/api/components/Constructor.md +17 -11
  75. package/docs/api/components/Property.md +30 -30
  76. package/docs/api/contexts/csharp-context.md +21 -0
  77. package/docs/api/contexts/index.md +3 -0
  78. package/docs/api/functions/createCSharpNamePolicy.md +4 -0
  79. package/docs/api/functions/index.md +5 -1
  80. package/docs/api/functions/isCSharpContextualKeyword.md +20 -0
  81. package/docs/api/functions/isCSharpKeyword.md +22 -0
  82. package/docs/api/functions/isValidCSharpIdentifier.md +20 -0
  83. package/docs/api/functions/sanitizeCSharpIdentifier.md +24 -0
  84. package/docs/api/index.md +3 -2
  85. package/docs/api/variables/csharpKeywords.md +11 -0
  86. package/docs/api/variables/index.md +1 -0
  87. package/package.json +8 -8
  88. package/src/components/constructor/constructor.test.tsx +64 -0
  89. package/src/components/constructor/constructor.tsx +81 -1
  90. package/src/components/method/method.test.tsx +36 -0
  91. package/src/components/namespace/namespace.test.tsx +6 -3
  92. package/src/components/namespace/namespace.tsx +2 -2
  93. package/src/components/namespace.ref.test.tsx +24 -12
  94. package/src/components/property/property.test.tsx +89 -0
  95. package/src/components/property/property.tsx +111 -16
  96. package/src/components/source-file/source-file.tsx +1 -4
  97. package/src/identifier-utils.ts +45 -0
  98. package/src/index.ts +2 -0
  99. package/src/keywords.ts +162 -0
  100. package/src/name-policy.test.ts +210 -0
  101. package/src/name-policy.ts +30 -6
  102. package/temp/api.json +317 -7
@@ -43,9 +43,11 @@ it("references types in a parent namespace", () => {
43
43
  );
44
44
 
45
45
  expect(tree).toRenderTo(`
46
- namespace Test {
46
+ namespace Test
47
+ {
47
48
  class TestClass;
48
- namespace Nested {
49
+ namespace Nested
50
+ {
49
51
  TestClass;
50
52
  }
51
53
  }
@@ -69,9 +71,11 @@ it("references types in a child namespace", () => {
69
71
  );
70
72
 
71
73
  expect(tree).toRenderTo(`
72
- namespace Test {
74
+ namespace Test
75
+ {
73
76
  Nested.TestClass;
74
- namespace Nested {
77
+ namespace Nested
78
+ {
75
79
  class TestClass;
76
80
  }
77
81
  }
@@ -96,10 +100,12 @@ it("references types in a different top-level namespace declared in the same fil
96
100
  expect(tree).toRenderTo(`
97
101
  using TestCode2;
98
102
 
99
- namespace TestCode1 {
103
+ namespace TestCode1
104
+ {
100
105
  TestClass
101
106
  }
102
- namespace TestCode2 {
107
+ namespace TestCode2
108
+ {
103
109
  class TestClass;
104
110
  }
105
111
  `);
@@ -125,12 +131,14 @@ it("references types in a different top-level namespace declared in a different
125
131
  "test.cs": `
126
132
  using TestCode2;
127
133
 
128
- namespace TestCode1 {
134
+ namespace TestCode1
135
+ {
129
136
  TestClass;
130
137
  }
131
138
  `,
132
139
  "other.cs": `
133
- namespace TestCode2 {
140
+ namespace TestCode2
141
+ {
134
142
  class TestClass;
135
143
  }
136
144
  `,
@@ -156,7 +164,8 @@ it("can be referenced by refkey", () => {
156
164
  expect(tree).toRenderTo(`
157
165
  using TestCode2;
158
166
 
159
- namespace TestCode2 {
167
+ namespace TestCode2
168
+ {
160
169
  class TestClass;
161
170
  }
162
171
  TestClass;
@@ -181,11 +190,14 @@ it("references types across sibling namespaces under the same parent", () => {
181
190
  );
182
191
 
183
192
  expect(tree).toRenderTo(`
184
- namespace Parent {
185
- namespace Models {
193
+ namespace Parent
194
+ {
195
+ namespace Models
196
+ {
186
197
  class User;
187
198
  }
188
- namespace Services {
199
+ namespace Services
200
+ {
189
201
  Models.User;
190
202
  }
191
203
  }
@@ -232,3 +232,92 @@ describe("format", () => {
232
232
  `);
233
233
  });
234
234
  });
235
+
236
+ describe("accessor bodies", () => {
237
+ it("renders get with body", () => {
238
+ expect(
239
+ <Wrapper>
240
+ <Property public name="Name" type="string" get={<>return _name;</>} />
241
+ </Wrapper>,
242
+ ).toRenderTo(`
243
+ public class TestClass
244
+ {
245
+ public string Name
246
+ {
247
+ get { return _name; }
248
+ }
249
+ }
250
+ `);
251
+ });
252
+
253
+ it("renders set with body", () => {
254
+ expect(
255
+ <Wrapper>
256
+ <Property
257
+ public
258
+ name="Value"
259
+ type="int"
260
+ get
261
+ set={<>_value = value;</>}
262
+ />
263
+ </Wrapper>,
264
+ ).toRenderTo(`
265
+ public class TestClass
266
+ {
267
+ public int Value
268
+ {
269
+ get;
270
+ set { _value = value; }
271
+ }
272
+ }
273
+ `);
274
+ });
275
+
276
+ it("renders both get and set with bodies", () => {
277
+ expect(
278
+ <Wrapper>
279
+ <Property
280
+ public
281
+ name="MinValue"
282
+ type="T"
283
+ get={<>return _minValue.HasValue ? _minValue.Value : default(T);</>}
284
+ set={<>_minValue = value;</>}
285
+ />
286
+ </Wrapper>,
287
+ ).toRenderTo(`
288
+ public class TestClass
289
+ {
290
+ public T MinValue
291
+ {
292
+ get { return _minValue.HasValue ? _minValue.Value : default(T); }
293
+ set { _minValue = value; }
294
+ }
295
+ }
296
+ `);
297
+ });
298
+
299
+ it("breaks long accessor body across lines", () => {
300
+ expect(
301
+ <TestNamespace printWidth={40}>
302
+ <ClassDeclaration name="Test">
303
+ <Property
304
+ public
305
+ name="Value"
306
+ type="int"
307
+ get={<>return _someVeryLongFieldName;</>}
308
+ />
309
+ </ClassDeclaration>
310
+ </TestNamespace>,
311
+ ).toRenderTo(`
312
+ class Test
313
+ {
314
+ public int Value
315
+ {
316
+ get {
317
+ return _someVeryLongFieldName;
318
+ }
319
+ }
320
+ }
321
+ `);
322
+ });
323
+ });
@@ -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
+ }