UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

430 lines (383 loc) 12.7 kB
const fs = require("fs"); const path = require("upath"); qx.Class.define("qx.tool.compiler.MetaDatabase", { extend: qx.core.Object, construct() { super(); this.__metaByClassname = {}; this.__metaByFilename = {}; this.__cachedMeta = {}; this.__dirtyClasses = {}; this.__database = {}; }, properties: { /** Where the meta files for individual classes are stored */ rootDir: { init: "compiled/meta", check: "String" } }, members: { /** @type{Map<String,qx.tool.compiler.MetaExtraction>} list of meta indexed by classname */ __metaByClassname: null, /** @type{Map<String,Boolean} list of classes which need to have their second pass */ __dirtyClasses: null, /** The database */ __database: null, /** * Saves the database */ async save() { await qx.tool.utils.Utils.makeDirs(this.getRootDir()); this.__database.classnames = Object.keys(this.__metaByClassname); await qx.tool.utils.Json.saveJsonAsync( this.getRootDir() + "/db.json", this.__database ); }, getDatabase() { return this.__database; }, /** * Loads the database and all of the meta data */ async load() { let filename = this.getRootDir() + "/db.json"; if (!fs.existsSync(filename)) { return; } this.__metaByClassname = {}; this.__dirtyClasses = {}; let data = await qx.tool.utils.Json.loadJsonAsync(filename); this.__database = data; for (let classname of data.classnames) { let filename = this.getRootDir() + "/" + classname.replace(/\./g, "/") + ".json"; if (fs.existsSync(filename)) { await qx.tool.utils.Utils.makeParentDir(filename); let meta = new qx.tool.compiler.MetaExtraction(this.getRootDir()); await meta.loadMeta(filename); this.__metaByClassname[classname] = meta; let classFilename = meta.getMetaData().classFilename; classFilename = path.resolve( path.join(this.getRootDir(), classFilename) ); this.__metaByFilename[classFilename] = meta; } } }, /** * Implementation of `qx.tool.compiler.jsdoc.ITypeResolver` * * @param {*} currentClassMeta * @param {String} type * @returns {String} */ resolveType(currentClassMeta, type) { if (!type) { return type; } // in certain limited circumstances, the code at the end of this method will break usage of vanilla JS types // for example, usage of `String` within a class `qx.bom.*` will instead resolve to `qx.bom.String` // to prevent this, the following object traps the most common vanilla JS types const plainJsTypes = { string: "string", number: "number", boolean: "boolean", object: "Record<any, any>", array: "Array<any>", function: "((...args: any[]) => any)", map: "Map<any, any>", set: "Set<any>", regexp: "RegExp", date: "Date", error: "Error", promise: "Promise<any>" }; if (plainJsTypes[type.toLowerCase()]) { return plainJsTypes[type.toLowerCase()]; } let pos = currentClassMeta.className.lastIndexOf("."); let packageName = pos > -1 ? currentClassMeta.className.substring(0, pos) : null; if (packageName) { pos = type.indexOf("."); if (pos < 0 && this.__metaByClassname[packageName + "." + type]) { return packageName + "." + type; } } return type; }, /** * Adds a file to the database * * @param {String} filename * @param {Boolean} force */ async addFile(filename, force) { filename = await qx.tool.utils.files.Utils.correctCase(filename); filename = path.resolve(filename); let meta = this.__metaByFilename[filename]; if (meta && !force && !(await meta.isOutOfDate())) { return; } meta = new qx.tool.compiler.MetaExtraction(this.getRootDir()); let metaData = await meta.parse(filename); if (metaData.className === undefined) { return; } this.__metaByClassname[metaData.className] = meta; this.__metaByFilename[filename] = meta; this.__dirtyClasses[metaData.className] = true; }, /** * Returns a list of all class names * * @return {String[]} */ getClassnames() { return Object.keys(this.__metaByClassname); }, /** * Returns the meta data for a class * * @param {String} className * @returns */ getMetaData(className) { return this.__metaByClassname[className]?.getMetaData() || null; }, /** * Once all meta data has been loaded, this method traverses the database * to add information that can only be added once all classes are known, * eg which methods override other methods and where they were overridden from */ async reparseAll() { let classnames = Object.keys(this.__dirtyClasses); this.__dirtyClasses = {}; let derivedClassLookup = this.__createDerivedClassLookup(); for (let i = 0; i < classnames.length; i++) { const className = classnames[i]; const derived = derivedClassLookup[className]; for (const derivedClass of derived.values()) { if (!classnames.includes(derivedClass)) { classnames.push(derivedClass); } } } for (let className of classnames) { let meta = this.__metaByClassname[className]; let metaData = meta.getMetaData(); const typeResolver = { resolveType: this.resolveType.bind(this, metaData) }; meta.fixupJsDoc(typeResolver); this.__fixupMembers(metaData); this.__fixupEntries(metaData, "members"); this.__fixupEntries(metaData, "statics"); this.__fixupEntries(metaData, "properties"); let filename = this.getRootDir() + "/" + className.replace(/\./g, "/") + ".json"; await meta.saveMeta(filename); } }, __createDerivedClassLookup() { const lookup = {}; const add = (key, item) => { lookup[key] ??= new Set(); lookup[key].add(item); }; for (let classname in this.__metaByClassname) { lookup[classname] ??= new Set(); // ensuring this makes operations with the lookup simpler let metaData = this.__metaByClassname[classname].getMetaData(); if (metaData.superClass) { add(metaData.superClass, classname); } for (let mixin of metaData.mixins ?? []) { add(mixin, classname); } for (let iface of metaData.interfaces ?? []) { add(iface, classname); } } return lookup; }, /** * Finds info about a method * * @param {*} metaData starting point * @param {String} methodName name of the method * @param {Boolean} firstPass * @returns {*} meta data values to add to the method */ __findSuperMethod(metaData, methodName, firstPass) { if (!firstPass) { let method = metaData.members?.[methodName]; if (method) { return { overriddenFrom: metaData.className }; } } if (metaData.mixins) { for (let mixinName of metaData.mixins) { let mixinMeta = this.__metaByClassname[mixinName]; if (mixinMeta) { let mixinMetaData = mixinMeta.getMetaData(); let method = mixinMetaData.members?.[methodName]; if (method) { return { mixin: mixinName }; } } } } if (!metaData.superClass) { return null; } let superMeta = this.__metaByClassname[metaData.superClass]; if (superMeta) { return this.__findSuperMethod( superMeta.getMetaData(), methodName, false ); } return null; }, /** * @param {*} metaData class metadata * @param {string} entryKind name of the entry type * @param {string} entryName name of the entry * @returns {string[]} list of classes where the entry appears */ __findAppearances(metaData, entryKind, entryName) { const getSuperLikes = meta => [ ...(meta.mixins ?? []), ...(meta.superClass ? [meta.superClass] : []), ...(meta.interfaces ?? []) ]; const resolve = meta => { if (meta[entryKind]?.[entryName]) { appearances.push(meta.className); } }; const appearances = []; const toResolve = getSuperLikes(metaData); while (toResolve.length) { const currentMeta = this.__metaByClassname[toResolve.shift()]; if (currentMeta) { resolve(currentMeta.getMetaData()); toResolve.push(...getSuperLikes(currentMeta.getMetaData())); } } return appearances; }, /** * Discovers data about the members in the hierarchy, eg whether overridden etc * * @param {*} metaData */ __fixupMembers(metaData) { if (!metaData.members) { return; } if (metaData.abstract) { for (const itf of metaData.interfaces ?? []) { const itfMembers = this.__metaByClassname[itf]?.getMetaData().members; for (const memberName in itfMembers ?? {}) { const member = itfMembers[memberName]; if (!metaData.members[memberName]) { metaData.members[memberName] = { ...member, abstract: true, fromInterface: itf }; } } } } for (const methodName in metaData.members) { const methodMeta = metaData.members[methodName]; const superMethod = this.__findSuperMethod(metaData, methodName, true); if (superMethod) { for (const key in superMethod) { methodMeta[key] = superMethod[key]; } } } }, /** * Detects the superlike (class/mixin/interface) appearances and includes the * mixin entries into the class metadata * @param {*} metaData * @param {string} kind */ __fixupEntries(metaData, kind) { metaData[kind] ??= {}; for (const mixin of metaData.mixins ?? []) { const mixinMeta = this.__metaByClassname[mixin]?.getMetaData(); for (const name in mixinMeta?.[kind] ?? {}) { const appearsIn = this.__findAppearances(metaData, kind, name); const meta = qx.lang.Object.clone(mixinMeta[kind][name]); meta.mixin = mixin; meta.appearsIn = appearsIn; metaData[kind][name] = meta; } } for (const name in metaData[kind] ?? {}) { const meta = metaData[kind][name]; meta.appearsIn = this.__findAppearances(metaData, kind, name); } }, /** * Gets a flattened type hierarchy for a class * @param {string|object} metaOrClassName - the classname or the meta data of the class to get the hierarchy for * @returns the type hierarchy * */ getHierarchyFlat(metaOrClassName) { const meta = typeof metaOrClassName === "string" ? this.getMetaData(metaOrClassName) : metaOrClassName; const data = { className: meta.className, superClasses: {}, mixins: {}, interfaces: {} }; let toResolve = [meta]; while (toResolve.length) { const currentMeta = toResolve.shift(); if (currentMeta.superClass) { const superClassMeta = this.getMetaData(currentMeta.superClass); if (superClassMeta) { data.superClasses[superClassMeta.className] = superClassMeta; toResolve.push(superClassMeta); } } if (currentMeta.mixins) { for (const mixin of currentMeta.mixins) { const mixinMeta = this.getMetaData(mixin); if (mixinMeta) { data.mixins[mixinMeta.className] = mixinMeta; toResolve.push(mixinMeta); } } } if (currentMeta.interfaces) { for (const iface of currentMeta.interfaces) { const ifaceMeta = this.getMetaData(iface); if (ifaceMeta) { data.interfaces[ifaceMeta.className] = ifaceMeta; toResolve.push(ifaceMeta); } } } } return data; } } });