UNPKG

haystack-codegen

Version:

Project Haystack Core code generation tools

299 lines (298 loc) 10.1 kB
"use strict"; /* * Copyright (c) 2021, J2 Innovations. All Rights Reserved */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CodeGenerator = exports.TypeGuardOptions = void 0; const haystack_core_1 = require("haystack-core"); const DocNode_1 = require("./nodes/DocNode"); const InterfaceNode_1 = require("./nodes/InterfaceNode"); const InterfaceValueNode_1 = require("./nodes/InterfaceValueNode"); const NamespaceNode_1 = require("./nodes/NamespaceNode"); const TypeGuardNode_1 = require("./nodes/TypeGuardNode"); const util_1 = require("./nodes/util"); /** * Reserved words for features. */ const RESERVED_FEATURE_NAMES = ['valueIsKind']; /** * Typeguard options. */ var TypeGuardOptions; (function (TypeGuardOptions) { TypeGuardOptions["entity"] = "entity"; TypeGuardOptions["all"] = "all"; })(TypeGuardOptions || (exports.TypeGuardOptions = TypeGuardOptions = {})); /** * Generate code for a TypeScript document. */ class CodeGenerator { /** * The def names. */ #names; /** * The defs namespace. */ #namespace; /** * Typeguard options. */ #typeGuardOptions; /** * A name to def cache. */ #nameToDefCache = {}; /** * Construct a new code generator. * * @param names The names of the defs to generate the code for. * @param namespace The defs namespace. */ constructor({ names, namespace, typeGuardOptions, }) { this.#names = names; this.#namespace = namespace; this.#typeGuardOptions = typeGuardOptions; } /** * Generate code from a node. * * @returns The TypeScript code. */ generate() { const doc = new DocNode_1.DocNode(); this.#nameToDefCache = {}; for (const name of this.sortNamesAndFeatures()) { if (!this.#namespace.has(name)) { throw new Error(`Could not find def for ${name}`); } this.addInterface(name, doc); } return (0, util_1.generateCodeFromNode)(doc); } /** * Sort alphabetically by names and then features. * * @returns The def names. */ sortNamesAndFeatures() { // It's nice not to have underscores added to the end of the non-feature // names (duplicates). Therefore process non-feature defs first. const names = this.removeDuplicates(this.#names); const features = []; for (let i = names.length; i >= 0; --i) { if (haystack_core_1.HNamespace.isFeature(names[i])) { features.push(names.splice(i, 1)[0]); } } return [...names.sort(), ...features.sort()]; } /** * Add an interface node. * * @param doc The document node. * @param name The name of the interface to add. */ addInterface(name, doc) { const tags = this.#namespace.tags(name); const def = this.#namespace.byName(name); if (def) { this.addLib(def, doc); } const intNode = new InterfaceNode_1.InterfaceNode({ def: name, name: (0, util_1.makeTypeName)(name, this.#nameToDefCache), doc: def?.get('doc')?.value ?? '', }); this.addExtends(name, doc, intNode); this.addValues(name, tags, intNode); if (haystack_core_1.HNamespace.isFeature(name)) { this.addNamespace(name, doc, intNode); } else { doc.addInterface(intNode); if (!haystack_core_1.HNamespace.isConjunct(name) && def && !CodeGenerator.isDefFromCorePh(def)) { if (this.#typeGuardOptions === TypeGuardOptions.all || this.#namespace.fitsEntity(name)) { this.addTypeGuard(name, doc); } } } } /** * Add the lib for the def to the libs node. * * @param def The def. * @param doc The document node. */ addLib(def, doc) { const lib = def?.get('lib'); if ((0, haystack_core_1.valueIsKind)(lib, haystack_core_1.Kind.Symbol)) { const libDef = this.#namespace.get(lib); if (libDef) { doc.libs.addLib(libDef); } } } /** * Return true if the def is from the core project haystack library. * * @param def The def to test. * @returns True if the def is from the core project haystack library. */ static isDefFromCorePh(def) { return def.get('lib')?.value === 'lib:ph'; } /** * Add the what types the interface extends from by querying the namespace. * * @param name The name of the def to query the super types from. * @param doc The document node. * @param intNode The interface name to add the extend types to. */ addExtends(name, doc, intNode) { for (const sup of this.#namespace.superTypesOf(name)) { let supDefName = sup.defName; // If a super type is dict then attempt to look up an an `of` to map // to the intended type. if (sup.defName === 'dict') { const dictName = this.#namespace .byName(name) ?.get('of')?.value; if (dictName) { supDefName = dictName; } } intNode.extend.push((0, util_1.makeTypeName)(supDefName, this.#nameToDefCache)); this.addInterface(supDefName, doc); } } /** * Add all the values to the interface node. * * @param name The name of the interface node. * @param tags The tags to query. * @param intNode The interface node to add the values to. */ addValues(name, tags, intNode) { // Ensure's no tag is added twice for a node. const tagSet = new Set(); const def = this.#namespace.byName(name); if (def.get('mandatory')?.equals(haystack_core_1.HMarker.make())) { this.addValueNode(def, tagSet, intNode, /*optional*/ false); } for (const tag of tags) { for (const tagOn of this.#namespace.tagOn(tag.defName)) { if (tagOn.defName === name) { const optional = !tag .get('compulsory') ?.equals(haystack_core_1.HMarker.make()); this.addValueNode(tag, tagSet, intNode, optional); } } } } addValueNode(tag, tagSet, intNode, optional) { if (!this.propertyAlreadyExistOnHDict(tag.defName) && !tagSet.has(tag)) { const kind = this.#namespace.defToKind(tag.defName); if (kind) { tagSet.add(tag); const genericInfo = this.resolveGenericInfo(tag); intNode.values.push(new InterfaceValueNode_1.InterfaceValueNode({ name: tag.defName, type: this.resolveType(tag.defName, kind), kind, doc: tag.get('doc')?.value, genericType: genericInfo?.type, genericKind: genericInfo?.kind, optional, })); } } } /** * Return the generic type for the given tag. * * @param tag The tag to resolve the generic parameter from. * @return The generic parameter or an empty string if one can't be resolved. */ resolveGenericInfo(tag) { let typeInfo; // Currently only support generic types for lists. const genericOf = (this.#namespace.defToKind(tag.defName) === haystack_core_1.Kind.List && tag.get('of')?.value) || ''; if (genericOf) { const genericKind = this.#namespace.defToKind(genericOf); if (genericKind) { typeInfo = { type: this.resolveType(genericOf, genericKind), kind: genericKind, }; } } return typeInfo; } /** * Resolve the type name using the existing symbol name and kind. * * @param name The name. * @param kind The kind. * @returns The type name to use. */ resolveType(name, kind) { // If the kind is a dict then we can map it to a generated dict interface. // If the kind is not a dict then map it to its haystack type. return kind === haystack_core_1.Kind.Dict && name !== 'dict' ? (0, util_1.makeTypeName)(name, this.#nameToDefCache) : (0, util_1.convertKindToCtorName)(kind); } /** * Add a namespace. * * @param name The name of the def. * @param doc The document node. * @param intNode The interface node. */ addNamespace(name, doc, intNode) { let feature = name.split(':')[0]; if (RESERVED_FEATURE_NAMES.includes(feature)) { feature += '_'; } const featureDef = this.#namespace.byName(feature); doc.addNamespace(new NamespaceNode_1.NamespaceNode(feature ?? '', intNode, featureDef?.get('doc')?.value ?? '')); } /** * Add a type guard. * * @param name The name of the def. * @param doc The document node. */ addTypeGuard(name, doc) { doc.addTypeGuard(new TypeGuardNode_1.TypeGuardNode(name, (0, util_1.makeTypeName)(name, this.#nameToDefCache))); } /** * Return true if the property already exists on HDict. * * @param prop The property name. * @returns True if it exists. */ propertyAlreadyExistOnHDict(prop) { return !!haystack_core_1.HDict.prototype[prop]; } /** * Remove any duplicates from the array. * * @param names The names process. * @returns The array with no duplicates. */ removeDuplicates(names) { const set = new Set(); names.forEach(set.add, set); return [...names]; } } exports.CodeGenerator = CodeGenerator;