elastic.js
Version:
Javascript API for ElasticSearch DSL
495 lines (419 loc) • 16.9 kB
JavaScript
/**
* @module jsdoc/src/parser
* @requires common/util
* @requires common/fs
* @requires common/events
*/
(function() {
var Token = Packages.org.mozilla.javascript.Token,
currentParser = null,
currentSourceName = '';
/**
* @class
* @mixes module:common/events
*
* @example <caption>Create a new parser.</caption>
* var jsdocParser = new (require('jsdoc/src/parser').Parser)();
*/
exports.Parser = function() {
this._resultBuffer = [];
this.refs = {};
}
require('common/util').mixin(exports.Parser.prototype, require('common/events'));
/**
* Parse the given source files for JSDoc comments.
* @param {Array.<string>} sourceFiles An array of filepaths to the JavaScript sources.
* @param {string} [encoding=utf8]
*
* @fires jsdocCommentFound
* @fires symbolFound
* @fires newDoclet
* @fires fileBegin
* @fires fileComplete
*
* @example <caption>Parse two source files.</caption>
* var myFiles = ['file1.js', 'file2.js'];
* var docs = jsdocParser.parse(myFiles);
*/
exports.Parser.prototype.parse = function(sourceFiles, encoding) {
const SCHEMA = 'javascript:';
var sourceCode = '',
filename = '';
if (typeof sourceFiles === 'string') { sourceFiles = [sourceFiles]; }
for (i = 0, leni = sourceFiles.length; i < leni; i++) {
if (sourceFiles[i].indexOf(SCHEMA) === 0) {
sourceCode = sourceFiles[i].substr(SCHEMA.length);
filename = '[[string' + i + ']]';
}
else {
filename = sourceFiles[i];
try {
sourceCode = require('fs').readFileSync(filename, encoding);
}
catch(e) {
console.log('FILE READ ERROR: in module:jsdoc/parser.parseFiles: "' + filename + '" ' + e);
continue;
}
}
currentParser = this;
this._parseSourceCode(sourceCode, filename);
currentParser = null;
}
return this._resultBuffer;
}
/**
* @returns {Array<Doclet>} The accumulated results of any calls to parse.
*/
exports.Parser.prototype.results = function() {
return this._resultBuffer;
}
/**
* @param {Object} o The parse result to add to the result buffer.
*/
exports.Parser.prototype.addResult = function(o) {
this._resultBuffer.push(o);
}
/**
* Empty any accumulated results of calls to parse.
*/
exports.Parser.prototype.clear = function() {
currentParser = null;
currentSourceName = '';
this._resultBuffer = [];
}
/** @private */
exports.Parser.prototype._parseSourceCode = function(sourceCode, sourceName) {
currentSourceName = sourceName;
sourceCode = pretreat(sourceCode);
var ast = parserFactory().parse(sourceCode, sourceName, 1);
var e = {filename: currentSourceName};
this.fire('fileBegin', e);
if (!e.defaultPrevented) {
ast.visit(
new Packages.org.mozilla.javascript.ast.NodeVisitor({
visit: visitNode
})
);
}
this.fire('fileComplete', e);
currentSourceName = '';
}
function pretreat(code) {
return code
// merge adjacent doclets
.replace(/\*\/\/\*\*+/g, '@also')
// make lent objectliterals documentable by giving them a dummy name
.replace(/(\/\*\*[\s\S]*?@lends\b[\s\S]*?\*\/\s*)\{/g, '$1____ = {');
}
/**
* Given a node, determine what the node is a member of.
* @param {astnode} node
* @returns {string} The long name of the node that this is a member of.
*/
exports.Parser.prototype.astnodeToMemberof = function(node) {
var memberof = {};
if (node.type === Token.VAR || node.type === Token.FUNCTION) {
if (node.enclosingFunction) { // an inner var or func
memberof.id = 'astnode'+node.enclosingFunction.hashCode();
memberof.doclet = this.refs[memberof.id];
if (!memberof.doclet) {
return '<anonymous>~';
}
return (memberof.doclet.longname||memberof.doclet.name) + '~';
}
}
else {
memberof.id = 'astnode'+node.parent.hashCode();
memberof.doclet = this.refs[memberof.id];
if (!memberof.doclet) return ''; // global?
return memberof.doclet.longname||memberof.doclet.name;
}
}
/**
* Resolve what "this" refers too, relative to a node.
* @param {astnode} node - The "this" node
* @returns {string} The longname of the enclosing node.
*/
exports.Parser.prototype.resolveThis = function(node) {
var memberof = {};
if (node.enclosingFunction) {
memberof.id = 'astnode'+node.enclosingFunction.hashCode();
memberof.doclet = this.refs[memberof.id];
if (!memberof.doclet) {
return '<anonymous>'; // TODO handle global this?
}
if (memberof.doclet['this']) {
return memberof.doclet['this'];
}
// like: Foo.constructor = function(n) { /** blah */ this.name = n; }
else if (memberof.doclet.kind === 'function' && memberof.doclet.memberof) {
return memberof.doclet.memberof;
}
// walk up to the closest class we can find
else if (memberof.doclet.kind === 'class' || memberof.doclet.kind === 'module') {
return memberof.doclet.longname||memberof.doclet.name;
}
else {
if (node.enclosingFunction){
return this.resolveThis(node.enclosingFunction/*memberof.doclet.meta.code.val*/);
}
else return ''; // TODO handle global this?
}
}
else if (node.parent) {
var parent = node.parent;
if (parent.type === Token.COLON) parent = parent.parent; // go up one more
memberof.id = 'astnode'+parent.hashCode();
memberof.doclet = this.refs[memberof.id];
if (!memberof.doclet) return ''; // global?
return memberof.doclet.longname||memberof.doclet.name;
}
else {
return ''; // global?
}
}
/**
* Resolve what function a var is limited to.
* @param {astnode} node
* @param {string} basename The leftmost name in the long name: in foo.bar.zip the basename is foo.
*/
exports.Parser.prototype.resolveVar = function(node, basename) {
var doclet,
enclosingFunction = node.enclosingFunction;
if (!enclosingFunction) { return ''; } // global
doclet = this.refs['astnode'+enclosingFunction.hashCode()];
if ( doclet && doclet.meta.vars && ~doclet.meta.vars.indexOf(basename) ) {
return doclet.longname;
}
return this.resolveVar(enclosingFunction, basename);
}
/** @private */
function visitNode(node) {
var e,
commentSrc;
// look for stand-alone doc comments
if (node.type === Token.SCRIPT && node.comments) {
// note: ALL comments are seen in this block...
for each(var comment in node.comments.toArray()) {
if (comment.commentType !== Token.CommentType.JSDOC) {
continue;
}
if (commentSrc = ''+comment.toSource()) {
e = {
comment: commentSrc,
lineno: comment.getLineno(),
filename: currentSourceName
};
if ( isValidJsdoc(commentSrc) ) {
currentParser.fire('jsdocCommentFound', e, currentParser);
}
}
}
}
else if (node.type === Token.ASSIGN) {
e = {
id: 'astnode'+node.hashCode(), // the id of the ASSIGN node
comment: String(node.jsDoc||'@undocumented'),
lineno: node.getLineno(),
filename: currentSourceName,
astnode: node,
code: aboutNode(node)
};
var basename = e.code.name.replace(/^([$a-z_][$a-z_0-9]*).*?$/i, '$1');
if (basename !== 'this') e.code.funcscope = currentParser.resolveVar(node, basename);
if ( isValidJsdoc(e.comment) ) {
currentParser.fire('symbolFound', e, currentParser);
}
if (e.doclet) {
currentParser.refs['astnode'+e.code.node.hashCode()] = e.doclet; // allow lookup from value => doclet
}
}
else if (node.type === Token.COLON) { // assignment within an object literal
e = {
id: 'astnode'+node.hashCode(), // the id of the COLON node
comment: String(node.left.jsDoc||'@undocumented'),
lineno: node.getLineno(),
filename: currentSourceName,
astnode: node,
code: aboutNode(node)
};
if ( isValidJsdoc(e.comment) ) {
currentParser.fire('symbolFound', e, currentParser);
}
if (e.doclet) {
currentParser.refs['astnode'+e.code.node.hashCode()] = e.doclet; // allow lookup from value => doclet
}
}
else if (node.type == Token.VAR || node.type == Token.LET || node.type == Token.CONST) {
if (node.variables) {
return true; // we'll get each var separately on future visits
}
if (node.parent.variables.toArray()[0] === node) { // like /** blah */ var a=1, b=2, c=3;
// the first var assignment gets any jsDoc before the whole var series
node.jsDoc = node.parent.jsDoc;
}
e = {
id: 'astnode'+node.hashCode(), // the id of the VARIABLE node
comment: String(node.jsDoc||'@undocumented'),
lineno: node.getLineno(),
filename: currentSourceName,
astnode: node,
code: aboutNode(node)
};
// keep track of vars in a function scope
if (node.enclosingFunction) {
var func = 'astnode'+node.enclosingFunction.hashCode(),
funcDoc = currentParser.refs[func];
if (funcDoc) {
funcDoc.meta.vars = funcDoc.meta.vars || [];
funcDoc.meta.vars.push(e.code.name);
}
}
if ( isValidJsdoc(e.comment) ) {
currentParser.fire('symbolFound', e, currentParser);
}
if (e.doclet) {
currentParser.refs['astnode'+e.code.node.hashCode()] = e.doclet; // allow lookup from value => doclet
}
}
else if (node.type == Token.FUNCTION) {
e = {
id: 'astnode'+node.hashCode(), // the id of the COLON node
comment: String(node.jsDoc||'@undocumented'),
lineno: node.getLineno(),
filename: currentSourceName,
astnode: node,
code: aboutNode(node)
};
e.code.name = String(node.name) || '';
if ( isValidJsdoc(e.comment) ) {
currentParser.fire('symbolFound', e, currentParser);
}
if (e.doclet) {
currentParser.refs['astnode'+e.code.node.hashCode()] = e.doclet; // allow lookup from value => doclet
}
else if (!currentParser.refs['astnode'+e.code.node.hashCode()]) { // keep references to undocumented anonymous functions too as they might have scoped vars
currentParser.refs['astnode'+e.code.node.hashCode()] = {
longname: '<anonymous>',
meta: { code: e.code }
};
}
}
return true;
}
/** @private */
function parserFactory() {
var cx = Packages.org.mozilla.javascript.Context.getCurrentContext();
var ce = new Packages.org.mozilla.javascript.CompilerEnvirons();
ce.setRecordingComments(true);
ce.setRecordingLocalJsDocComments(true);
ce.setLanguageVersion(180);
ce.initFromContext(cx);
return new Packages.org.mozilla.javascript.Parser(ce, ce.getErrorReporter());
}
/**
* Attempts to find the name and type of the given node.
* @private
* @memberof module:src/parser.Parser
*/
function aboutNode(node) {
about = {};
if (node.type == Token.FUNCTION) {
about.name = '' + node.name;
about.type = 'function';
about.node = node;
return about;
}
if (node.type == Token.VAR || node.type == Token.LET || node.type == Token.CONST) {
about.name = nodeToString(node.target);
if (node.initializer) { // like var i = 0;
about.node = node.initializer;
about.value = nodeToString(about.node);
about.type = getTypeName(node.initializer);
}
else { // like var i;
about.node = node.target;
about.value = nodeToString(about.node);
about.type = 'undefined';
}
return about;
}
if (node.type === Token.ASSIGN || node.type === Token.COLON) {
about.name = nodeToString(node.left);
if (node.type === Token.COLON) {
// objlit keys with unsafe variable-name characters must be quoted
if (!/^[$_a-z][$_a-z0-9]*$/i.test(about.name) ) {
about.name = '"'+about.name.replace(/"/g, '\\"')+'"';
}
}
about.node = node.right;
about.value = nodeToString(about.node);
about.type = getTypeName(node.right);
return about;
}
// type 39 (NAME)
var string = nodeToString(node);
if (string) {
about.name = string;
return about;
}
return about;
}
/** @private
@memberof module:src/parser.Parser
*/
function nodeToString(node) {
var str;
if (!node) return;
if (node.type === Token.GETPROP) {
str = [nodeToString(node.target), node.property.string].join('.');
}
else if (node.type === Token.VAR) {
str = nodeToString(node.target)
}
else if (node.type === Token.NAME) {
str = node.string;
}
else if (node.type === Token.STRING) {
str = node.value;
}
else if (node.type === Token.NUMBER) {
str = node.value;
}
else if (node.type === Token.THIS) {
str = 'this';
}
else if (node.type === Token.GETELEM) {
str = node.toSource(); // like: Foo['Bar']
}
else {
str = getTypeName(node);
}
return '' + str;
};
/** @private
@memberof module:src/parser.Parser
*/
function getTypeName(node) {
var type = '';
if (node) {
type = ''+ Packages.org.mozilla.javascript.Token.typeToName(node.getType());
}
return type;
}
/** @private
@memberof module:src/parser.Parser
*/
function isValidJsdoc(commentSrc) {
return commentSrc.indexOf('/***') !== 0; /*** ignore comments that start with many stars ***/
}
})();
/**
Fired whenever the parser encounters a JSDoc comment in the current source code.
@event jsdocCommentFound
@memberof module:jsdoc/src/parser.Parser
@param {event} e
@param {string} e.comment The text content of the JSDoc comment
@param {number} e.lineno The line number associated with the found comment.
@param {string} e.filename The file name associated with the found comment.
*/