UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

1,645 lines (1,514 loc) 104 kB
/* ************************************************************************ * * qooxdoo-compiler - node.js based replacement for the Qooxdoo python * toolchain * * https://github.com/qooxdoo/qooxdoo * * Copyright: * 2011-2017 Zenesis Limited, http://www.zenesis.com * * License: * MIT: https://opensource.org/licenses/MIT * * This software is provided under the same licensing terms as Qooxdoo, * please see the LICENSE file in the Qooxdoo project's top-level directory * for details. * * Authors: * * John Spackman (john.spackman@zenesis.com, @johnspackman) * * *********************************************************************** */ /* eslint-disable padded-blocks */ var fs = require("fs"); var babelCore = require("@babel/core"); var types = require("@babel/types"); var babylon = require("@babel/parser"); var async = require("async"); var log = qx.tool.utils.LogManager.createLog("analyser"); /** * Helper method that collapses the MemberExpression into a string * @param node * @returns {string} */ function collapseMemberExpression(node) { var done = false; function doCollapse(node) { if (node.type == "ThisExpression") { return "this"; } if (node.type == "Super") { return "super"; } if (node.type == "Identifier") { return node.name; } if (node.type == "ArrayExpression") { var result = []; node.elements.forEach(element => result.push(doCollapse(element))); return result; } if (node.type != "MemberExpression") { return "(" + node.type + ")"; } if (types.isIdentifier(node.object)) { let str = node.object.name; if (node.property.name) { str += "." + node.property.name; } else { done = true; } return str; } var str; if (node.object.type == "ArrayExpression") { str = "[]"; } else { str = doCollapse(node.object); } if (done) { return str; } // `computed` is set if the expression is a subscript, eg `abc[def]` if (node.computed) { done = true; } else if (node.property.name) { str += "." + node.property.name; } else { done = true; } return str; } return doCollapse(node); } function isCollapsibleLiteral(node) { let nodeType = node.type; return ( nodeType === "Literal" || nodeType === "StringLiteral" || nodeType === "NumericLiteral" || nodeType === "BooleanLiteral" || nodeType === "BigIntLiteral" ); } /** * Helper method that expands a dotted string into MemberExpression * @param str * @returns {*} */ function expandMemberExpression(str) { var segs = str.split("."); var expr = types.memberExpression( types.identifier(segs[0]), types.identifier(segs[1]) ); for (var i = 2; i < segs.length; i++) { expr = types.memberExpression(expr, types.identifier(segs[i])); } return expr; } function literalValueToExpression(value) { if (value === null || value === undefined) { return types.nullLiteral(); } if (typeof value == "boolean") { return types.booleanLiteral(value); } if (typeof value == "number") { return types.numericLiteral(value); } if (typeof value == "string") { return types.stringLiteral(value); } if (qx.lang.Type.isRegExp(value)) { return types.regExpLiteral(value.toString()); } if (qx.lang.Type.isDate(value)) { return types.stringLiteral(value.toString()); } if (qx.lang.Type.isArray(value)) { var arr = []; value.forEach(function (item) { arr.push(literalValueToExpression(item)); }); return types.arrayExpression(arr); } if (typeof value != "object") { log.error("Cannot serialise value " + value + " into AST"); return types.nullLiteral(); } var properties = []; for (var key in value) { var expr = literalValueToExpression(value[key]); var prop = types.objectProperty(types.stringLiteral(key), expr); properties.push(prop); } return types.objectExpression(properties); } function formatValueAsCode(value) { if (value === undefined) { return "undefined"; } if (value === null) { return "null"; } if (typeof value === "string") { return JSON.stringify(value); } if (typeof value === "object" && value instanceof Date) { return "new Date(" + value.getTime() + ")"; } if (qx.lang.Type.isArray(value)) { return "[" + value.map(formatValueAsCode).join(", ") + "]"; } return value.toString(); } /** * A class file is parsed and anaysed into an instance of ClassFile; it is * connected to the Analyser that found the class so that dependencies can be * identified. */ qx.Class.define("qx.tool.compiler.ClassFile", { extend: qx.core.Object, /** * Constructor * * @param analyser {Analyser} the Analyser that found the file * @param className {String} the full name of the class * @param library {Library} the Library the class belongs to (note that the class name is * not always enough to identify the library, eg private source files such as qxWeb.js) */ construct(analyser, className, library) { super(); this.__analyser = analyser; this.__className = className; this.__metaStack = []; this.__metaDefinitions = {}; this.__library = library; this.__sourceFilename = analyser.getClassSourcePath(library, className); this.__requiredClasses = {}; this.__environmentChecks = { provided: {}, required: {} }; this.__requiredAssets = []; this.__requiredFonts = {}; this.__translations = []; this.__markers = []; this.__haveMarkersFor = {}; this.__scope = { parent: null, vars: {}, unresolved: {} }; this.__externals = []; this.__commonjsModules = {}; this.__taskQueueDrains = []; this.__taskQueue = async.queue(function (task, cb) { task(cb); }); this.__taskQueue.drain = this._onTaskQueueDrain; this.__taskQueue.error = err => { qx.tool.compiler.Console.error(err.stack || err); }; analyser.getIgnores().forEach(s => this.addIgnore(s)); this.__globalSymbols = {}; this.__privates = {}; this.__blockedPrivates = {}; this.__privateMangling = analyser.getManglePrivates(); const CF = qx.tool.compiler.ClassFile; const addSymbols = arr => arr.forEach(s => (this.__globalSymbols[s] = true)); if (analyser.getGlobalSymbols().length) { addSymbols(analyser.getGlobalSymbols()); } else { addSymbols(CF.QX_GLOBALS); addSymbols(CF.COMMON_GLOBALS); addSymbols(CF.BROWSER_GLOBALS); } }, members: { __analyser: null, __className: null, __numClassesDefined: 0, __library: null, __requiredClasses: null, __environmentChecks: null, __requiredAssets: null, /** @type{Map<String,Object>} list of fonts indexed by name; the value is an object with `name` and `loc` */ __requiredFonts: null, __translateMessageIds: null, __scope: null, __inDefer: false, __inConstruct: false, __taskQueue: null, __taskQueueDrains: null, __markers: null, __haveMarkersFor: null, __classMeta: null, __metaStack: null, __metaDefinitions: null, __fatalCompileError: false, __translations: null, __dbClassInfo: null, __hasDefer: null, __definingType: null, __sourceFilename: null, __taskQueueDrain: null, __globalSymbols: null, __privates: null, __blockedPrivates: null, __externals: null, __commonjsModules: null, _onTaskQueueDrain() { var cbs = this.__taskQueueDrain; this.__taskQueueDrain = []; cbs.forEach(function (cb) { cb(); }); }, _waitForTaskQueueDrain(cb) { if (this.__taskQueue.length() == 0) { cb(); } else { this.__taskQueueDrains.push(cb); } }, _queueTask(cb) { this.__taskQueue.push(cb); }, /** * Returns the absolute path to the class file * @returns {string} */ getSourcePath() { return this.__sourceFilename; }, /** * Returns the path to the rewritten class file * @returns {string} */ getOutputPath() { return this.__analyser.getClassOutputPath(this.__className); }, /** * Loads the source, transpiles and analyses the code, storing the result in outputPath * * @param callback * {Function} callback for when the load is completed */ load(callback) { var t = this; var className = this.__className; t.__fatalCompileError = false; t.__numClassesDefined = 0; fs.readFile( this.getSourcePath(), { encoding: "utf-8" }, function (err, src) { if (err) { callback(err); return; } var result; try { let babelConfig = t.__analyser.getBabelConfig() || {}; let options = qx.lang.Object.clone(babelConfig.options || {}, true); options.modules = false; let extraPreset = [ { plugins: [] } ]; if (babelConfig.plugins) { for (let key in babelConfig.plugins) { if (babelConfig.plugins[key] === true) { extraPreset[0].plugins.push(require.resolve(key)); } else if (babelConfig.plugins[key]) { extraPreset[0].plugins.push([ require.resolve(key), babelConfig.plugins[key] ]); } } } let myPlugins = t._babelClassPlugins(); var config = { babelrc: false, sourceFileName: t.getSourcePath(), filename: t.getSourcePath(), sourceMaps: true, presets: [ [ { plugins: [myPlugins.CodeElimination] } ], [ { plugins: [myPlugins.Compiler] } ], [require.resolve("@babel/preset-env"), options], [require.resolve("@babel/preset-typescript")], [ require.resolve("@babel/preset-react"), qx.tool.compiler.ClassFile.JSX_OPTIONS ] ], generatorOpts: { compact: false }, parserOpts: { allowSuperOutsideMethod: true, sourceType: "script" }, passPerPreset: true }; if (extraPreset[0].plugins.length) { config.presets.push(extraPreset); } if (this.__privateMangling == "unreadable") { config.blacklist = ["spec.functionName"]; } result = babelCore.transform(src, config); } catch (ex) { qx.tool.compiler.Console.log(ex); t.addMarker("compiler.syntaxError", ex.loc, ex.message); t.__fatalCompileError = true; t._compileDbClassInfo(); callback(); return; } if (!t.__numClassesDefined) { t.addMarker("compiler.missingClassDef"); t.__fatalCompileError = true; t._compileDbClassInfo(); callback(); return; } if (!t.__metaDefinitions[className]) { t.addMarker( "compiler.wrongClassName", null, className, Object.keys(t.__metaDefinitions).join(", ") ); t._compileDbClassInfo(); } var pos = className.lastIndexOf("."); var name = pos > -1 ? className.substring(pos + 1) : className; var outputPath = t.getOutputPath(); qx.tool.utils.Utils.mkParentPath(outputPath, function (err) { if (err) { callback(err); return; } let mappingUrl = name + ".js.map"; if ( qx.lang.Array.contains( t.__analyser.getApplicationTypes(), "browser" ) ) { mappingUrl += "?dt=" + new Date().getTime(); } fs.writeFile( outputPath, result.code + "\n\n//# sourceMappingURL=" + mappingUrl, { encoding: "utf-8" }, function (err) { if (err) { callback(err); return; } fs.writeFile( outputPath + ".map", JSON.stringify(result.map, null, 2), { encoding: "utf-8" }, function (err) { if (err) { callback(err); return; } t._waitForTaskQueueDrain(function () { callback(); }); } ); } ); }); } ); }, /** * Writes the data for the database; updates the record, which may have been previously * used (so needs to be zero'd out) * @param dbClassInfo {Map} */ writeDbInfo(dbClassInfo) { delete dbClassInfo.unresolved; delete dbClassInfo.dependsOn; delete dbClassInfo.assets; delete dbClassInfo.translations; delete dbClassInfo.markers; delete dbClassInfo.fatalCompileError; delete dbClassInfo.commonjsModules; for (var key in this.__dbClassInfo) { dbClassInfo[key] = this.__dbClassInfo[key]; } }, /** * Compiles the DbInfo POJO to be stored in the database about this class * */ _compileDbClassInfo() { var t = this; var dbClassInfo = (this.__dbClassInfo = {}); // Collect the dependencies on other classes var deps = this.getRequiredClasses(); if (t.__usesJsx) { let JSX = qx.tool.compiler.ClassFile.JSX_OPTIONS; let classname = JSX.pragma; let pos = classname.lastIndexOf("."); classname = classname.substring(0, pos); if (!deps[classname]) { deps[classname] = {}; } } for (var name in deps) { var dep = deps[name]; if (!dep.ignore) { if (!dbClassInfo.dependsOn) { dbClassInfo.dependsOn = {}; } dbClassInfo.dependsOn[name] = dep; } } function fixAnnos(section) { if (!section) { return; } Object.keys(section).forEach(name => { if (name[0] == "@") { var value = section[name]; delete section[name]; name = name.substring(1); var meta = section[name]; if (meta) { if (!meta.annotations) { meta.annotations = []; } meta.annotations.push(value); } } }); } var meta = this.getOuterClassMeta(); if (meta) { fixAnnos(meta.events); fixAnnos(meta.members); fixAnnos(meta.statics); if (meta.properties && meta.members) { Object.keys(meta.properties).forEach(name => { let pm = meta.properties[name]; if (pm.apply) { let fm = meta.members[pm.apply]; if (fm) { if (!fm.applyFor) { fm.applyFor = []; } fm.applyFor.push(name); } } }); } // Class heirararchy dbClassInfo.extends = meta.superClass; dbClassInfo.include = meta.mixins.slice(0); dbClassInfo.implement = meta.interfaces.slice(0); } // Environment Checks if ( Object.keys(this.__environmentChecks.provided).length || Object.keys(this.__environmentChecks.required).length ) { dbClassInfo.environment = { provided: [], required: {} }; for (let key in this.__environmentChecks.provided) { dbClassInfo.environment.provided.push(key); } for (let key in this.__environmentChecks.required) { dbClassInfo.environment.required[key] = this.__environmentChecks.required[key]; } } // Save whether the class has a defer method dbClassInfo.hasDefer = this.hasDefer(); // Unresolved symbols dbClassInfo.unresolved = []; for (let name in this.__scope.unresolved) { let item = this.__scope.unresolved[name]; // item is undefined if it has already been removed from the list if (item === undefined) { continue; } // One of multiple classes defined in this file if (this.__metaDefinitions[name]) { continue; } var info = t.__analyser.getSymbolType(name); if (info && info.className) { t._requireClass(info.className, { load: item.load, defer: item.defer }); } else if (info && info.symbolType == "package") { t.deleteReference(name); } else { dbClassInfo.unresolved.push(item); for (var j = 0; j < item.locations.length; j++) { t.addMarker( "symbol.unresolved#" + name, item.locations[j].start, name ); } } } if (!dbClassInfo.unresolved.length) { delete dbClassInfo.unresolved; } // Assets var assets = this.getAssets(); if (assets.length) { dbClassInfo.assets = assets; } // Fonts var fontNames = Object.keys(this.__requiredFonts); if (fontNames.length) { dbClassInfo.fonts = fontNames; for (let fontName in this.__requiredFonts) { if (!this.__analyser.getFont(fontName)) { t.addMarker( "fonts.unresolved#" + fontName, this.__requiredFonts[fontName].loc.start, fontName ); } } } if (meta) { if (meta.aliases) { dbClassInfo.aliases = {}; for (let name in meta.aliases.aliasMap) { dbClassInfo.aliases[name] = meta.aliases.aliasMap[name]; } } if (meta.themeMeta) { dbClassInfo.themeMeta = {}; for (let name in meta.themeMeta.themeMetaMap) { dbClassInfo.themeMeta[name] = meta.themeMeta.themeMetaMap[name]; } } } if (this.__externals.length) { dbClassInfo.externals = this.__externals; } // Translation if (this.__translations.length) { dbClassInfo.translations = this.__translations.slice(0); } // Markers if (this.__markers.length) { dbClassInfo.markers = qx.lang.Array.clone(this.__markers); } // Errors if (this.__fatalCompileError) { dbClassInfo.fatalCompileError = true; } // CommonJS modules if (Object.keys(this.__commonjsModules).length > 0) { dbClassInfo.commonjsModules = {}; for (let moduleName in this.__commonjsModules) { dbClassInfo.commonjsModules[moduleName] = [ ...this.__commonjsModules[moduleName] ]; } } return dbClassInfo; }, /** * Returns the loaded meta data */ getOuterClassMeta() { let src = this.__metaDefinitions[this.__className] || null; if (!src) { return src; } let dest = {}; Object.keys(src) .filter(key => key[0] != "_") .forEach(key => (dest[key] = src[key])); return dest; }, /** * Babel plugin */ _babelClassPlugins() { var t = this; function getKeyName(key) { var keyName = key.type == "StringLiteral" ? key.value : key.name; return keyName; } function checkNodeJsDocDirectives(node) { var jsdoc = getJsDoc(node.leadingComments); if (jsdoc) { checkJsDocDirectives(jsdoc, node.loc); } return jsdoc; } function checkJsDocDirectives(jsdoc, loc) { if (!jsdoc) { return jsdoc; } if (jsdoc["@use"]) { jsdoc["@use"].forEach(function (elem) { t._requireClass(elem.body, { where: "use", load: false, location: loc }); }); } if (jsdoc["@require"]) { jsdoc["@require"].forEach(function (elem) { t._requireClass(elem.body, { where: "require", load: false, location: loc }); }); } if (jsdoc["@optional"]) { jsdoc["@optional"].forEach(function (elem) { t.addIgnore(elem.body); }); } if (jsdoc["@ignore"]) { jsdoc["@ignore"].forEach(function (elem) { t.addIgnore(elem.body); }); } if (jsdoc["@external"]) { jsdoc["@external"].forEach(function (elem) { t.addExternal(elem.body); t._requireAsset(elem.body); }); } if (jsdoc["@asset"]) { jsdoc["@asset"].forEach(function (elem) { t._requireAsset(elem.body); }); } if (jsdoc["@usefont"]) { jsdoc["@usefont"].forEach(function (elem) { t._requireFont(elem.body, loc); }); } return jsdoc; } function enterFunction(path, node, idNode) { node = node || path.node; idNode = idNode || node.id || null; let isClassMember = t.__classMeta && t.__classMeta._topLevel && t.__classMeta._topLevel.keyName == "members" && path.parentPath.parentPath.parentPath == t.__classMeta._topLevel.path; if (idNode) { t.addDeclaration(idNode.name); } t.pushScope(idNode ? idNode.name : null, node, isClassMember); function addDecl(param) { if (param.type == "AssignmentPattern") { addDecl(param.left); } else if (param.type == "RestElement") { t.addDeclaration(param.argument.name); } else if (param.type == "Identifier") { t.addDeclaration(param.name); } else if (param.type == "ArrayPattern") { param.elements.forEach(elem => addDecl(elem)); } else if (param.type == "ObjectPattern") { param.properties.forEach(prop => addDecl(prop.value)); } else { t.addMarker("testForFunctionParameterType", node.loc, param.type); } } node.params.forEach(param => { addDecl(param); }); checkNodeJsDocDirectives(node); if ( node.key?.name === "_createQxObjectImpl" && t.__classMeta.type !== "interface" ) { if (t.__classMeta.type === "mixin") { if (t.__classMeta.className === "qx.core.MObjectId") { return; } qx.tool.compiler.Console.print( "qx.tool.compiler.compiler.mixinQxObjectImpl", t.__classMeta.className ); } const injectCode = `{ let object = qx.core.MObjectId.handleObjects(${ t.__classMeta.className ?? "this.constructor" }, this, ...arguments); if (object !== undefined) { return object; } }`; const injectBlockAst = babylon.parse(injectCode, { errorRecovery: true }).program.body[0]; const bodyLines = node.body.body; node.body.body = [injectBlockAst].concat(bodyLines); path.skip(); } } function exitFunction(path, node) { node = node || path.node; t.popScope(node); } var FUNCTION_DECL_OR_EXPR = { enter: path => enterFunction(path), exit: path => exitFunction(path) }; function getJsDoc(comment) { if (!comment) { return null; } if (!qx.lang.Type.isArray(comment)) { comment = [comment]; } var result = {}; comment.forEach(comment => { var tmp = qx.tool.compiler.jsdoc.Parser.parseComment(comment.value); for (var key in tmp) { var value = tmp[key]; if (!result[key]) { result[key] = value; } else { qx.lang.Array.append(result[key], value); } } }); return result; } function makeMeta(sectionName, functionName, node) { var meta; if (functionName) { var section = t.__classMeta[sectionName]; if (section === undefined) { section = t.__classMeta[sectionName] = {}; } meta = section[functionName]; if (meta === undefined) { meta = section[functionName] = {}; } } else { meta = t.__classMeta[sectionName]; if (meta === undefined) { meta = t.__classMeta[sectionName] = {}; } } meta.location = node.loc; if (node.leadingComments) { let jsdoc = checkNodeJsDocDirectives(node); if (jsdoc) { meta.jsdoc = jsdoc; } } if (sectionName === "members" || sectionName === "statics") { if ( node.type == "ObjectMethod" || node.value.type === "FunctionExpression" || node.value.type === "MemberExpression" ) { meta.type = "function"; } else { meta.type = "variable"; } if (node.async) { meta.async = true; } if (functionName.startsWith("__")) { meta.access = "private"; } else if (functionName.startsWith("_")) { meta.access = "protected"; } else { meta.access = "public"; } } return meta; } var es6ClassDeclarations = 0; var needsQxCoreEnvironment = false; var COLLECT_CLASS_NAMES_VISITOR = { MemberExpression(path) { var self = this; var str = collapseMemberExpression(path.node); t._requireClass(str, { location: path.node.loc }); var info = t.__analyser.getSymbolType(str); if (info && info.symbolType == "class") { self.collectedClasses.push(str); } } }; const CODE_ELIMINATION_VISITOR = { ClassBody: { enter(path) { es6ClassDeclarations++; }, exit(path) { es6ClassDeclarations--; } }, CallExpression(path) { const name = collapseMemberExpression(path.node.callee); const env = t.__analyser.getEnvironment(); if ( env["qx.environment.allowRuntimeMutations"] !== true && (name === "qx.core.Environment.select" || name === "qx.core.Environment.get") && types.isLiteral(path.node.arguments[0]) ) { const arg = path.node.arguments[0]; const envValue = env[arg.value]; if (envValue !== undefined) { if (name === "qx.core.Environment.get") { path.skip(); path.replaceWithSourceString(formatValueAsCode(envValue)); return; } else if (name === "qx.core.Environment.select") { const subPath = path.get("arguments.1"); let option = subPath.node.properties.find( prop => prop.key.value === envValue.toString() ); if (!option) { // try to find default value option = subPath.node.properties.find( prop => prop.key.value === "default" ); } if (option) { // path.skip(); path.replaceWith(option.value); return; } } } needsQxCoreEnvironment = path.node.loc; } }, IfStatement: { exit(path) { let node = path.node; // If it's a literal value, we can eliminate code because we can resolve it now. This // is really important for anything wrapped in `if (qx.core.Environment.get("qx.debug")) ...` // because the `qx.core.Environment.get` is replaced with a literal value and we need to // use this to remove the unwanted code. if (types.isLiteral(node.test)) { if (node.test.value) { path.replaceWith(node.consequent); } else if (node.alternate) { path.replaceWith(node.alternate); } else { path.remove(); } } } }, LogicalExpression: { exit(path) { let node = path.node; if (types.isLiteral(node.left) && types.isLiteral(node.right)) { let result = (node.operator == "&&" && node.left.value && node.right.value) || (node.operator == "||" && (node.left.value || node.right.value)); path.replaceWith(literalValueToExpression(result)); } } }, BinaryExpression: { exit(path) { let node = path.node; if ( isCollapsibleLiteral(node.left) && isCollapsibleLiteral(node.right) ) { if ("+-*/".indexOf(node.operator) > -1) { let result; switch (node.operator) { case "+": result = node.left.value + node.right.value; break; case "-": result = node.left.value - node.right.value; break; case "/": result = node.left.value / node.right.value; break; case "*": result = node.left.value * node.right.value; break; } path.skip(); path.replaceWithSourceString(formatValueAsCode(result)); } else { let result; switch (node.operator) { case "==": result = node.left.value == node.right.value; break; case "===": result = node.left.value === node.right.value; break; case "!=": result = node.left.value != node.right.value; break; case "!==": result = node.left.value !== node.right.value; break; } if (result !== undefined) { path.replaceWith(types.booleanLiteral(Boolean(result))); } } } } }, UnaryExpression: { exit(path) { if ( path.node.operator === "!" && types.isLiteral(path.node.argument) ) { path.replaceWith(types.booleanLiteral(!path.node.argument.value)); } } } }; function collectJson(node, isProperties, jsonPath) { var result; if (node.type == "ObjectExpression") { result = {}; let nextJsonPath = jsonPath ? jsonPath + "." : ""; node.properties.forEach(function (prop) { var key = prop.key.name; if (prop.type == "ObjectMethod") { result[key] = "[[ ObjectMethod Function ]]"; } else { var value = collectJson( prop.value, isProperties, nextJsonPath + key ); result[key] = value; } }); } else if ( node.type == "Literal" || node.type == "StringLiteral" || node.type == "BooleanLiteral" || node.type == "NumericLiteral" || node.type == "NullLiteral" ) { if (typeof node.value == "string") { let isIdentifier = false; if ( isProperties && (jsonPath === "apply" || jsonPath === "transform" || jsonPath === "isEqual") ) { isIdentifier = true; } node.value = t.encodePrivate(node.value, isIdentifier, node.loc); } result = node.value; } else if (node.type == "ArrayExpression") { result = []; node.elements.forEach(function (elem) { result.push(collectJson(elem, isProperties)); }); } else if (node.type == "Identifier") { node.name = t.encodePrivate(node.name, true, node.loc); result = node.name; } else if ( node.type == "CallExpression" || node.type == "FunctionExpression" || node.type == "ArrowFunctionExpression" ) { result = new Function("[[ Function ]]"); } else if (node.type == "MemberExpression") { result = collapseMemberExpression(node); } else if (node.type == "UnaryExpression") { if (node.operator == "-") { let tmp = collectJson(node.argument, isProperties); if (typeof tmp == "number") { return tmp * -1; } } else if (node.operator == "!") { let tmp = collectJson(node.argument, isProperties); if (typeof tmp == "boolean") { return !tmp; } } result = "[[ UnaryExpression ]]"; } else if ( node.type == "NewExpression" || node.type == "BinaryExpression" ) { result = "[[ " + node.type + " ]]"; } else { t.warn( "Cannot interpret AST " + node.type + " at " + t.__className + (node.loc ? " [" + node.loc.start.line + "," + node.loc.start.column + "]" : "") ); result = null; } return result; } const ALLOWED_KEYS = { class: { static: { "@": "object", type: "string", // String statics: "object", // Map environment: "object", // Map defer: "function" // Function }, normal: { "@": "object", "@construct": "object", "@destruct": "object", type: "string", // String extend: "function", // Function implement: "object", // Interface[] include: "object", // Mixin[] construct: "function", // Function statics: "object", // Map properties: "object", // Map members: "object", // Map environment: "object", // Map events: "object", // Map defer: "function", // Function destruct: "function", // Function objects: "object" // Map } }, interface: { extend: "object", // Interface | Interface[] statics: "object", // Map members: "object", // Map properties: "object", // Map events: "object" // Map }, mixin: { include: "object", // Mixin | Mixin[] statics: "object", // Map members: "object", // Map properties: "object", // Map events: "object", // Map destruct: "function", // Function construct: "function", // Function objects: "object" // Map }, theme: { title: "string", // String aliases: "object", // Map type: "string", // String extend: "object", // Theme colors: "object", // Map borders: "object", // Map decorations: "object", // Map fonts: "object", // Map icons: "object", // Map widgets: "object", // Map appearances: "object", // Map meta: "object", // Map include: "object", // Array patch: "object", // Array boot: "function" // Function } }; function isValidExtendClause(prop) { if ( prop.value.type == "MemberExpression" || prop.value.type == "Identifier" || prop.value.type == "NullLiteral" ) { return true; } if (t.__classMeta.type === "class") { return false; } if (prop.value.type == "ArrayExpression") { return prop.value.elements.every( elem => elem.type == "MemberExpression" || elem.type == "Identifier" ); } return false; } const FUNCTION_NAMES = { construct: "$$constructor", destruct: "$$destructor", defer: null }; function ensureCreateQxObjectImpl(membersPropertyPath) { // if we are in a class with no top-level properties, don't bother creating _createQxObjectImpl if ( !membersPropertyPath.parent.properties.some( p => p.key?.name === "objects" ) ) { return; } const membersPropertyNode = membersPropertyPath.node; const memberNames = membersPropertyNode?.value?.properties?.map( it => it.key.name ); if (!memberNames) { t.addMarker( "compiler.membersNotAnObject", membersPropertyPath.loc, t.__classMeta.type ); return; } if (memberNames.includes("_createQxObjectImpl")) { return; } const functionBody = `{ return super._createQxObjectImpl(...arguments); }`; const functionBlock = babylon.parse(functionBody, { errorRecovery: true }).program.body[0]; membersPropertyNode.value.properties.push( types.objectMethod( "method", types.identifier("_createQxObjectImpl"), [], functionBlock ) ); } function checkValidTopLevel(path) { var prop = path.node; var keyName = getKeyName(prop.key); let allowedKeys = ALLOWED_KEYS[t.__classMeta.type]; if (t.__classMeta.type === "class") { allowedKeys = allowedKeys[t.__classMeta.isStatic ? "static" : "normal"]; } if (allowedKeys[keyName] === undefined) { t.addMarker( "compiler.invalidClassDefinitionEntry", prop.loc, t.__classMeta.type, keyName ); } } function handleTopLevelMethods(path, keyName, functionNode) { if (keyName == "defer") { t.__hasDefer = true; t.__inDefer = true; } var isSpecialFunctionName = Object.keys(FUNCTION_NAMES).includes(keyName); t.__classMeta.functionName = isSpecialFunctionName ? FUNCTION_NAMES[keyName] : keyName; if (isSpecialFunctionName) { makeMeta(keyName, null, functionNode); } enterFunction(path, functionNode); path.traverse(VISITOR); exitFunction(path, functionNode); path.skip(); t.__classMeta.functionName = null; } var CLASS_DEF_VISITOR = { ClassBody: { enter(path) { es6ClassDeclarations++; }, exit(path) { es6ClassDeclarations--; } }, ObjectMethod(path) { let functionNode = path.node; if (path.parentPath.parentPath != this.classDefPath) { path.skip(); enterFunction(path, functionNode); path.traverse(VISITOR); exitFunction(path, functionNode); return; } var keyName = getKeyName(path.node.key); checkValidTopLevel(path); handleTopLevelMethods(path, keyName, functionNode); }, ObjectProperty(path) { var prop = path.node; if (path.parentPath.parentPath != this.classDefPath) { path.skip(); path.traverse(VISITOR); return; } var keyName = getKeyName(prop.key); checkValidTopLevel(path); var isSpecialFunctionName = Object.keys(FUNCTION_NAMES).includes(keyName); if (isSpecialFunctionName) { let val = path.node.value; val.leadingComments = (path.node.leadingComments || []).concat( val.leadingComments || [] ); handleTopLevelMethods(path, keyName, val); return; } if (keyName == "extend") { if (!isValidExtendClause(prop)) { t.addMarker("compiler.invalidExtendClause", prop.value.loc); t.__fatalCompileError = true; } else { t.__classMeta.superClass = collapseMemberExpression(prop.value); t._requireClass(t.__classMeta.superClass, { location: path.node.loc }); } } else if (keyName == "type") { var type = prop.value.value; t.__classMeta.isAbstract = type === "abstract"; t.__classMeta.isStatic = type === "static"; t.__classMeta.isSingleton = type === "singleton"; } else if (keyName == "implement") { path.skip(); path.traverse(COLLECT_CLASS_NAMES_VISITOR, { collectedClasses: t.__classMeta.interfaces }); } else if (keyName == "include" || keyName == "patch") { path.skip(); path.traverse(COLLECT_CLASS_NAMES_VISITOR, { collectedClasses: t.__classMeta.mixins }); } else if ( keyName == "members" || keyName == "statics" || keyName == "objects" || keyName == "@" ) { t.__classMeta._topLevel = { path, keyName }; path.skip(); if (keyName === "members" && t.__classMeta.type === "class") { ensureCreateQxObjectImpl(path); } path.traverse(VISITOR); t.__classMeta._topLevel = null; } else if (keyName == "properties") { path.skip(); if (!prop.value.properties) { t.addMarker("class.invalidProperties", prop.loc || null); } else { prop.value.properties.forEach(function (pdNode) { var propName = getKeyName(pdNode.key); var meta = makeMeta("properties", propName, pdNode); var data = collectJson(pdNode.value, true); meta.name = propName; meta.propertyType = "new"; [ "refine", "themeable", "event", "inheritable", "apply", "async", "group", "nullable", "init", "transform" ].forEach(name => (meta[name] = data[name])); if (data.nullable !== undefined) { meta.allowNull = data.nullable; } if (data.check !== undefined) { let checks; if (qx.lang.Type.isArray(data.check)) { checks = meta.possibleValues = data.check; } else { meta.check = data.check; checks = [data.check]; } checks.forEach(check => { if (!qx.tool.compiler.ClassFile.SYSTEM_CHECKS[check]) { let symbolData = t.__analyser.getSymbolType(check); if (symbolData?.symbolType == "class") { t._requireClass(check, { load: false, usage: "dynamic", location: path.node.loc }); } } }); } if (data.init !== undefined) { meta.defaultValue = data.init; } }); } path.traverse(VISITOR); } else if (keyName == "events") { path.skip(); if (prop.value.properties) { prop.value.properties.forEach(function (eventNode) { var eventName = getKeyName(eventNode.key); var meta = makeMeta("events", eventName, eventNode); meta.name = eventName; meta.type = collectJson(eventNode.value); }); } path.traverse(VISITOR); } else if (keyName == "aliases") { path.skip(); if (!prop.value.properties) { t.addMarker("class.invalidAliases", prop.loc || null); } else { var meta = makeMeta("aliases", null, prop); meta.aliasMap = {}; prop.value.properties.forEach(function (aliasNode) { var aliasName = getKeyName(aliasNode.key); var aliasValue = getKeyName(aliasNode.value); meta.aliasMap[aliasName] = aliasValue; }); } } else if (keyName == "meta") { path.skip(); if (!prop.value.properties) { t.addMarker("class.invalidThemeMeta", prop.loc || null); } else { let meta = makeMeta("themeMeta", null, prop); meta.themeMetaMap = {}; prop.value.properties.forEach(function (node) { var key = getKeyName(node.key); var value = collapseMemberExpression(node.value); meta.themeMetaMap[key] = value; }); } path.traverse(VISITOR); } } }; const TYPE = { "qx.Class.define": "class", "qx.Mixin.define": "mixin", "qx.Theme.define": "theme", "qx.Interface.define": "interface", "qx.Bootstrap.define": "class" }; var VISITOR = { NewExpression: { enter(path) { var str = collapseMemberExpression(path.node.callee); t._requireClass(str, { usage: "dynamic", location: path.node.loc }); }, exit(path) { if (t.__analyser.isAddCreatedAt()) { var fn = types.memberExpression( types.identifier("qx"), types.identifier("$$createdAt") ); var tmp = types.callExpression(fn, [ path.node, types.stringLiteral(t.__className.replace(/\./g, "/") + ".js"), types.numericLiteral( path.node.loc ? path.node.loc.start.line : 0 ), types.numericLiteral( path.node.loc ? path.node.loc.start.column : 0 ), types.booleanLiteral(t.__analyser.isVerboseCreatedAt()) ]); path.replaceWith(tmp); path.skip(); } } }, ExpressionStatement: { enter: path => { checkNodeJsDocDirectives(path.node); }, exit: path => { checkNodeJsDocDirectives(path.node); } }, EmptyStatement: path => { checkNodeJsDocDirectives(path.node); }, JSXElement(path) { t.__usesJsx = true; }, Program: { exit(path) { let dbClassInfo = t._compileDbClassInfo(); let copyInfo = {}; let hasLoadDeps = false; if (dbClassInfo.dependsOn) { copyInfo.dependsOn = {}; Object.keys(dbClassInfo.dependsOn).forEach(key => { let tmp = (copyInfo.dependsOn[key] = Object.assign( {}, dbClassInfo.dependsOn[key] )); if (tmp.load) { delete tmp.load; tmp.require = true; hasLoadDeps = true; } }); } if (dbClassInfo.environment) { copyInfo.environment = dbClassInfo.environment; let required = dbClassInfo.environment.required; if (required) { for (let key in required) { if (required[key].load) { hasLoadDeps = true; break; } } } } let tmp = types.variableDeclaration("var", [ types.variableDeclarator( types.identifier("$$dbClassInfo"), literalValueToExpression(copyInfo) ) ]);