-
Notifications
You must be signed in to change notification settings - Fork 308
Expand file tree
/
Copy pathname-registry.ts
More file actions
1394 lines (1255 loc) · 54 KB
/
name-registry.ts
File metadata and controls
1394 lines (1255 loc) · 54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* @file Type canonicalization utilities for C# code generation.
*
* This module provides functionality to manage and canonicalize C# type names and namespaces
* to avoid naming conflicts during code generation. It maintains registries of known types,
* namespaces, and identifiers to ensure generated code follows C# naming conventions and
* avoids conflicts with built-in types and namespaces.
*
* Key features:
* - Type name conflict detection and resolution
* - Namespace canonicalization and remapping
* - Built-in type awareness and conflict avoidance
* - Type registry persistence and restoration
*
*/
import { type Field as AstField, ClassReference } from "../ast/index.js";
import { Generation } from "../context/generation-info.js";
import { Origin } from "../context/model-navigator.js";
import { fail } from "../utils/fail.js";
import { is } from "../utils/type-guards.js";
import { builtIns } from "./knownTypes.js";
// C# Keywords and Reserved Names (from Registry)
const base_keywords = new Set([
"abstract",
"as",
"base",
"bool",
"break",
"byte",
"case",
"catch",
"char",
"checked",
"class",
"const",
"continue",
"decimal",
"default",
"delegate",
"do",
"double",
"else",
"enum",
"event",
"explicit",
"extern",
"false",
"finally",
"fixed",
"float",
"for",
"foreach",
"goto",
"if",
"implicit",
"in",
"int",
"interface",
"internal",
"is",
"lock",
"long",
"namespace",
"new",
"null",
"object",
"operator",
"out",
"override",
"params",
"private",
"protected",
"public",
"readonly",
"ref",
"return",
"sbyte",
"sealed",
"short",
"sizeof",
"stackalloc",
"static",
"string",
"struct",
"switch",
"this",
"throw",
"true",
"try",
"typeof",
"uint",
"ulong",
"unchecked",
"unsafe",
"ushort",
"using",
"virtual",
"void",
"volatile",
"while"
]);
const accessor_keywords = new Set(["get", "set", "init", "value", "add", "remove"]);
const generic_keywords = new Set(["where"]);
const linq_keywords = new Set(["from", "where", "select", "orderby", "groupby", "into", "let", "join", "on", "equals"]);
const async_keywords = new Set(["async", "await"]);
const iterator_keywords = new Set(["yield"]);
const declarator_keywords = new Set(["var", "dynamic"]);
const using_keywords = new Set(["using", "alias"]);
const namespace_keywords = new Set(["nameof"]);
const modifier_keywords = new Set(["required", "scoped", "unmanaged", "managed"]);
const pattern_keywords = new Set(["when", "and", "or", "not"]);
const member_names = new Set(["Equals", "GetHashCode", "ToString", "GetType", "MemberwiseClone", "Finalize"]);
// Generally, these are the keywords that we'd like to always avoid using for identifiers
const keywords = new Set([
...base_keywords,
...linq_keywords,
...async_keywords,
...iterator_keywords,
...declarator_keywords,
...using_keywords,
...modifier_keywords,
...pattern_keywords
]);
type JsonPath = string;
type FullyQualifiedName = string;
/**
* Base class for identifiers in the name registry system.
* Represents a named entity that is tracked by the NameRegistry.
*/
class Identifier {
constructor(
public readonly registry: NameRegistry,
public readonly name: string,
public readonly jsonPath?: JsonPath
) {}
}
/**
* An identifier that creates a scope where other identifiers can be nested.
* Manages forbidden names and ensures unique names within the scope.
*/
class Scope extends Identifier {
private readonly forbidden = new Set<string>();
/**
* Makes a name off-limits within this scope.
* @param name - The name to forbid
*/
forbid(name: string) {
this.forbidden.add(name);
}
/**
* Creates an identifier in this scope with the specified name.
* Two things cannot exist with the same name in the same scope.
* @param name - The name to create
*/
create(name: string) {
//
}
/**
* Gets a name, appending an underscore if it's forbidden.
* @param name - The requested name
* @returns The name with an underscore suffix if forbidden, otherwise the original name
*/
getName(name: string) {
if (this.forbidden.has(name)) {
return `${name}_`;
}
return name;
}
}
/**
* Collection of members (fields or methods) within a TypeScope.
* Maintains lookups by name and JSON path, and handles name redirections for conflict resolution.
*/
class Members<T extends Member> implements Iterable<T> {
constructor(private readonly scope: TypeScope) {}
private readonly byName = new Map<string, T>();
private readonly byPath = new Map<JsonPath, T>();
private readonly redirections = new Map<string, string>();
[Symbol.iterator](): Iterator<T> {
return this.byName.values();
}
/**
* Checks if a member with the given name exists in this collection.
* @param name - The member name to check
* @returns `true` if a member with this name exists, `false` otherwise
*/
has(name: string): boolean {
return this.byName.has(name);
}
/**
* Retrieves a member by its JSON path from the IR.
* @param jsonPath - The JSON path identifying the member in the IR
* @returns The member if found, `undefined` otherwise
*/
getByJsonPath(jsonPath: JsonPath): T | undefined {
return this.byPath.get(jsonPath);
}
/**
* Retrieves a member by its name.
* @param name - The member name to look up
* @returns The member if found, `undefined` otherwise
*/
getByName(name: string): T | undefined {
return this.byName.get(name);
}
/**
* Gets the redirected name for a member if one exists.
* Redirections occur when a member's intended name conflicts with other identifiers.
* @param name - The original member name
* @returns The redirected name if one exists, `undefined` otherwise
*/
getRedirectedName(name: string): string | undefined {
return this.redirections.get(name);
}
/**
* Adds a member to this collection.
* Fails if a member with the same name or JSON path already exists.
* @param member - The member to add
* @throws Error if the member already exists by name or JSON path
*/
set(member: T) {
if (this.byName.has(member.name)) {
fail(`set: ${member.name} in ${this.scope.fullyQualifiedName} already exists`);
}
if (member.jsonPath && this.byPath.has(member.jsonPath)) {
fail(`set: ${member.name} in ${this.scope.fullyQualifiedName} already exists by jsonPath`);
}
this.byName.set(member.name, member);
if (member.jsonPath) {
this.byPath.set(member.jsonPath, member);
}
}
/**
* Registers a name redirection.
* This is used to track when a member's intended name was changed to avoid conflicts.
* @param name - The original name
* @param newName - The redirected name
* @throws Error if a redirection for this name already exists
*/
redirect(name: string, newName: string) {
if (this.redirections.has(name)) {
fail(`redirect: ${name} in ${this.scope.fullyQualifiedName} already has a redirect`);
}
this.redirections.set(name, newName);
}
}
/**
* Represents the scope of a type (class, struct, etc.) and manages its members.
* Handles field and method name registration, conflict detection, and name resolution.
*/
export class TypeScope extends Identifier {
readonly fields: Members<Field>;
readonly methods: Members<Method>;
constructor(
registry: NameRegistry,
name: string,
readonly namespace: string,
readonly fullyQualifiedName: FullyQualifiedName
) {
super(registry, name);
this.fields = new Members<Field>(this);
this.methods = new Members<Method>(this);
}
/**
* Checks if a name is a C# keyword.
* @param name - The name to check
* @returns `true` if the name is a C# keyword, `false` otherwise
*/
isKeyword(name: string) {
return keywords.has(name);
}
/**
* Checks if a name is a built-in .NET member name (e.g., Equals, GetHashCode, ToString).
* @param name - The name to check
* @returns `true` if the name is a built-in member name, `false` otherwise
*/
isBuiltinMemberName(name: string) {
return member_names.has(name);
}
/**
* Checks if a name matches this type's name.
* @param name - The name to check
* @returns `true` if the name matches this type's name, `false` otherwise
*/
isTypeName(name: string) {
return this.name === name;
}
/**
* Checks if a name is already registered as a field in this type.
* @param name - The name to check
* @returns `true` if the name is a registered field, `false` otherwise
*/
isField(name: string) {
return this.fields.has(name);
}
/**
* Checks if a name is already registered as a method in this type.
* @param name - The name to check
* @returns `true` if the name is a registered method, `false` otherwise
*/
isMethod(name: string) {
return this.methods.has(name);
}
/**
* Checks if a name is registered as either a field or method in this type.
* @param name - The name to check
* @returns `true` if the name is a registered member, `false` otherwise
*/
isMember(name: string) {
return this.isField(name) || this.isMethod(name);
}
/**
* Determines why a name is blocked from use, if at all.
* @param name - The name to check
* @returns The reason the name is blocked, or `undefined` if available
*/
nameBlocked(name: string): "keyword" | "builtin" | "typeName" | "field" | "method" | undefined {
if (this.isKeyword(name)) {
return "keyword";
}
if (this.isBuiltinMemberName(name)) {
return "builtin";
}
if (this.isTypeName(name)) {
return "typeName";
}
if (this.isField(name)) {
return "field";
}
if (this.isMethod(name)) {
return "method";
}
return undefined;
}
/**
* Generates an alternative name when the requested name is blocked.
* Appends underscores and numbers until an available name is found.
* @param name - The blocked name
* @returns An available alternative name (e.g., "name_", "name_2", "name_3", etc.)
*/
getAlternativeName(name: string): string {
// first append an underscore.
let newName = `${name}_`;
let i = 2;
while (this.nameBlocked(newName)) {
newName = `${name}_${i}`;
i++;
}
return newName;
}
/**
* Retrieves a field by its JSON path from the IR.
* @param jsonPath - The JSON path identifying the field
* @returns The field if found, `undefined` otherwise
*/
getFieldByJsonPath(jsonPath?: string): Member | undefined {
return jsonPath !== undefined ? this.fields.getByJsonPath(jsonPath) : undefined;
}
/**
* Retrieves a field by its name.
* @param name - The field name to look up
* @returns The field if found, `undefined` otherwise
*/
getFieldByName(name: string): Member | undefined {
return this.fields.getByName(name);
}
/**
* Gets the redirected name for a field if one exists.
* @param name - The original field name
* @returns The redirected name if one exists, `undefined` otherwise
*/
getRedirectedFieldName(name: string): string | undefined {
return this.fields.getRedirectedName(name);
}
/**
* Registers a field in this type scope with conflict resolution.
* If the expected name is unavailable, an alternative name is chosen and a redirection is registered.
* @param expectedName - The desired field name
* @param origin - The IR origin node for this field
* @param field - The AST field object
* @returns The actual field name (may differ from expectedName if conflicts occurred)
*/
registerField(expectedName: string, origin?: Origin, field?: AstField): string {
const jsonPath = this.registry.model.jsonPath(origin);
if (jsonPath) {
// quick lookup by json paths
const member = this.fields.getByJsonPath(jsonPath);
if (member) {
return member.name;
}
}
// lookup
const member = this.fields.getByName(expectedName);
if (member && jsonPath === member.jsonPath) {
// the origin the same, assume it's the same member.
return expectedName;
}
switch (this.nameBlocked(expectedName)) {
case "field":
// if the name we're asking for is already a field
// and they are creating a new field with the same name - this should only happen if the generator is trying to create a field with a specific name like "Value" and
// there is a property that came from the IR called "Value" that is already registered. (so if the origin is an explicitly named member we'll give them a new name)
if (!(origin && is.Provenance(origin) && origin.explicit)) {
fail(
`Field ${expectedName} already exists - attempting to add a duplicate field with the same name that is not an explicitly named property`
);
}
break;
case "keyword":
case "builtin":
case "typeName":
case "method":
// if we're blocked because the name is a keyword/builtin/typename or an existing method, we can just get a new name, and the redirect will work fine.
break;
default:
// name is available, register it and return the name
this.fields.set(new Field(this.registry, expectedName, this, jsonPath, field));
this.registry.setFieldNameShortcut(jsonPath, expectedName);
return expectedName;
}
const newName = this.getAlternativeName(expectedName);
this.fields.set(new Field(this.registry, newName, this, jsonPath, field));
this.fields.redirect(expectedName, newName);
this.registry.setFieldNameShortcut(jsonPath, newName);
return newName;
}
/**
* Retrieves the name of a field given an origin and expected name.
* This method performs intelligent lookup considering IR origins and explicit property names.
*
* IMPORTANT: This method DOES NOT register the field in the type scope.
*
* @param node - The IR origin node for the field
* @param expectedName - The expected field name
* @returns The actual field name if found or inferred, `undefined` otherwise
*/
getFieldName(node: Origin, expectedName: string): string | undefined {
// if we have already identified this node's name then return that quickly.
const result = this.getFieldByJsonPath(this.registry.model.jsonPath(node))?.name;
if (result) {
return result;
}
// we are being asked to find a field that we didn't get a match on the jsonPath.
// given the origin however, we can are going to make a pretty good guess as to what the consumer is actually looking for.
if (is.Provenance(node)) {
// if the origin is an explicitly named property (aka one that the name is hand-coded in the generator)
// then we should check to see if that has been redirected. If it has, then we can return the redirected name.
const redirectedName = this.getRedirectedFieldName(expectedName);
if (redirectedName) {
return redirectedName;
}
// if it hasn't been redirected, and there is a property by that name,
const property = this.getFieldByName(expectedName);
if (property) {
// does the property have an IR origin, if so, then this is not a good match (a hand-coded name shouldn't match a property that has an IR origin)
const provenance = this.registry.model.provenance(property.jsonPath);
if (provenance?.explicit) {
// this is a good match, return the expected name
return expectedName;
}
// this is not a good match (the hand-coded property should have been redirected)
// `BAD: getFieldName: ${this.fullyQualifiedName} for ${expectedName} found a IR-based property, but is being requested for an explicitly named property.`
// We should register a redirect
return `${expectedName}_`;
}
// there is no property by that name, so we're going to return the expected name
// `BAD: getFieldName: ${this.fullyQualifiedName} for ${expectedName} found no property by that name. You should register it before use.`
return expectedName;
}
// the origin is an IR node.
const property = this.getFieldByName(expectedName);
if (property) {
// there is an existing property by that name.
// if the property has an IR origin, then we're going to assume that these two are intended to be the same.
const provenance = this.registry.model.provenance(property.jsonPath);
if (provenance?.explicit) {
// if the origin is an IR node and the property has an explicitly named origin, then it will not be a match,
// this should have been redirected (this should be avoided if possible by making explicitly named properties created after properties that are IR-based)
// `BAD: getFieldName: ${this.fullyQualifiedName} for ${expectedName} found a IR-based property, but is being requested for an explicitly named property.`
// You should redirect the explicitly named property
return expectedName;
}
// this is a good match, (or at least it's not an explicitly named property) we'll return the name
return property.name;
}
// there is no registered property, we can check for a redirect
const actualName = this.getRedirectedFieldName(expectedName);
if (actualName) {
return actualName;
}
// there was no redirect. Is there a method by the expected name?
if (this.isMethod(expectedName)) {
// this means we have a method where we think we wanted a property.
// `BAD: getFieldName: ${this.fullyQualifiedName} for ${expectedName} found a method, but is being requested for a property.`
return undefined;
}
// there is no member by this name registered, I can't tell you that it is OK to use.
return undefined;
}
}
/**
* Base class representing a member (field or method) within a TypeScope.
*/
class Member extends Identifier {
constructor(
registry: NameRegistry,
name: string,
public readonly scope: TypeScope,
jsonPath?: JsonPath
) {
super(registry, name, jsonPath);
}
}
/**
* Represents a field member within a type.
*/
class Field extends Member {
constructor(
registry: NameRegistry,
name: string,
scope: TypeScope,
jsonPath?: JsonPath,
readonly field?: AstField
) {
super(registry, name, scope, jsonPath);
}
}
/**
* Represents a method member within a type.
*/
class Method extends Member {}
/**
* Type/name canonicalization utilities for C# code generation.
*
* This class provides functionality to manage and canonicalize C# type names and namespaces
* to avoid naming conflicts during code generation. It maintains registries of known types,
* namespaces, and identifiers to ensure generated code follows C# naming conventions and
* avoids conflicts with built-in types and namespaces.
*
* Key features:
* - Type name conflict detection and resolution
* - Namespace canonicalization and remapping
* - Built-in type awareness and conflict avoidance
* - Type registry persistence and restoration
*/
export class NameRegistry {
/**
* Registry mapping JSON paths from the IR to their ClassReference objects.
* Provides fast lookup of class references by their IR origin.
*/
private readonly classReferenceByJsonPath = new Map<JsonPath, ClassReference>();
/**
* Registry mapping fully qualified type names to their canonical ClassReference objects.
* This registry tracks all known types and may contain remapped versions to avoid conflicts.
*
* Key format: "Namespace.TypeName" or "Namespace.EnclosingType.TypeName"
* Value: The canonical ClassReference for the type
*/
private readonly typeRegistry = new Map<FullyQualifiedName, ClassReference>();
/**
* Registry mapping original namespace names to their canonical (potentially remapped) versions.
* This is used to track namespace remappings that occur during conflict resolution.
*
* Key: Original namespace name
* Value: Canonical namespace name (may be modified to avoid conflicts)
*/
private readonly namespaceRegistry = new Map<string, string>();
/**
* Registry tracking type names that exist in multiple namespaces, making them ambiguous.
* These types should be explicitly qualified when used to avoid compilation errors.
*
* Key: Type name (e.g., "String", "Object")
* Value: Set of namespaces where this type name exists
*/
private readonly typeNames = new Map<string, Set<string>>();
/**
* Registry tracking namespace names that appear in multiple contexts.
* Used for detecting ambiguous namespace references.
*
* Key: Namespace segment (e.g., "Collections", "Data")
* Value: Set of parent namespace paths where this segment appears
*/
private readonly namespaceNames = new Map<string, Set<string>>();
/**
* Set of namespaces that are implicitly imported/available.
* Types in nested namespaces under these prefixes are tracked for ambiguity detection.
*/
private readonly implicitNamespaces = new Set<string>();
/**
* Shortcut mapping JSON paths to field names for fast lookup.
* Enables quick field name resolution without needing to know the containing type.
*/
private readonly shortcuts = new Map<string, string>();
/**
* Set of well-known C# identifiers that should be avoided or handled carefully during code generation.
* These include common .NET Framework namespaces and types that could cause naming conflicts.
*
* This set is populated with:
* - Common .NET Framework namespace segments
* - Built-in type names from the builtIns registry
* - Namespace segments from all known built-in types
*/
private readonly knownBuiltInIdentifiers = new Set([
"Text",
"Json",
"Xml",
"Security",
"Collections",
"Data",
"Diagnostics",
"Globalization",
"Math",
"Reflection",
"Runtime",
"Security",
"Serialization",
"Threading",
"Xml"
]);
constructor(readonly generation: Generation) {
this.initializeBuiltIns();
}
/**
* Gets the CSharp generation context.
* @returns The CSharp generation context
*/
get csharp() {
return this.generation.csharp;
}
/**
* Gets the model navigator for accessing the IR.
* @returns The model navigator
*/
get model() {
return this.generation.model;
}
/**
* Initializes the known identifiers set with all built-in types and namespace segments.
* This populates the registry with .NET Framework types to enable conflict detection
* and ensures generated code doesn't conflict with framework types.
*/
private initializeBuiltIns(): void {
for (const [namespace, types] of Object.entries(builtIns)) {
// Add each namespace segment to known identifiers
namespace.split(".").forEach((segment) => this.knownBuiltInIdentifiers.add(segment));
// Add each built-in type name to known identifiers
types.forEach((type) => this.knownBuiltInIdentifiers.add(type));
}
// Initialize the typeNames registry with built-in types to enable conflict detection.
// This allows us to identify when user-defined types might conflict with .NET Framework types.
for (const [namespace, names] of Object.entries(builtIns)) {
for (const name of names) {
this.typeNames.set(name, new Set([namespace]));
}
// and the first word of the namespace itself
const firstWord = namespace.split(".")[0];
if (firstWord) {
this.typeNames.set(firstWord, new Set([namespace]));
}
}
this.typeNames.set("System", new Set(["System"]));
this.typeNames.set("NUnit", new Set(["NUnit"]));
this.typeNames.set("OneOf", new Set(["OneOf"]));
// Also track NUnit and OneOf as known built-in identifiers so they
// are excluded from type-namespace conflict detection (just like System)
this.knownBuiltInIdentifiers.add("NUnit");
this.knownBuiltInIdentifiers.add("OneOf");
}
/**
* Registers a shortcut mapping from JSON path to field name.
* This enables fast field name lookup by IR origin without knowing the containing type.
* @param jsonPath - The JSON path from the IR
* @param name - The field name to associate with this JSON path
*/
setFieldNameShortcut(jsonPath: JsonPath | undefined, name: string): void {
if (jsonPath) {
const current = this.shortcuts.get(jsonPath);
if (current && current !== name) {
fail(
`BAD_BAD_BAD setFieldNameShortcut: ${jsonPath} already has a name: ${current} - while setting to ${name} - if this is happening, then you could be getting back the wrong name later!`
);
}
this.shortcuts.set(jsonPath, name);
}
}
/**
* Retrieves the name of a previously registered field using only its IR origin.
*
* This provides a shortcut for field name lookup without needing to know the containing type,
* which is particularly useful when generating examples and snippets.
*
* Note: It is remotely possible that two fields generated from the same IR origin node could
* have different names. This scenario is detected and logged by `setFieldNameShortcut`.
* If this becomes an issue, the type must be resolved before retrieving the field name.
*
* @param origin - The IR origin node for the field
* @returns The field name if found, `undefined` otherwise
*/
getFieldNameByOrigin(origin: Origin | undefined): string | undefined {
return this.shortcuts.get(this.model.jsonPath(origin) ?? ">ignore<");
}
/**
* Checks if the given identifier is a well-known C# identifier that should be handled carefully.
*
* @param identifier - The identifier to check (e.g., "String", "Collections", "System")
* @returns `true` if the identifier is a known C# identifier that could cause conflicts, `false` otherwise
*
* @example
* ```typescript
* nameRegistry.isKnownBuiltInIdentifier("String"); // true - conflicts with System.String
* nameRegistry.isKnownBuiltInIdentifier("MyCustomType"); // false - safe to use
* ```
*/
public isKnownBuiltInIdentifier(identifier: string): boolean {
if (!identifier || typeof identifier !== "string") {
return false;
}
return this.knownBuiltInIdentifiers.has(identifier);
}
/**
* Checks if a namespace has been registered in the namespace registry.
* This includes both original namespaces and their canonical (potentially remapped) versions.
*
* @param namespace - The namespace to check
* @returns `true` if the namespace is known/registered, `false` otherwise
*
* @example
* ```typescript
* nameRegistry.isKnownNamespace("System"); // true - registered during initialization
* nameRegistry.isKnownNamespace("MyNamespace"); // false - not yet registered
* ```
*/
public isKnownNamespace(namespace: string): boolean {
if (!namespace || typeof namespace !== "string") {
return false;
}
return this.namespaceRegistry.has(namespace);
}
/**
* Checks if a fully qualified type name is registered in the type registry.
* This indicates whether the type has been encountered and processed during code generation.
*
* @param typeName - The fully qualified type name to check (e.g., "System.String", "MyNamespace.MyType")
* @returns `true` if the type is registered, `false` otherwise
*
* @example
* ```typescript
* nameRegistry.isRegisteredTypeName("System.String"); // true - built-in type
* nameRegistry.isRegisteredTypeName("MyNamespace.MyType"); // false - not yet registered
* ```
*/
public isRegisteredTypeName(typeName: string): boolean {
return this.typeRegistry.has(typeName);
}
/**
* Determines if a type name is ambiguous (exists in multiple namespaces).
* Ambiguous type names should be explicitly qualified when used to avoid compilation errors.
*
* @param name - The type name to check for ambiguity (optional)
* @returns `true` if the type name exists in multiple namespaces and is ambiguous, `false` otherwise
*
* @example
* ```typescript
* nameRegistry.isAmbiguousTypeName("String"); // true - exists in System and other namespaces
* nameRegistry.isAmbiguousTypeName("MyUniqueType"); // false - only exists in one namespace
* nameRegistry.isAmbiguousTypeName(); // false - no name provided
* ```
*/
public isAmbiguousTypeName(name?: string): boolean {
return name ? (this.typeNames.get(name)?.size ?? 0) > 1 : false;
}
/**
* Determines if a namespace name is ambiguous (appears in multiple contexts).
* @param name - The namespace name to check for ambiguity (optional)
* @returns `true` if the namespace name appears in multiple contexts, `false` otherwise
*/
public isAmbiguousNamespaceName(name?: string): boolean {
return name ? (this.namespaceNames.get(name)?.size ?? 0) > 1 : false;
}
/**
* Checks if a name is both a registered type name and a root-level namespace segment.
* This detects cases where a class name shadows a namespace root, causing CS0426 errors.
*
* For example, if there's a class `Candid` in namespace `Candid.Net`, then any reference
* to `Candid.Net.Something` from within the `Candid.Net` namespace tree will fail because
* the C# compiler resolves `Candid` to the class instead of the namespace.
*
* @param name - The name to check (optional)
* @returns `true` if the name is both a type name and a root namespace segment, `false` otherwise
*/
public hasTypeNamespaceConflict(name?: string): boolean {
if (!name) {
return false;
}
// Exclude known built-in identifiers (System, NUnit, OneOf, etc.) since these
// are framework names that don't create shadowing conflicts in user code.
// The conflict we're detecting is when a USER-DEFINED type name (like a client
// class "Candid") matches a root namespace segment (like "Candid" in "Candid.Net").
if (this.knownBuiltInIdentifiers.has(name)) {
return false;
}
// Check if this name is a tracked type name AND a root-level namespace segment
// (i.e., it appears as the first segment of some namespace, indicated by having
// an empty string "" as a parent in the namespaceNames registry)
return this.typeNames.has(name) && (this.namespaceNames.get(name)?.has("") ?? false);
}
/**
* Generates a fully qualified name string from a class reference identity.
* For nested types, includes the enclosing type in the qualified name.
*
* @param classReference - The class reference identity to convert to a qualified name
* @param classReference.name - The type name
* @param classReference.namespace - The namespace containing the type
* @param classReference.enclosingType - Optional enclosing type for nested types
* @returns A fully qualified name string (e.g., "Namespace.TypeName" or "Namespace.EnclosingType.TypeName")
*/
public static fullyQualifiedNameOf(classReference: ClassReference.Identity): string {
// Create a consistent string representation for registry keys.
// Nested types use '+' separator (matching .NET IL convention) to structurally
// distinguish them from types in sub-namespaces that would otherwise produce
// the same dotted path. For example:
// Nested: namespace=A, enclosingType=B, name=C → "A.B+C"
// Sub-namespace: namespace=A.B, name=C → "A.B.C"
if (classReference.enclosingType) {
const enclosingFqn =
classReference.enclosingType.fullyQualifiedName ??
`${classReference.namespace}.${classReference.enclosingType.name}`;
return `${enclosingFqn}+${classReference.name}`;
}
return `${classReference.namespace}.${classReference.name}`;
}
/**
* Registers a namespace mapping, recording when an original namespace is remapped to a different name.
* Logs an error if attempting to register conflicting mappings for the same namespace.
* @param from - The original namespace name
* @param to - The canonical namespace name (potentially modified to avoid conflicts)
*/
registerNamespace(from: string, to: string): void {
if (this.namespaceRegistry.has(from) && this.namespaceRegistry.get(from) !== to) {
return;
}
this.namespaceRegistry.set(from, to);
}
/**
* Registers a type in the type registry and updates related tracking data structures.
* This method is called when a new type is encountered during code generation.
* It handles JSON path mapping, namespace registration, and ambiguity tracking.
*
* @param classReference - The ClassReference to register
* @param originalFullyQualifiedName - Optional original fully qualified name before any remapping
* @returns The same ClassReference that was passed in (for method chaining)
*
* @example
* ```typescript
* const typeRef = csharp.classReference({ name: "MyType", namespace: "MyNamespace" });
* nameRegistry.trackType(typeRef); // Registers the type
* nameRegistry.trackType(typeRef, "OriginalNamespace.MyType"); // Also registers under original name
* ```
*/
public trackType(classReference: ClassReference, originalFullyQualifiedName?: FullyQualifiedName): ClassReference {
const { name, namespace, enclosingType, fullyQualifiedName, origin } = classReference;
if (origin) {
const jsonPath =
this.model.jsonPath(origin) ??
fail(`JsonPath not found for origin: ${JSON.stringify(origin).substring(0, 100)}`);
this.classReferenceByJsonPath.set(jsonPath, classReference);
}
// if we were given an original name for the class reference, then register the type
if (
originalFullyQualifiedName &&
originalFullyQualifiedName !== fullyQualifiedName &&
!this.typeRegistry.has(originalFullyQualifiedName)
) {
this.typeRegistry.set(originalFullyQualifiedName, classReference);
}
if (!this.typeRegistry.has(fullyQualifiedName)) {
// Register the type in the main registry
this.typeRegistry.set(fullyQualifiedName, classReference);
// Register all parent namespaces
for (const ns of this.allNamespacesOf(namespace)) {
this.registerNamespace(ns, ns);
}
// Track the type name and its namespace for ambiguity detection
if (!enclosingType) {
// Implementation Note:
// if the classReference is actually a nested type, we're going to skip
// tracking it for ambiguity for the moment, as the ambiguity would only be if the type
// was rendered in the enclosing type, and I don't think that happens.
// regardless, if we wanted to make sure that worked, we'd have to know the scope where
// the node was being rendered
// (ie, in the code generator, keep track of where we are, not *just* the current namespace)
this.trackTypeName(name, namespace);
}
for (const each of [this.generation.namespaces.root, ...this.implicitNamespaces]) {
if (namespace.startsWith(each)) {
// get the next word of the namespace
const trimmed = namespace.split(".")[each.split(".").length];
if (trimmed) {
this.trackTypeName(trimmed, namespace);
}
}
}
}
return classReference;
}