Skip to content

Commit 916cb04

Browse files
committed
refactor(types): clear backboneSliceTyping and sliceDelegation
257 -> 237. backboneSliceTyping: the slice-group field name comes from at(-1) with a skip, replacing a non-null assertion; the union collapse destructures its sole member; the root-interface update and the type-alias conversion each hoist the entry once instead of re-indexing it four times; the choice-parent capture and the root segment go through optional access and firstSegment. sliceDelegation: the delegation discriminator returns when handed an empty fixed-value list, the Reference target url is read through optional indexing instead of a length test, and two more split idioms go through firstSegment. These were written while the shell was unavailable, so they were reviewed by reading before they could be compiled — which caught one real break: removing `const segments = relativePath.split('.')` left a later `segments.slice(1).join('.')` referring to nothing. Restored, and the compiler agrees now. us-core HL7 replay 54/54 random and 54/54 empty; typecheck, lint and 769 tests green.
1 parent c285e94 commit 916cb04

2 files changed

Lines changed: 27 additions & 19 deletions

File tree

src/generator/emitters/interface/backboneSliceTyping.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*/
2121

2222
import type { Field } from '../../core/sdTypes.js';
23-
import { capitalize, choicePropertyName, narrowedChoiceType, sanitizeIdentifier } from '../../core/utils.js';
23+
import { capitalize, choicePropertyName, narrowedChoiceType, sanitizeIdentifier, firstSegment } from '../../core/utils.js';
2424
import { getRules } from '../../fhir/versionContext.js';
2525
import type { ImportManager } from './importManager.js';
2626
import { logger } from '../../../logger.js';
@@ -62,7 +62,10 @@ export function generateBackboneSliceTypes(ctx: BackboneSliceTypingContext): voi
6262

