From 70fcc00971429f8090906157bcab8b6f54370c70 Mon Sep 17 00:00:00 2001 From: Sindre Gulseth Date: Tue, 7 May 2024 20:41:38 +0200 Subject: [PATCH] fix(typeEvaluator): properly handle object splats with non-concrete types --- src/typeEvaluator/typeEvaluate.ts | 88 +++++++++++++++++++------------ 1 file changed, 55 insertions(+), 33 deletions(-) diff --git a/src/typeEvaluator/typeEvaluate.ts b/src/typeEvaluator/typeEvaluate.ts index e6fdaf8..da1b997 100644 --- a/src/typeEvaluator/typeEvaluate.ts +++ b/src/typeEvaluator/typeEvaluate.ts @@ -119,33 +119,41 @@ function handleDerefNode(node: DerefNode, scope: Scope): TypeNode { function mapObjectSplat( node: TypeNode, scope: Scope, - mapper: (name: string, attribute: ObjectAttribute) => void, -) { - if (node.type === 'union') { - for (const scoped of node.of) { - mapObjectSplat(scoped, scope, mapper) - } - } - - if (node.type === 'object') { - // if the rest is unknown the entire object is unknown - if (node.rest !== undefined && node.rest.type === 'unknown') { - return - } - - for (const name in node.attributes) { - if (!node.attributes.hasOwnProperty(name)) { + mapper: (attribute: ObjectAttribute) => ObjectAttribute, +): ObjectTypeNode | UnknownTypeNode | NullTypeNode { + const object = {type: 'object', attributes: {}} satisfies ObjectTypeNode + + const concreteNode = mapConcrete(node, scope, (node) => node) + if (concreteNode.type === 'object') { + for (const name in concreteNode.attributes) { + if (!concreteNode.attributes.hasOwnProperty(name)) { continue } - mapper(name, node.attributes[name]) + object.attributes[name] = mapper(concreteNode.attributes[name]) } - if (node.rest !== undefined) { - const rest = mapConcrete(node.rest, scope, (rest) => rest) - mapObjectSplat(rest, scope, mapper) + if (concreteNode.rest !== undefined) { + const concreteRest = mapConcrete(concreteNode.rest, scope, (rest) => rest) + // if the rest is unknown the entire object is unknown + if (concreteRest.type === 'unknown') { + return {type: 'unknown'} + } + if (concreteRest.type !== 'object') { + return {type: 'null'} + } + for (const name in concreteRest.attributes) { + // eslint-disable-next-line + if (!concreteRest.attributes.hasOwnProperty(name)) { + continue + } + object.attributes[name] = mapper(concreteRest.attributes[name]) + } } } + + return object } + function handleObjectNode(node: ObjectNode, scope: Scope) { $trace('object.node %O', node) $trace('object.scope %O', scope) @@ -162,9 +170,16 @@ function handleObjectNode(node: ObjectNode, scope: Scope) { if (attr.type === 'ObjectSplat') { const value = walk({node: attr.value, scope}) $trace('object.splat.value %O', value) - mapObjectSplat(value, scope, (name, attribute) => { - attributes[name] = attribute - }) + const mapped = mapObjectSplat(value, scope, (attribute) => attribute) + if (mapped.type === 'unknown' || mapped.type === 'null') { + return mapped + } + for (const name in mapped.attributes) { + if (!mapped.attributes.hasOwnProperty(name)) { + continue + } + attributes[name] = mapped.attributes[name] + } } if (attr.type === 'ObjectConditionalSplat') { const condition = resolveCondition(attr.condition, scope) @@ -172,19 +187,26 @@ function handleObjectNode(node: ObjectNode, scope: Scope) { if (condition || condition === undefined) { const value = walk({node: attr.value, scope}) - mapObjectSplat(value, scope, (name, attribute) => { + const mapped = mapObjectSplat(value, scope, (attribute) => { if (condition) { - attributes[name] = attribute - } else if (condition === undefined) { - attributes[name] = { - type: 'objectAttribute', - value: attribute.value, - optional: true, - } - } else { - throw new Error('Unexpected condition') + return attribute + } + + return { + type: 'objectAttribute', + value: attribute.value, + optional: true, } }) + if (mapped.type === 'unknown' || mapped.type === 'null') { + return mapped + } + for (const name in mapped.attributes) { + if (!mapped.attributes.hasOwnProperty(name)) { + continue + } + attributes[name] = mapped.attributes[name] + } } } }