Skip to content

Commit e9f6538

Browse files
committed
fix: Replace accessor shadowing with reserved names in collision resolution
Instead of skipping field accessors that collide with slice/extension names, treat field names as reserved during collision resolution so slices/extensions get bumped to qualified names. All field accessors are now always generated.
1 parent 2c11976 commit e9f6538

2 files changed

Lines changed: 14 additions & 29 deletions

File tree

src/api/writer-generator/typescript/profile.ts

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
} from "@root/typeschema/types";
1515
import type { TypeSchemaIndex } from "@root/typeschema/utils";
1616
import {
17-
tsCamelCase,
1817
tsExtensionFlatTypeName,
1918
tsFieldName,
2019
tsModulePath,
@@ -555,11 +554,7 @@ const generateFactoryMethods = (
555554
w.line();
556555
};
557556

558-
const generateFieldAccessors = (
559-
w: TypeScript,
560-
factoryInfo: ProfileFactoryInfo,
561-
extSliceMethodBaseNames: Set<string>,
562-
) => {
557+
const generateFieldAccessors = (w: TypeScript, factoryInfo: ProfileFactoryInfo) => {
563558
w.line("// Field accessors");
564559
for (const p of factoryInfo.params) {
565560
const methodBaseName = uppercaseFirstLetter(p.name);
@@ -574,10 +569,8 @@ const generateFieldAccessors = (
574569
w.line();
575570
}
576571

577-
// Getter and setter methods for choice instance fields (skip if extension/slice has same name)
578572
for (const a of factoryInfo.accessors) {
579-
const methodBaseName = uppercaseFirstLetter(tsCamelCase(a.name));
580-
if (extSliceMethodBaseNames.has(methodBaseName)) continue;
573+
const methodBaseName = uppercaseFirstLetter(a.name);
581574
const fieldAccess = tsFieldName(a.name);
582575
w.curlyBlock([`get${methodBaseName}`, "()", `: ${a.tsType} | undefined`], () => {
583576
w.lineSM(`return ${tsGet("this.resource", fieldAccess)} as ${a.tsType} | undefined`);
@@ -721,18 +714,6 @@ const generateFlatInputType = (w: TypeScript, flatProfile: ProfileTypeSchema) =>
721714
w.line();
722715
};
723716

724-
/** Collect all resolved base names (extensions + slices) for field accessor dedup. */
725-
const collectAllBaseNames = (flatProfile: ProfileTypeSchema, sliceDefs: SliceDef[]): Set<string> => {
726-
const names = new Set<string>();
727-
for (const ext of flatProfile.extensions ?? []) {
728-
if (ext.url) names.add(ext.nameCandidates.recommended);
729-
}
730-
for (const slice of sliceDefs) {
731-
names.add(slice.baseName);
732-
}
733-
return names;
734-
};
735-
736717
export const generateProfileClass = (w: TypeScript, tsIndex: TypeSchemaIndex, flatProfile: ProfileTypeSchema) => {
737718
const tsBaseResourceName = tsTypeFromIdentifier(flatProfile.base);
738719
const profileClassName = tsProfileClassName(flatProfile);
@@ -750,16 +731,14 @@ export const generateProfileClass = (w: TypeScript, tsIndex: TypeSchemaIndex, fl
750731
const canonicalUrl = flatProfile.identifier.url;
751732
w.comment("CanonicalURL:", canonicalUrl, `(pkg: ${packageMetaToFhir(packageMeta(flatProfile))})`);
752733

753-
const allBaseNames = collectAllBaseNames(flatProfile, sliceDefs);
754-
755734
w.curlyBlock(["export", "class", profileClassName], () => {
756735
w.lineSM(`static readonly canonicalUrl = ${JSON.stringify(canonicalUrl)}`);
757736
w.line();
758737
generateStaticSliceFields(w, sliceDefs);
759738
w.lineSM(`private resource: ${tsBaseResourceName}`);
760739
w.line();
761740
generateFactoryMethods(w, tsIndex, flatProfile, factoryInfo);
762-
generateFieldAccessors(w, factoryInfo, allBaseNames);
741+
generateFieldAccessors(w, factoryInfo);
763742

764743
w.line("// Extensions");
765744
generateExtensionMethods(w, tsIndex, flatProfile);

src/typeschema/core/name-candidates.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,26 @@ const sliceCandidates = (fieldName: string, sliceName: string): string[] => {
4242

4343
type NameEntry = { key: string; candidates: string[] };
4444

45-
const countBy = (entries: NameEntry[], level: number): Record<string, number> =>
45+
const countBy = (entries: NameEntry[], level: number, reserved: Set<string>): Record<string, number> =>
4646
entries.reduce(
4747
(counts, e) => {
4848
const name = e.candidates[level] ?? "";
4949
counts[name] = (counts[name] ?? 0) + 1;
50+
if (reserved.has(name)) counts[name] = (counts[name] ?? 0) + 1;
5051
return counts;
5152
},
5253
{} as Record<string, number>,
5354
);
5455

5556
/** Resolve naming collisions across multiple levels of candidates.
56-
* Each entry provides candidate names in priority order (e.g. base → qualified → discriminated). */
57-
const resolveNameCollisions = (entries: NameEntry[]): Record<string, string> => {
57+
* Each entry provides candidate names in priority order (e.g. base → qualified → discriminated).
58+
* Names in `reserved` are treated as taken — entries colliding with them are bumped to the next level. */
59+
const resolveNameCollisions = (entries: NameEntry[], reserved: Set<string>): Record<string, string> => {
5860
const levels = entries[0]?.candidates.length ?? 0;
5961

6062
const resolve = (unresolved: NameEntry[], level: number): Record<string, string> => {
6163
if (unresolved.length === 0 || level >= levels) return {};
62-
const counts = countBy(unresolved, level);
64+
const counts = countBy(unresolved, level, reserved);
6365
const isLastLevel = level >= levels - 1;
6466
const [resolved, colliding] = unresolved.reduce(
6567
([res, col], e) => {
@@ -87,6 +89,7 @@ export const mkSliceNameCandidates = (fieldName: string, sliceName: string): Nam
8789
};
8890

8991
/** Resolve collisions across all extensions and slices within a profile.
92+
* Field accessor names are reserved — slices/extensions are bumped to avoid them.
9093
* Mutates `nameCandidates.recommended` on each extension/slice in place. */
9194
export const assignRecommendedBaseNames = (profile: ProfileTypeSchema): void => {
9295
const extensionEntries: NameEntry[] = (profile.extensions ?? [])
@@ -104,10 +107,13 @@ export const assignRecommendedBaseNames = (profile: ProfileTypeSchema): void =>
104107
}));
105108
});
106109

110+
// Field names are reserved so slices/extensions avoid colliding with field accessors
111+
const reservedNames = new Set(Object.keys(profile.fields ?? {}).map(normalizeCamelName));
112+
107113
const allEntries = [...extensionEntries, ...sliceEntries];
108114
if (allEntries.length === 0) return;
109115

110-
const resolved = resolveNameCollisions(allEntries);
116+
const resolved = resolveNameCollisions(allEntries, reservedNames);
111117

112118
for (const ext of profile.extensions ?? []) {
113119
if (!ext.url) continue;

0 commit comments

Comments
 (0)