Skip to content

Instantly share code, notes, and snippets.

@hns
Created November 13, 2009 08:48
Show Gist options
  • Save hns/233686 to your computer and use it in GitHub Desktop.
Save hns/233686 to your computer and use it in GitHub Desktop.
const Token = org.mozilla.javascript.Token;
function parse(source, name) {
var exportedFunction;
var exportedName;
var exported = [];
var jsdocs = [];
var seen = {};
var checkAssignment = function(node, root, exported) {
if (node.type == Token.ASSIGN) {
if (node.left.type == Token.GETPROP) {
var target = node.left.target;
var name = node.left.property.string;
var propname = nodeToString(node.left);
if (propname.indexOf('exports.') == 0 && exported.indexOf(name) < 0) {
addDocItem(name, root.jsDoc);
exported.push(name);
if (node.right.type == Token.FUNCTION) {
exportedFunction = node.right;
exportedName = name;
}
return;
} else if (target.type == Token.THIS) {
if (root.parent && root.parent.parent && root.parent.parent.parent
&& root.parent.parent.parent == exportedFunction) {
addDocItem(exportedName + ".instance." + name, root.jsDoc);
/* if (node.right.type == Token.FUNCTION) {
exportedFunction = node.right;
exportedName = exportedName + ".prototype." + name;
} */
} else if (exported.indexOf(name) > -1) {
addDocItem(name, root.jsDoc);
}
}
} else if (node.left.type == Token.GETELEM
&& node.left.target.type == Token.NAME
&& node.left.target.string == "exports"
&& node.left.element.type == Token.STRING) {
// exports["foo"] = bar
addDocItem(node.left.element.value, root.jsDoc);
}
}
};
var addDocItem = function(name, jsdoc) {
if (!seen[name]) {
jsdocs.push({name: name, jsdoc: jsdoc ? extractTags(String(jsdoc)) : {}});
seen[name] = true;
}
};
parseScript(source, name, function(node) {
// loop through all comments looking for dangling jsdocs
if (node.type == Token.SCRIPT && node.comments) {
for each (var comment in node.comments.toArray()) {
if (comment.commentType == Token.CommentType.JSDOC) {
if (/@fileoverview\s/.test(comment.value)) {
Object.defineProperty(jsdocs, "fileoverview", {
value: extractTags(String(comment.value))
});
break;
}
// check for top level module doc
// log.info("found jsdoc comment: " + comment.value);
}
}
}
// export("foo") (helma-only)
if (node.type == Token.CALL && node.target.type == Token.NAME && node.target.string == "export") {
for each (var arg in node.arguments.toArray()) {
if (arg.type == Token.STRING) exported.push(arg.value);
}
}
// check for Object.defineProperty(foo, bar, {})
if (node.type == Token.CALL && node.target.type == Token.GETPROP) {
var getprop = node.target;
if (getprop.target.type == Token.NAME && getprop.target.string == "Object"
&& getprop.property.string == "defineProperty") {
var args = node.arguments ? node.arguments.toArray() : [];
var target = nodeToString(args[0]).split('.');
// rhino puts jsdoc on the first name of the third argument object literal (property descriptor)
var jsdoc = args[2] && args[2].elements && args[2].elements.get(0).left.jsDoc;
if (exported.indexOf(target[0]) > -1 || standardObjects.indexOf(target[0]) > -1) {
target.push(nodeToString(args[1]));
addDocItem(target.join('.'), jsdoc);
} else if (target[0] == 'this' && exportedFunction != null) {
target[0] = exportedName;
target.push('instance', nodeToString(args[1]));
addDocItem(target.join('.'), jsdoc);
}
}
}
// exported function
if (node.type == Token.FUNCTION && exported.indexOf(node.name) > -1) {
addDocItem(node.name, node.jsDoc);
exportedFunction = node;
exportedName = node.name;
}
// var foo = exports.foo = bar
if ((node.type == Token.VAR || node.type == Token.LET) && node.variables) {
for each (var n in node.variables.toArray()) {
if (n.target.type == Token.NAME && exported.indexOf(n.target.string) > -1) {
addDocItem(n.target.string, node.jsDoc);
} else if (n.initializer && n.initializer.type == Token.ASSIGN) {
checkAssignment(n.initializer, node, exported);
}
}
}
// exports.foo = bar
if (node.type == Token.ASSIGN) {
checkAssignment(node, node, exported);
}
return true;
});
return jsdocs;
}
/**
* Remove slash-star comment wrapper from a raw comment string.
* @type String
*/
function unwrapComment(/**String*/comment) {
return comment ? comment.replace(/(^\/\*\*|\*\/$)/g, "").replace(/^\s*\* ?/gm, "") : "";
}
/**
* Parse a JSDoc comment into an array of tags, with tags being represented
* as [tagname, tagtext].
* @param {String} comment the raw JSDoc comment
* @return {Array} an array of tags.
*/
function extractTags(/**String*/comment) {
if (comment.indexOf("/**") == 0) {
comment = unwrapComment(comment);
}
var tags = comment.split(/(^|[\r\n])\s*@/)
.filter(function($){return $.match(/\S/)});
return tags.map(function(tag, idx) {
if (idx == 0 && comment.indexOf('@') != 0) {
return ['desc', tag.trim()];
} else {
var space = tag.search(/\s/);
return space > -1 ?
[tag.substring(0, space), tag.substring(space + 1).trim()] :
[tag, ''];
}
})
}
/**
* Utility function to test whether a node is a Name node
* (a node of type org.mozilla.javascript.ast.Name)
* @param node {Object} an AST node
* @return {Boolean} true if node is a name node
*/
function isName(node) {
return node instanceof org.mozilla.javascript.ast.Name;
}
/**
* Utility function to get the name value of a node, or the empty
* string if it is not a name node.
* @param node an AST node
* @return {String} the name value of the node
*/
function getName(node) {
return isName(node) ? node.getString() : "";
}
function getTypeName(node) {
return node ? org.mozilla.javascript.Token.typeToName(node.getType()) : "" ;
}
function nodeToString(node) {
if (node.type == Token.GETPROP) {
return [nodeToString(node.target), node.property.string].join('.');
} else if (node.type == Token.NAME) {
return node.string;
} else if (node.type == Token.STRING) {
return node.value;
} else if (node.type == Token.THIS) {
return "this";
} else {
return getTypeName(node);
}
};
function parseScript(source, name, visitorFunction) {
var ast = getParser().parse(source, name, 0);
ast.visit(new org.mozilla.javascript.ast.NodeVisitor({
visit: visitorFunction
}));
}
function getParser() {
var ce = new org.mozilla.javascript.CompilerEnvirons();
ce.setRecordingComments(true);
ce.setRecordingLocalJsDocComments(true);
ce.initFromContext(org.mozilla.javascript.Context.getCurrentContext());
return new org.mozilla.javascript.Parser(ce, ce.getErrorReporter());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment