UNPKG

core-types

Version:

Generic type declarations for e.g. TypeScript, GraphQL and JSON Schema

286 lines (285 loc) 10.8 kB
import { simplifySingle } from './simplifications/single.js'; import { mergeConstEnumUnion } from './simplifications/const-enum.js'; import { intersectConstEnum } from './simplifications/intersect-const-enum.js'; import { MalformedTypeError } from './error.js'; import { extractAnnotations, mergeAnnotations } from './annotation.js'; import { copyName, firstSplitTypeIndex, flattenSplitTypeValues, isNodeDocument, splitTypes, } from './util.js'; const enumableTypeNames = [ 'any', 'string', 'number', 'integer', 'boolean', ]; export function simplify(node, { mergeObjects = false } = {}) { const ctx = { mergeObjects, refs: new Map(), }; if (Array.isArray(node)) return node.map(node => simplifyImpl(node, ctx)); if (isNodeDocument(node)) { const simplified = { ...node, types: simplify(node.types, ctx), }; if (!mergeObjects) return simplified; // Run simplification again, with the named refs available to merge // further, if necessary simplified.types.forEach(type => { ctx.refs.set(type.name, type); }); simplified.types = simplified.types.map(node => simplifyImpl(node, ctx)); return simplified; } return simplifyImpl(node, ctx); } export function simplifyImpl(node, ctx) { const wrapName = (newNode) => copyName(node, newNode); if (node.type === 'tuple') { return { ...node, elementTypes: node.elementTypes.map(type => simplify(type)), ...(node.additionalItems && typeof node.additionalItems === 'object' ? { additionalItems: simplify(node.additionalItems) } : {}), }; } else if (node.type === 'array') { return { ...node, elementType: simplify(node.elementType) }; } else if (node.type === 'object') { return { ...node, properties: Object.fromEntries(Object.entries(node.properties) .map(([name, { node, required }]) => [name, { node: simplify(node), required }])), ...(node.additionalProperties && typeof node.additionalProperties === 'object' ? { additionalProperties: simplify(node.additionalProperties) } : {}), }; } else if (node.type !== 'and' && node.type !== 'or') return wrapName(simplifySingle(node)); else if (node.type === 'and') { const and = maybeMergeObjects(simplifyIntersection([].concat(...node.and.map(node => { const simplifiedNode = simplify(node); return simplifiedNode.and ? simplifiedNode.and : [simplifiedNode]; }))), ctx); if (and.length === 1) return wrapName({ ...and[0], ...mergeAnnotations([extractAnnotations(node), and[0]]) }); return wrapName({ type: 'and', and, ...extractAnnotations(node) }); } else if (node.type === 'or') { const or = simplifyUnion([].concat(...node.or.map(node => { const simplifiedNode = simplify(node); return simplifiedNode.or ? simplifiedNode.or : [simplifiedNode]; }))); if (or.length === 1) return wrapName({ ...or[0], ...mergeAnnotations([extractAnnotations(node), or[0]]) }); return wrapName({ type: 'or', or, ...extractAnnotations(node) }); } else { // istanbul ignore next throw new MalformedTypeError("Invalid node", node); } } function maybeMergeObjects(nodes, ctx) { const { mergeObjects, refs } = ctx; if (!mergeObjects || nodes.length < 2) return nodes; const visited = new Set(); const expandNode = (node) => { if (visited.has(node)) throw new Error(`Cyclic dependency detected`); visited.add(node); if (node.type === 'object') return [node]; else if (node.type === 'ref') { const ref = refs.get(node.ref); if (!ref) // Return the node itself if the ref wasn't found - we'll not // try to merge this tree if there are missing refs. return [node]; return expandNode(ref); } else if (node.type === 'and') return node.and.flatMap(node => expandNode(node)); return [node]; }; const expanded = nodes.flatMap(node => expandNode(node)); if (expanded.some(node => node.type !== 'object')) // Any of the nodes was not an object, so won't try to merge. return nodes; const objects = expanded; const mergedAnnotations = mergeAnnotations(objects.map(obj => extractAnnotations(obj))); // Pick the loosest value const additionalProperties = objects.reduce((prev, cur) => prev === false ? cur.additionalProperties : prev === true ? prev : cur.additionalProperties, false); const allProperties = new Map(); objects.forEach(obj => { Object.entries(obj.properties).forEach(([name, prop]) => { const props = allProperties.get(name) ?? []; props.push(prop); allProperties.set(name, props); }); }); const properties = Object.fromEntries([...allProperties.entries()] .map(([name, props]) => { // If any is required, it's required const required = props.reduce((prev, cur) => prev || cur.required, false); const node = simplifyImpl({ type: 'and', and: props.map(prop => prop.node), }, ctx); return [name, { node, required }]; })); return [{ type: 'object', properties, additionalProperties, ...mergedAnnotations, }]; } // Combine types/nodes where one is more generic than some other, or where // they can be combined to fewer nodes. function simplifyUnion(nodes) { const typeMap = splitTypes(nodes); if (typeMap.any.length > 0) { const enums = mergeConstEnumUnion(typeMap.any.map(({ node }) => node)); if (enums.length === 0) // If any type in a set of types is an "any" type, without const // or enum, the whole union is "any". return [{ type: 'any', ...mergeAnnotations(typeMap.any.map(({ node }) => node)), }]; } for (const [_typeName, _types] of Object.entries(typeMap)) { const typeName = _typeName; if (!enumableTypeNames.includes(typeName) || !_types.length) continue; const orderedTypes = _types; const types = orderedTypes.map(({ node }) => node); const merged = mergeConstEnumUnion(types); if (merged.length === 0) typeMap[typeName] = [{ node: { type: typeName, ...mergeAnnotations(types), }, order: firstSplitTypeIndex(orderedTypes), }]; else typeMap[typeName] = [{ node: simplifySingle({ type: typeName, enum: merged, ...mergeAnnotations(types), }), order: firstSplitTypeIndex(orderedTypes), }]; } if (typeMap.or.length > 0) typeMap.or = typeMap.or.filter(({ node }) => node.or.length > 0); if (typeMap.and.length > 0) typeMap.and = typeMap.and .filter(({ node }) => node.and.length > 0); return flattenSplitTypeValues(typeMap); } // Combine types/nodes and exclude types, const and enum where other are // narrower/stricter. function simplifyIntersection(nodes) { const typeMap = splitTypes(nodes); if (typeMap.any.length > 0) { if (typeMap.and.length === 0 && typeMap.or.length === 0 && typeMap.ref.length === 0 && typeMap.null.length === 0 && typeMap.string.length === 0 && typeMap.number.length === 0 && typeMap.integer.length === 0 && typeMap.boolean.length === 0 && typeMap.object.length === 0 && typeMap.array.length === 0 && typeMap.tuple.length === 0) return [{ type: 'any', ...mergeAnnotations(typeMap.any.map(({ node }) => node)), }]; else // A more precise type will supercede this typeMap.any = []; } const cast = (nodes) => nodes.map(({ node }) => node); if (typeMap.boolean.length > 1) typeMap.boolean = [{ node: intersectConstEnum([ ...typeMap.boolean.map(({ node }) => node), ...cast(typeMap.any), ]), order: firstSplitTypeIndex(typeMap.boolean), }]; if (typeMap.string.length > 1) typeMap.string = [{ node: intersectConstEnum([ ...typeMap.string.map(({ node }) => node), ...cast(typeMap.any), ]), order: firstSplitTypeIndex(typeMap.string), }]; if (typeMap.number.length > 0 && typeMap.integer.length > 0) { typeMap.number = [{ node: intersectConstEnum([ ...typeMap.number.map(({ node }) => node), ...cast(typeMap.integer), ...cast(typeMap.any), ]), order: firstSplitTypeIndex(typeMap.number), }]; typeMap.integer = []; } else if (typeMap.number.length > 1) typeMap.number = [{ node: intersectConstEnum([ ...typeMap.number.map(({ node }) => node), ...cast(typeMap.any), ]), order: firstSplitTypeIndex(typeMap.number), }]; else if (typeMap.integer.length > 1) typeMap.integer = [{ node: intersectConstEnum([ ...typeMap.integer.map(({ node }) => node), ...cast(typeMap.any), ]), order: firstSplitTypeIndex(typeMap.integer), }]; if (typeMap.or.length > 0) typeMap.or = typeMap.or.filter(({ node }) => node.or.length > 0); if (typeMap.and.length > 0) typeMap.and = typeMap.and .filter(({ node }) => node.and.length > 0); return flattenSplitTypeValues(typeMap); }