Refactor type analysis utilities and field collection logic using type stack management

This commit is contained in:
2026-04-27 16:01:34 +02:00
parent d5ba85b55f
commit 7eb475e7f1
2 changed files with 91 additions and 11 deletions

View File

@@ -21,6 +21,7 @@ interface TraversalOptions {
skipProperties: ReadonlySet<string>; skipProperties: ReadonlySet<string>;
includeIdentityProperties: boolean; includeIdentityProperties: boolean;
skipPropertiesForThisObject?: ReadonlySet<string>; skipPropertiesForThisObject?: ReadonlySet<string>;
typeStack: ReadonlySet<string>;
} }
const AUTHORABLE_ROOTS: readonly FieldRoot[] = [ const AUTHORABLE_ROOTS: readonly FieldRoot[] = [
@@ -313,6 +314,29 @@ function isLeafType(type: ts.Type): boolean {
); );
} }
function getTypeKey(type: ts.Type): string {
const internalType = type as ts.Type & { id?: number };
return String(
internalType.id ??
type.aliasSymbol?.escapedName ??
type.symbol?.escapedName ??
type.flags
);
}
function getTypeLabel(type: ts.Type): string {
try {
return checker.typeToString(type);
} catch {
return getTypeKey(type);
}
}
function isCallableType(type: ts.Type): boolean {
const normalizedType = withoutNullish(type);
return normalizedType.getCallSignatures().length > 0;
}
function getArrayElementType(type: ts.Type): ts.Type | null { function getArrayElementType(type: ts.Type): ts.Type | null {
const normalizedType = withoutNullish(type); const normalizedType = withoutNullish(type);
@@ -325,7 +349,11 @@ function getArrayElementType(type: ts.Type): ts.Type | null {
function isTypedArrayType(type: ts.Type): boolean { function isTypedArrayType(type: ts.Type): boolean {
const normalizedType = withoutNullish(type); const normalizedType = withoutNullish(type);
const typeName = checker.typeToString(normalizedType); const typeName = String(
normalizedType.aliasSymbol?.escapedName ??
normalizedType.symbol?.escapedName ??
""
);
return TYPED_ARRAY_TYPE_NAMES.has(typeName); return TYPED_ARRAY_TYPE_NAMES.has(typeName);
} }
@@ -469,6 +497,14 @@ function collectFields(
return; return;
} }
if (isCallableType(normalizedType)) {
entries.push({
path: currentPath,
condition: options.condition
});
return;
}
const arrayElementType = getArrayElementType(normalizedType); const arrayElementType = getArrayElementType(normalizedType);
if (arrayElementType !== null) { if (arrayElementType !== null) {
@@ -492,11 +528,37 @@ function collectFields(
} }
if (normalizedType.isUnion()) { if (normalizedType.isUnion()) {
collectUnionFields(normalizedType.types, currentPath, entries, options); const typeKey = getTypeKey(normalizedType);
if (options.typeStack.has(typeKey)) {
entries.push({
path: currentPath,
condition: options.condition
});
return;
}
collectUnionFields(normalizedType.types, currentPath, entries, {
...options,
typeStack: new Set([...options.typeStack, typeKey])
});
return; return;
} }
collectObjectFields(normalizedType, currentPath, entries, options); const typeKey = getTypeKey(normalizedType);
if (options.typeStack.has(typeKey)) {
entries.push({
path: currentPath,
condition: options.condition
});
return;
}
collectObjectFields(normalizedType, currentPath, entries, {
...options,
typeStack: new Set([...options.typeStack, typeKey])
});
} }
function collectUnionFields( function collectUnionFields(
@@ -548,7 +610,7 @@ function collectUnionFields(
const typeLabels = objectTypes.map((objectType) => { const typeLabels = objectTypes.map((objectType) => {
const propertyType = getPropertyType(objectType, name); const propertyType = getPropertyType(objectType, name);
return propertyType === null ? "" : checker.typeToString(propertyType); return propertyType === null ? "" : getTypeLabel(propertyType);
}); });
return new Set(typeLabels).size === 1; return new Set(typeLabels).size === 1;
@@ -573,7 +635,7 @@ function collectUnionFields(
? "" ? ""
: literalUnionLabels(discriminatorType)[0] ?? ""; : literalUnionLabels(discriminatorType)[0] ?? "";
const groupKey = const groupKey =
discriminator === null ? checker.typeToString(objectType) : discriminatorValue; discriminator === null ? getTypeLabel(objectType) : discriminatorValue;
groupedTypes.set(groupKey, [...(groupedTypes.get(groupKey) ?? []), objectType]); groupedTypes.set(groupKey, [...(groupedTypes.get(groupKey) ?? []), objectType]);
} }
@@ -594,7 +656,8 @@ function collectUnionFields(
collectUnionFields(groupTypes, currentPath, entries, { collectUnionFields(groupTypes, currentPath, entries, {
condition, condition,
skipProperties: skippedForGroup, skipProperties: skippedForGroup,
includeIdentityProperties: options.includeIdentityProperties includeIdentityProperties: options.includeIdentityProperties,
typeStack: options.typeStack
}); });
continue; continue;
} }
@@ -603,7 +666,8 @@ function collectUnionFields(
condition, condition,
skipProperties: options.skipProperties, skipProperties: options.skipProperties,
includeIdentityProperties: options.includeIdentityProperties, includeIdentityProperties: options.includeIdentityProperties,
skipPropertiesForThisObject: skippedForGroup skipPropertiesForThisObject: skippedForGroup,
typeStack: options.typeStack
}); });
} }
} }
@@ -630,7 +694,8 @@ function collectObjectFields(
collectFields(propertyType, `${currentPath}.${propertyName}`, entries, { collectFields(propertyType, `${currentPath}.${propertyName}`, entries, {
condition: options.condition, condition: options.condition,
skipProperties: options.skipProperties, skipProperties: options.skipProperties,
includeIdentityProperties: options.includeIdentityProperties includeIdentityProperties: options.includeIdentityProperties,
typeStack: options.typeStack
}); });
} }
} }
@@ -688,7 +753,8 @@ function collectRootFields(
collectFields(getRootType(root), root.path, entries, { collectFields(getRootType(root), root.path, entries, {
condition: null, condition: null,
skipProperties: new Set(root.skipProperties ?? []), skipProperties: new Set(root.skipProperties ?? []),
includeIdentityProperties includeIdentityProperties,
typeStack: new Set()
}); });
return { return {

View File

@@ -576,8 +576,8 @@ describe("validateSceneDocument", () => {
navigationMode: "invalidMode" as unknown as "firstPerson", navigationMode: "invalidMode" as unknown as "firstPerson",
interactionReachMeters: Number.NaN, interactionReachMeters: Number.NaN,
interactionAngleDegrees: Number.NaN, interactionAngleDegrees: Number.NaN,
allowLookInputTargetSwitch: true, allowLookInputTargetSwitch: "yes",
targetButtonCyclesActiveTarget: false, targetButtonCyclesActiveTarget: 1,
movementTemplate: { movementTemplate: {
kind: "invalidTemplate", kind: "invalidTemplate",
moveSpeed: 0, moveSpeed: 0,
@@ -612,6 +612,7 @@ describe("validateSceneDocument", () => {
sprint: "", sprint: "",
crouch: "", crouch: "",
interact: "", interact: "",
clearTarget: "",
pauseTime: "" pauseTime: ""
}, },
gamepad: { gamepad: {
@@ -620,6 +621,7 @@ describe("validateSceneDocument", () => {
sprint: "invalidButton", sprint: "invalidButton",
crouch: "invalidButton", crouch: "invalidButton",
interact: "invalidButton", interact: "invalidButton",
clearTarget: "invalidButton",
pauseTime: "invalidButton" pauseTime: "invalidButton"
} }
} as unknown as ReturnType< } as unknown as ReturnType<
@@ -657,6 +659,12 @@ describe("validateSceneDocument", () => {
expect.objectContaining({ expect.objectContaining({
code: "invalid-player-start-interaction-angle" code: "invalid-player-start-interaction-angle"
}), }),
expect.objectContaining({
code: "invalid-player-start-look-input-target-switch"
}),
expect.objectContaining({
code: "invalid-player-start-target-button-cycles-active-target"
}),
expect.objectContaining({ expect.objectContaining({
code: "invalid-player-start-movement-template-kind" code: "invalid-player-start-movement-template-kind"
}), }),
@@ -714,6 +722,9 @@ describe("validateSceneDocument", () => {
expect.objectContaining({ expect.objectContaining({
code: "invalid-player-start-interact-keyboard-binding" code: "invalid-player-start-interact-keyboard-binding"
}), }),
expect.objectContaining({
code: "invalid-player-start-clear-target-keyboard-binding"
}),
expect.objectContaining({ expect.objectContaining({
code: "invalid-player-start-pause-keyboard-binding" code: "invalid-player-start-pause-keyboard-binding"
}), }),
@@ -729,6 +740,9 @@ describe("validateSceneDocument", () => {
expect.objectContaining({ expect.objectContaining({
code: "invalid-player-start-interact-gamepad-binding" code: "invalid-player-start-interact-gamepad-binding"
}), }),
expect.objectContaining({
code: "invalid-player-start-clear-target-gamepad-binding"
}),
expect.objectContaining({ expect.objectContaining({
code: "invalid-player-start-pause-gamepad-binding" code: "invalid-player-start-pause-gamepad-binding"
}), }),