@qooxdoo/framework
Version:
The JS Framework for Coders
504 lines (452 loc) • 15.4 kB
JavaScript
const fs = require("fs");
const path = require("upath");
qx.Class.define("qx.tool.compiler.MetaExtraction", {
extend: qx.core.Object,
construct(metaRootDir) {
super();
this.setMetaRootDir(metaRootDir || null);
},
properties: {
/** Root directory for meta data; if provided then paths are stored relative, not absolute, which helps make
* meta directories relocatable
*/
metaRootDir: {
init: null,
nullable: true,
check: "String"
}
},
statics: {
/** Meta Data Version - stored in meta data files */
VERSION: 0.3
},
members: {
/** @type{Object} the parsed data*/
__metaData: null,
/**
* Loads the meta from disk
*
* @param {String} filename
*/
async loadMeta(filename) {
let metaData = await qx.tool.utils.Json.loadJsonAsync(filename);
if (metaData?.version === qx.tool.compiler.MetaExtraction.VERSION) {
this.__metaData = metaData;
} else {
this.__metaData = null;
}
},
/**
* Saves the meta to disk
*
* @param {String} filename
*/
async saveMeta(filename) {
await qx.tool.utils.Utils.makeParentDir(filename);
await qx.tool.utils.Json.saveJsonAsync(filename, this.__metaData);
},
/**
* Returns the actual meta data
*
* @returns {*}
*/
getMetaData() {
return this.__metaData;
},
/**
* Checks whether the meta data is out of date compared to the last modified
* timestamp of the classname
*
* @returns {Boolean}
*/
async isOutOfDate() {
let classFilename = this.__metaData.classFilename;
if (this.getMetaRootDir()) {
classFilename = path.join(this.getMetaRootDir(), classFilename);
}
let stat = await fs.promises.stat(classFilename);
let lastModified = this.__metaData?.lastModified;
if (lastModified && lastModified == stat.mtime.getTime()) {
return false;
}
return true;
},
/**
* Parses the file and returns the metadata
*
* @param {String} classFilename the .js file to parse
* @return {Object}
*/
async parse(classFilename) {
classFilename =
await qx.tool.utils.files.Utils.correctCase(classFilename);
let stat = await fs.promises.stat(classFilename);
this.__metaData = {
version: qx.tool.compiler.MetaExtraction.VERSION,
lastModified: stat.mtime.getTime(),
lastModifiedIso: stat.mtime.toISOString()
};
if (this.getMetaRootDir()) {
this.__metaData.classFilename = path.relative(
this.getMetaRootDir(),
classFilename
);
} else {
this.__metaData.classFilename = path.resolve(classFilename);
}
const babelCore = require("@babel/core");
let src = await fs.promises.readFile(classFilename, "utf8");
let plugins = [require("@babel/plugin-syntax-jsx"), this.__plugin()];
var config = {
ast: true,
babelrc: false,
sourceFileName: classFilename,
filename: classFilename,
sourceMaps: false,
presets: [
[
{
plugins: plugins
}
]
],
parserOpts: {
allowSuperOutsideMethod: true,
sourceType: "script"
},
generatorOpts: {
retainLines: true,
compact: false
},
passPerPreset: true
};
let result;
result = babelCore.transform(src, config);
return this.__metaData;
},
/**
* The Babel plugin
*
* @returns {Object}
*/
__plugin() {
let metaData = this.__metaData;
let t = this;
return {
visitor: {
Program(path) {
path.skip();
let found = false;
path.get("body").forEach(path => {
let node = path.node;
if (
node.type == "ExpressionStatement" &&
node.expression.type == "CallExpression"
) {
let str = qx.tool.utils.BabelHelpers.collapseMemberExpression(
node.expression.callee
);
let m = str.match(/^qx\.([a-z]+)\.define$/i);
let definingType = m && m[1];
if (definingType) {
if (found) {
qx.tool.compiler.Console.warn(
`Ignoring class '${node.expression.arguments[0].value}' in file '${metaData.classFilename}' because a class, mixin, or interface was already found in this file.`
);
return;
}
found = true;
metaData.type = definingType.toLowerCase();
metaData.location = {
start: node.loc.start,
end: node.loc.end
};
metaData.className = node.expression.arguments[0].value;
if (typeof metaData.className != "string") {
metaData.className = null;
}
metaData.jsdoc = qx.tool.utils.BabelHelpers.getJsDoc(
node.leadingComments
);
t.__scanClassDef(path.get("expression.arguments")[1]);
}
}
});
}
}
};
},
/**
* Scans the class definition
*
* @param {NodePath} path
*/
__scanClassDef(path) {
let metaData = this.__metaData;
const getFunctionParams = node => {
if (node.type == "ObjectMethod") {
return node.params;
}
if (node.value.type == "FunctionExpression") {
return node.value.params;
}
throw new Error("Don't know how to get parameters from " + node.type);
};
const collapseParamMeta = (node, meta) => {
getFunctionParams(node).forEach((param, i) => {
let name = qx.tool.utils.BabelHelpers.collapseParam(param, i);
meta.params.push({ name });
});
};
path.skip();
let ctorAnnotations = {};
path.get("properties").forEach(path => {
let property = path.node;
let propertyName;
if (property.key.type === "Identifier") {
propertyName = property.key.name;
} else if (property.key.type === "StringLiteral") {
propertyName = property.key.value;
}
// Extend
if (propertyName == "extend") {
metaData.superClass =
qx.tool.utils.BabelHelpers.collapseMemberExpression(property.value);
}
// Class Annotations
else if (propertyName == "@") {
metaData.annotation = path.get("value").toString();
}
// Core
else if (propertyName == "implement" || propertyName == "include") {
let name = propertyName == "include" ? "mixins" : "interfaces";
metaData[name] = [];
// eg: `include: [qx.my.first.MMixin, qx.my.next.MMixin, ..., qx.my.last.MMixin]`
if (property.value.type == "ArrayExpression") {
property.value.elements.forEach(element => {
metaData[name].push(
qx.tool.utils.BabelHelpers.collapseMemberExpression(element)
);
});
}
// eg: `include: qx.my.MMixin`
else if (property.value.type == "MemberExpression") {
metaData[name].push(
qx.tool.utils.BabelHelpers.collapseMemberExpression(
property.value
)
);
}
// eg, `include: qx.core.Environment.filter({...})`
else if (property.value.type === "CallExpression") {
let calleeLiteral = "";
let current = property.value.callee;
while (current) {
let suffix = calleeLiteral ? `.${calleeLiteral}` : "";
if (current.type === "MemberExpression") {
calleeLiteral = current.property.name + suffix;
current = current.object;
continue;
} else if (current.type === "Identifier") {
calleeLiteral = current.name + suffix;
break;
}
throw new Error(
`${metaData.className}: error parsing mixin types: cannot resolve ${property.value.callee.type} in CallExpression`
);
}
if (calleeLiteral === "qx.core.Environment.filter") {
const properties = property.value.arguments[0]?.properties;
properties?.forEach(prop =>
metaData[name].push(
qx.tool.utils.BabelHelpers.collapseMemberExpression(
prop.value
)
)
);
} else {
this.warn(
`${metaData.className}: could not determine mixin types from call \`${calleeLiteral}\`. Type support for this class may be limited.`
);
}
}
}
// Type
else if (propertyName == "type") {
metaData.isSingleton = property.value.value == "singleton";
metaData.abstract = property.value.value == "abstract";
}
// Constructor & Destructor Annotations
else if (propertyName == "@construct" || propertyName == "@destruct") {
ctorAnnotations[propertyName] = path.get("value").toString();
}
// Constructor & Destructor Methods
else if (propertyName == "construct" || propertyName == "destruct") {
let memberMeta = (metaData[propertyName] = {
type: "function",
params: [],
location: {
start: path.node.loc.start,
end: path.node.loc.end
}
});
collapseParamMeta(property, memberMeta);
}
// Events
else if (propertyName == "events") {
metaData.events = {};
property.value.properties.forEach(event => {
let name = event.key.name;
metaData.events[name] = {
type: null,
jsdoc: qx.tool.utils.BabelHelpers.getJsDoc(event.leadingComments)
};
if (event.value.type == "StringLiteral") {
metaData.events[name].type = event.value.value;
metaData.events[name].location = {
start: event.loc.start,
end: event.loc.end
};
}
});
}
// Properties
else if (propertyName == "properties") {
this.__scanProperties(path.get("value.properties"));
}
// Members & Statics
else if (propertyName == "members" || propertyName == "statics") {
let type = propertyName;
let annotations = {};
metaData[type] = {};
path.get("value.properties").forEach(memberPath => {
let member = memberPath.node;
const name = qx.tool.utils.BabelHelpers.collapseMemberExpression(
member.key
);
if (name[0] == "@") {
annotations[name] = memberPath.get("value").toString();
return;
}
let memberMeta = (metaData[type][name] = {
jsdoc: qx.tool.utils.BabelHelpers.getJsDoc(member.leadingComments)
});
memberMeta.access = name.startsWith("__")
? "private"
: name.startsWith("_")
? "protected"
: "public";
memberMeta.location = {
start: member.loc.start,
end: member.loc.end
};
if (
member.type === "ObjectMethod" ||
(member.type === "ObjectProperty" &&
member.value.type === "FunctionExpression")
) {
memberMeta.type = "function";
memberMeta.params = [];
collapseParamMeta(member, memberMeta);
}
});
for (let metaName in annotations) {
let bareName = metaName.substring(1);
let memberMeta = metaData[type][bareName];
if (memberMeta) {
memberMeta.annotation = annotations[metaName];
}
}
}
});
if (ctorAnnotations["@construct"] && metaData.construct) {
metaData.construct.annotation = ctorAnnotations["@construct"];
}
if (ctorAnnotations["@destruct"] && metaData.destruct) {
metaData.destruct.annotation = ctorAnnotations["@destruct"];
}
},
/**
* Scans the properties in the class definition
*
* @param {NodePath[]} paths
*/
__scanProperties(paths) {
let metaData = this.__metaData;
if (!metaData.properties) {
metaData.properties = {};
}
paths.forEach(path => {
path.skip();
let property = path.node;
let name = qx.tool.utils.BabelHelpers.collapseMemberExpression(
property.key
);
metaData.properties[name] = {
location: {
start: path.node.loc.start,
end: path.node.loc.end
},
json: qx.tool.utils.BabelHelpers.collectJson(property.value, true),
jsdoc: qx.tool.utils.BabelHelpers.getJsDoc(property.leadingComments)
};
});
},
fixupJsDoc(typeResolver) {
let metaData = this.__metaData;
const fixupEntry = obj => {
if (obj && obj.jsdoc) {
qx.tool.compiler.jsdoc.Parser.parseJsDoc(obj.jsdoc, typeResolver);
if (obj.jsdoc["@param"] && obj.params) {
let paramsLookup = {};
obj.params.forEach(param => {
paramsLookup[param.name] = param;
});
obj.jsdoc["@param"].forEach(paramDoc => {
let param = paramsLookup[paramDoc.paramName];
if (param) {
if (paramDoc.type) {
param.type = paramDoc.type;
}
if (paramDoc.optional !== undefined) {
param.optional = paramDoc.optional;
}
if (paramDoc.defaultValue !== undefined) {
param.defaultValue = paramDoc.defaultValue;
}
}
});
}
let returnDoc = obj.jsdoc["@return"]?.[0];
if (returnDoc) {
obj.returnType = {
type: returnDoc.type
};
if (returnDoc.optional !== undefined) {
obj.returnType.optional = returnDoc.optional;
}
if (returnDoc.defaultValue !== undefined) {
obj.returnType.defaultValue = returnDoc.defaultValue;
}
}
}
};
const fixupSection = sectionName => {
var section = metaData[sectionName];
if (section) {
for (var name in section) {
fixupEntry(section[name]);
}
}
};
fixupSection("properties");
fixupSection("events");
fixupSection("members");
fixupSection("statics");
fixupEntry(metaData.clazz);
fixupEntry(metaData.construct);
fixupEntry(metaData.destruct);
fixupEntry(metaData.defer);
}
}
});