6363
for (const group of sliceGroups) {
6464
const { parentField, sliceFields, childFields, slicingRules } = group;
65-
const fieldName = parentField.name.split('.').pop()!;
65+
const fieldName = parentField.name.split('.').at(-1);
66+
// A grouped parent always has a dotted path, so this is unreachable; skipping
67+
// is right if it ever is not, and it replaces a non-null assertion.
68+
if (!fieldName) continue;
6669

6770
// Determine the base backbone type for this field
6871
let backboneType = inferBackboneType(baseResource, fieldName);
@@ -138,8 +141,9 @@ export function generateBackboneSliceTypes(ctx: BackboneSliceTypingContext): voi
138141
if (isOpen) {
139142
unionMembers.push(backboneType);
140143
}
141-
const unionType = unionMembers.length === 1
142-
? `${unionMembers[0]}[]`
144+
const [soleMember] = unionMembers;
145+
const unionType = unionMembers.length === 1 && soleMember
146+
? `${soleMember}[]`
143147
: `(${unionMembers.join(' | ')})[]`;
144148

145149
// Find and update the field in the root interface
@@ -222,7 +226,7 @@ function isEligibleBackbonePath(baseType: string, path: string, baseResource: st
222226
// Must be a root-level field (direct child of the base resource)
223227
const parts = path.split('.');
224228
if (parts.length !== 2) return false;
225-
if (baseResource && parts[0] !== baseResource) return false;
229+
if (baseResource && firstSegment(path) !== baseResource) return false;
226230

227231
return true;
228232
}
@@ -284,9 +288,10 @@ function buildSliceInterfaceBody(
284288
const childId = child.elementId || child.name;
285289
const relativePath = childId.substring(sliceElementId.length + 1);
286290
const match = choiceSlicePattern.exec(relativePath);
287-
if (match) {
291+
const choiceParent = match?.[1]; // e.g. "value[x]"
292+
if (choiceParent) {
288293
choiceTypeSlices.push(child);
289-
choiceTypeParents.add(match[1]); // e.g. "value[x]"
294+
choiceTypeParents.add(choiceParent);
290295
}
291296
}
292297

@@ -303,7 +308,9 @@ function buildSliceInterfaceBody(
303308
if (relativePath.includes('.')) {
304309
if (child.fixedValue !== undefined) {
305310
const segments = relativePath.split('.');
306-
const rawPropName = segments[0];
311+
// firstSegment is segments[0] without the possibly-undefined read; segments
312+
// is still needed for the tail below.
313+
const rawPropName = firstSegment(relativePath);
307314
const propName = rawPropName.replace(/\[x\]$/, '');
308315
// Skip nested values under:
309316
// - choice-type elements ([x]) — handled by direct child or sub-slices
@@ -495,8 +502,8 @@ function updateRootInterfaceField(
495502
i.startsWith(`export interface ${interfaceName} `) || i.startsWith(`export interface ${interfaceName}<`),
496503
);
497504

498-
if (rootIdx >= 0) {
499-
const iface = interfaces[rootIdx];
505+
const iface = rootIdx >= 0 ? interfaces[rootIdx] : undefined;
506+
if (iface !== undefined) {
500507
const fieldPattern = new RegExp(`^(\\s+)${fieldName}[?]?:\\s*.+;`, 'm');
501508

502509
if (fieldPattern.test(iface)) {
@@ -517,10 +524,10 @@ function updateRootInterfaceField(
517524
i.startsWith(`export type ${interfaceName} =`),
518525
);
519526

520-
if (aliasIdx >= 0) {
527+
const alias = aliasIdx >= 0 ? interfaces[aliasIdx] : undefined;
528+
if (alias !== undefined) {
521529
// Convert type alias to interface with the field
522-
const aliasMatch = interfaces[aliasIdx].match(/export type \S+ = (\S+);/);
523-
const baseType = aliasMatch ? aliasMatch[1] : baseResource || '';
530+
const baseType = alias.match(/export type \S+ = (\S+);/)?.[1] ?? baseResource ?? '';
524531
interfaces[aliasIdx] = `export interface ${interfaceName} extends ${baseType} {\n${newFieldLine}\n}`;
525532
}
526533
}

src/generator/emitters/validator/sliceDelegation.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import type { Field } from '../../core/sdTypes.js';
1111
import type { DatatypeProfilePin } from './validatorTypes.js';
12-
import { stripVersionFromCanonicalUrl } from '../../core/utils.js';
12+
import { stripVersionFromCanonicalUrl, firstSegment } from '../../core/utils.js';
1313
// Type-only, so the mutual reference with sliceValidatorGenerator carries no
1414
// runtime cycle.
1515
import type { SliceValidationContext } from './sliceValidatorGenerator.js';
@@ -39,10 +39,11 @@ export function generateProfiledChildDelegation(
3939

4040
// Get discriminator info to build the filter expression
4141
const discriminatorChild = childFieldsWithFixedValue[0];
42+
if (!discriminatorChild) return;
4243
const childFieldId = (discriminatorChild.elementId || discriminatorChild.name).replace(/^[^.]+\./, '');
4344
let discriminatorProp = '';
4445
if (childFieldId.startsWith(sliceElemId + '.')) {
45-
discriminatorProp = childFieldId.substring(sliceElemId.length + 1).split('.')[0];
46+
discriminatorProp = firstSegment(childFieldId.substring(sliceElemId.length + 1));
4647
}
4748
if (!discriminatorProp) return;
4849

@@ -170,8 +171,8 @@ export function detectReferenceProfileDiscriminator(
170171
): string | undefined {
171172
if (!slice.typeOptions?.length) return undefined;
172173
const refType = slice.typeOptions.find(t => t.code === 'Reference');
173-
if (!refType?.targetProfileUrls?.length) return undefined;
174-
const targetUrl = refType.targetProfileUrls[0];
174+
const targetUrl = refType?.targetProfileUrls?.[0];
175+
if (!targetUrl) return undefined;
175176
// 1. Authoritative: resolve from profileUrlToType map
176177
if (profileUrlToType) {
177178
const resolved = profileUrlToType.get(targetUrl);
@@ -215,7 +216,7 @@ export function generateReferenceProfileDiscriminatorValidation(
215216
): void {
216217
// Build within-slice child max constraint checks (e.g., max=0 forbidden fields)
217218
const maxChildren = fields ? collectWithinSliceMaxChildren(slice, fields) : [] as ReturnType<typeof collectWithinSliceMaxChildren>;
218-
const resType = baseResourceType || (slice.elementId || slice.name).split('.')[0];
219+
const resType = baseResourceType || firstSegment(slice.elementId || slice.name);
219220
const childMaxChecks = generateWithinSliceMaxChecks(maxChildren, `_${varName}Refs`, resType, relPath, sliceLabel);
220221

221222
out.push(`
@@ -404,7 +405,7 @@ export function detectProfileDiscriminator(
404405

405406
// 2. Try to extract from profile URL last segment (core profiles)
406407
const profileUrl = profileUrls[0];
407-
const lastSegment = profileUrl.split('/').pop() || '';
408+
const lastSegment = profileUrl?.split('/').pop() || '';
408409
if (/^[A-Z][a-zA-Z]+$/.test(lastSegment)) {
409410
return { resourceType: lastSegment, profileUrls };
410411
}

0 commit comments

Comments
 (0)