@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.
- package/CHANGELOG.md +19 -0
- package/dist/dev/src/components/method/method.test.js +64 -0
- package/dist/dev/src/components/method/method.test.js.map +1 -1
- package/dist/dev/src/components/namespace/namespace.js +2 -1
- package/dist/dev/src/components/namespace/namespace.js.map +1 -1
- package/dist/dev/src/components/namespace/namespace.test.js +13 -10
- package/dist/dev/src/components/namespace/namespace.test.js.map +1 -1
- package/dist/dev/src/components/namespace.ref.test.js +54 -42
- package/dist/dev/src/components/namespace.ref.test.js.map +1 -1
- package/dist/dev/src/components/property/property.js +101 -24
- package/dist/dev/src/components/property/property.js.map +1 -1
- package/dist/dev/src/components/property/property.test.js +140 -0
- package/dist/dev/src/components/property/property.test.js.map +1 -1
- package/dist/dev/src/components/source-file/source-file.js +12 -11
- package/dist/dev/src/components/source-file/source-file.js.map +1 -1
- package/dist/dev/src/identifier-utils.js +45 -0
- package/dist/dev/src/identifier-utils.js.map +1 -0
- package/dist/dev/src/index.js +2 -0
- package/dist/dev/src/index.js.map +1 -1
- package/dist/dev/src/keywords.js +39 -0
- package/dist/dev/src/keywords.js.map +1 -0
- package/dist/dev/src/name-policy.js +29 -6
- package/dist/dev/src/name-policy.js.map +1 -1
- package/dist/dev/src/name-policy.test.js +167 -0
- package/dist/dev/src/name-policy.test.js.map +1 -0
- package/dist/src/components/method/method.test.js +48 -0
- package/dist/src/components/method/method.test.js.map +1 -1
- package/dist/src/components/namespace/namespace.js +2 -1
- package/dist/src/components/namespace/namespace.js.map +1 -1
- package/dist/src/components/namespace/namespace.test.js +6 -3
- package/dist/src/components/namespace/namespace.test.js.map +1 -1
- package/dist/src/components/namespace.ref.test.js +24 -12
- package/dist/src/components/namespace.ref.test.js.map +1 -1
- package/dist/src/components/property/property.d.ts +42 -4
- package/dist/src/components/property/property.d.ts.map +1 -1
- package/dist/src/components/property/property.js +64 -11
- package/dist/src/components/property/property.js.map +1 -1
- package/dist/src/components/property/property.test.js +104 -0
- package/dist/src/components/property/property.test.js.map +1 -1
- package/dist/src/components/source-file/source-file.d.ts.map +1 -1
- package/dist/src/components/source-file/source-file.js +3 -2
- package/dist/src/components/source-file/source-file.js.map +1 -1
- package/dist/src/identifier-utils.d.ts +22 -0
- package/dist/src/identifier-utils.d.ts.map +1 -0
- package/dist/src/identifier-utils.js +45 -0
- package/dist/src/identifier-utils.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/keywords.d.ts +32 -0
- package/dist/src/keywords.d.ts.map +1 -0
- package/dist/src/keywords.js +39 -0
- package/dist/src/keywords.js.map +1 -0
- package/dist/src/name-policy.d.ts +7 -0
- package/dist/src/name-policy.d.ts.map +1 -1
- package/dist/src/name-policy.js +29 -6
- package/dist/src/name-policy.js.map +1 -1
- package/dist/src/name-policy.test.d.ts +2 -0
- package/dist/src/name-policy.test.d.ts.map +1 -0
- package/dist/src/name-policy.test.js +167 -0
- package/dist/src/name-policy.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/api/components/Property.md +30 -30
- package/docs/api/contexts/csharp-context.md +21 -0
- package/docs/api/contexts/index.md +3 -0
- package/docs/api/functions/createCSharpNamePolicy.md +4 -0
- package/docs/api/functions/index.md +5 -1
- package/docs/api/functions/isCSharpContextualKeyword.md +20 -0
- package/docs/api/functions/isCSharpKeyword.md +22 -0
- package/docs/api/functions/isValidCSharpIdentifier.md +20 -0
- package/docs/api/functions/sanitizeCSharpIdentifier.md +24 -0
- package/docs/api/index.md +3 -2
- package/docs/api/variables/csharpKeywords.md +11 -0
- package/docs/api/variables/index.md +1 -0
- package/package.json +6 -6
- package/src/components/method/method.test.tsx +36 -0
- package/src/components/namespace/namespace.test.tsx +6 -3
- package/src/components/namespace/namespace.tsx +2 -2
- package/src/components/namespace.ref.test.tsx +24 -12
- package/src/components/property/property.test.tsx +89 -0
- package/src/components/property/property.tsx +111 -16
- package/src/components/source-file/source-file.tsx +1 -4
- package/src/identifier-utils.ts +45 -0
- package/src/index.ts +2 -0
- package/src/keywords.ts +162 -0
- package/src/name-policy.test.ts +210 -0
- package/src/name-policy.ts +30 -6
- 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
|
-
/**
|
|
58
|
-
|
|
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
|
-
/**
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
+
<>
|
|
219
|
+
{" ="}
|
|
220
|
+
<indent>
|
|
221
|
+
<line />
|
|
222
|
+
{props.initializer};
|
|
223
|
+
</indent>
|
|
224
|
+
</>
|
|
152
225
|
)}
|
|
153
|
-
</
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
{
|
|
161
|
-
|
|
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";
|
package/src/keywords.ts
ADDED
|
@@ -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
|
+
});
|
package/src/name-policy.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
56
|
+
result = changecase.pascalCase(name);
|
|
57
|
+
break;
|
|
39
58
|
case "constant":
|
|
40
|
-
|
|
59
|
+
result = changecase.constantCase(name);
|
|
60
|
+
break;
|
|
41
61
|
case "class-member-private":
|
|
42
|
-
|
|
62
|
+
result = `_${changecase.camelCase(name)}`;
|
|
63
|
+
break;
|
|
43
64
|
default:
|
|
44
|
-
|
|
65
|
+
result = changecase.camelCase(name);
|
|
66
|
+
break;
|
|
45
67
|
}
|
|
68
|
+
|
|
69
|
+
return escapeIfKeyword(sanitizeCSharpIdentifier(result));
|
|
46
70
|
});
|
|
47
71
|
}
|
|
48
72
|
|