haystack-codegen
Version:
Project Haystack Core code generation tools
299 lines (298 loc) • 10.1 kB
JavaScript
"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;