501 lines
14 KiB
JavaScript
501 lines
14 KiB
JavaScript
/*global env, Packages */
|
|
/*eslint no-script-url:0 */
|
|
/**
|
|
* @module jsdoc/src/parser
|
|
*/
|
|
'use strict';
|
|
|
|
var jsdoc = {
|
|
doclet: require('jsdoc/doclet'),
|
|
name: require('jsdoc/name'),
|
|
src: {
|
|
astnode: require('jsdoc/src/astnode'),
|
|
syntax: require('jsdoc/src/syntax')
|
|
}
|
|
};
|
|
var logger = require('jsdoc/util/logger');
|
|
var util = require('util');
|
|
|
|
var hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var Syntax = jsdoc.src.syntax.Syntax;
|
|
|
|
// Prefix for JavaScript strings that were provided in lieu of a filename.
|
|
var SCHEMA = 'javascript:';
|
|
// TODO: docs
|
|
var PARSERS = exports.PARSERS = {
|
|
esprima: 'jsdoc/src/parser',
|
|
rhino: 'rhino/jsdoc/src/parser'
|
|
};
|
|
|
|
// TODO: docs
|
|
// TODO: not currently used
|
|
function makeGlobalDoclet(globalScope) {
|
|
var doclet = new jsdoc.doclet.Doclet('/** Auto-generated doclet for global scope */', {});
|
|
|
|
if (globalScope) {
|
|
// TODO: handle global aliases
|
|
Object.keys(globalScope.ownedVariables).forEach(function(variable) {
|
|
doclet.meta.vars = doclet.meta.vars || {};
|
|
doclet.meta.vars[variable] = null;
|
|
});
|
|
}
|
|
|
|
return doclet;
|
|
}
|
|
|
|
// TODO: docs
|
|
exports.createParser = function(type) {
|
|
var path = require('jsdoc/path');
|
|
var runtime = require('jsdoc/util/runtime');
|
|
|
|
var modulePath;
|
|
|
|
if (!type) {
|
|
type = runtime.isRhino() ? 'rhino' : 'esprima';
|
|
}
|
|
|
|
if (PARSERS[type]) {
|
|
modulePath = PARSERS[type];
|
|
}
|
|
else {
|
|
modulePath = path.join( path.getResourcePath(path.dirname(type)), path.basename(type) );
|
|
}
|
|
|
|
try {
|
|
return new ( require(modulePath) ).Parser();
|
|
}
|
|
catch (e) {
|
|
logger.fatal('Unable to create the parser type "' + type + '": ' + e);
|
|
}
|
|
};
|
|
|
|
// TODO: docs
|
|
/**
|
|
* @class
|
|
* @mixes module:events.EventEmitter
|
|
*
|
|
* @example <caption>Create a new parser.</caption>
|
|
* var jsdocParser = new (require('jsdoc/src/parser').Parser)();
|
|
*/
|
|
var Parser = exports.Parser = function(builderInstance, visitorInstance, walkerInstance) {
|
|
this.clear();
|
|
|
|
this._astBuilder = builderInstance || new (require('jsdoc/src/astbuilder')).AstBuilder();
|
|
this._visitor = visitorInstance || new (require('jsdoc/src/visitor')).Visitor(this);
|
|
this._walker = walkerInstance || new (require('jsdoc/src/walker')).Walker();
|
|
|
|
Object.defineProperties(this, {
|
|
astBuilder: {
|
|
get: function() {
|
|
return this._astBuilder;
|
|
}
|
|
},
|
|
visitor: {
|
|
get: function() {
|
|
return this._visitor;
|
|
}
|
|
},
|
|
walker: {
|
|
get: function() {
|
|
return this._walker;
|
|
}
|
|
}
|
|
});
|
|
};
|
|
util.inherits(Parser, require('events').EventEmitter);
|
|
|
|
// TODO: docs
|
|
Parser.prototype.clear = function() {
|
|
this._resultBuffer = [];
|
|
this.refs = {};
|
|
this.refs[jsdoc.src.astnode.GLOBAL_NODE_ID] = {};
|
|
this.refs[jsdoc.src.astnode.GLOBAL_NODE_ID].meta = {};
|
|
};
|
|
|
|
// TODO: update docs
|
|
/**
|
|
* 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 module:jsdoc/src/parser.Parser.parseBegin
|
|
* @fires module:jsdoc/src/parser.Parser.fileBegin
|
|
* @fires module:jsdoc/src/parser.Parser.jsdocCommentFound
|
|
* @fires module:jsdoc/src/parser.Parser.symbolFound
|
|
* @fires module:jsdoc/src/parser.Parser.newDoclet
|
|
* @fires module:jsdoc/src/parser.Parser.fileComplete
|
|
* @fires module:jsdoc/src/parser.Parser.parseComplete
|
|
*
|
|
* @example <caption>Parse two source files.</caption>
|
|
* var myFiles = ['file1.js', 'file2.js'];
|
|
* var docs = jsdocParser.parse(myFiles);
|
|
*/
|
|
Parser.prototype.parse = function(sourceFiles, encoding) {
|
|
encoding = encoding || env.conf.encoding || 'utf8';
|
|
|
|
var filename = '';
|
|
var sourceCode = '';
|
|
var parsedFiles = [];
|
|
var e = {};
|
|
|
|
if (typeof sourceFiles === 'string') {
|
|
sourceFiles = [sourceFiles];
|
|
}
|
|
|
|
e.sourcefiles = sourceFiles;
|
|
logger.debug('Parsing source files: %j', sourceFiles);
|
|
|
|
this.emit('parseBegin', e);
|
|
|
|
for (var i = 0, l = sourceFiles.length; i < l; i++) {
|
|
sourceCode = '';
|
|
|
|
if (sourceFiles[i].indexOf(SCHEMA) === 0) {
|
|
sourceCode = sourceFiles[i].substr(SCHEMA.length);
|
|
filename = '[[string' + i + ']]';
|
|
}
|
|
else {
|
|
filename = sourceFiles[i];
|
|
try {
|
|
sourceCode = require('jsdoc/fs').readFileSync(filename, encoding);
|
|
}
|
|
catch(e) {
|
|
logger.error('Unable to read and parse the source file %s: %s', filename, e);
|
|
}
|
|
}
|
|
|
|
if (sourceCode.length) {
|
|
this._parseSourceCode(sourceCode, filename);
|
|
parsedFiles.push(filename);
|
|
}
|
|
}
|
|
|
|
this.emit('parseComplete', {
|
|
sourcefiles: parsedFiles,
|
|
doclets: this._resultBuffer
|
|
});
|
|
logger.debug('Finished parsing source files.');
|
|
|
|
return this._resultBuffer;
|
|
};
|
|
|
|
// TODO: docs
|
|
Parser.prototype.fireProcessingComplete = function(doclets) {
|
|
this.emit('processingComplete', { doclets: doclets });
|
|
};
|
|
|
|
// TODO: docs
|
|
Parser.prototype.results = function() {
|
|
return this._resultBuffer;
|
|
};
|
|
|
|
// TODO: update docs
|
|
/**
|
|
* @param {Object} o The parse result to add to the result buffer.
|
|
*/
|
|
Parser.prototype.addResult = function(o) {
|
|
this._resultBuffer.push(o);
|
|
};
|
|
|
|
// TODO: docs
|
|
Parser.prototype.addAstNodeVisitor = function(visitor) {
|
|
this._visitor.addAstNodeVisitor(visitor);
|
|
};
|
|
|
|
// TODO: docs
|
|
Parser.prototype.getAstNodeVisitors = function() {
|
|
return this._visitor.getAstNodeVisitors();
|
|
};
|
|
|
|
// TODO: docs
|
|
function pretreat(code) {
|
|
return code
|
|
// comment out hashbang at the top of the file, like: #!/usr/bin/env node
|
|
.replace(/^(\#\![\S \t]+\r?\n)/, '// $1')
|
|
|
|
// to support code minifiers that preserve /*! comments, treat /*!* as equivalent to /**
|
|
.replace(/\/\*\!\*/g, '/**')
|
|
// merge adjacent doclets
|
|
.replace(/\*\/\/\*\*+/g, '@also');
|
|
}
|
|
|
|
/** @private */
|
|
Parser.prototype._parseSourceCode = function(sourceCode, sourceName) {
|
|
var ast;
|
|
var globalScope;
|
|
|
|
var e = {
|
|
filename: sourceName
|
|
};
|
|
|
|
this.emit('fileBegin', e);
|
|
logger.printInfo('Parsing %s ...', sourceName);
|
|
|
|
if (!e.defaultPrevented) {
|
|
e = {
|
|
filename: sourceName,
|
|
source: sourceCode
|
|
};
|
|
this.emit('beforeParse', e);
|
|
sourceCode = e.source;
|
|
sourceName = e.filename;
|
|
|
|
sourceCode = pretreat(e.source);
|
|
|
|
ast = this._astBuilder.build(sourceCode, sourceName);
|
|
if (ast) {
|
|
this._walker.recurse(sourceName, ast, this._visitor);
|
|
}
|
|
|
|
}
|
|
|
|
this.emit('fileComplete', e);
|
|
logger.info('complete.');
|
|
};
|
|
|
|
// TODO: docs
|
|
Parser.prototype.addDocletRef = function(e) {
|
|
var node;
|
|
|
|
if (e && e.code && e.code.node) {
|
|
node = e.code.node;
|
|
// allow lookup from value => doclet
|
|
if (e.doclet) {
|
|
this.refs[node.nodeId] = e.doclet;
|
|
}
|
|
// keep references to undocumented anonymous functions, too, as they might have scoped vars
|
|
else if (
|
|
(node.type === Syntax.FunctionDeclaration || node.type === Syntax.FunctionExpression) &&
|
|
!this.refs[node.nodeId] ) {
|
|
this.refs[node.nodeId] = {
|
|
longname: jsdoc.name.ANONYMOUS_LONGNAME,
|
|
meta: {
|
|
code: e.code
|
|
}
|
|
};
|
|
}
|
|
}
|
|
};
|
|
|
|
// TODO: docs
|
|
Parser.prototype._getDoclet = function(id) {
|
|
if ( hasOwnProp.call(this.refs, id) ) {
|
|
return this.refs[id];
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
// TODO: docs
|
|
/**
|
|
* @param {string} name - The symbol's longname.
|
|
* @return {string} The symbol's basename.
|
|
*/
|
|
Parser.prototype.getBasename = function(name) {
|
|
if (name !== undefined) {
|
|
return name.replace(/^([$a-z_][$a-z_0-9]*).*?$/i, '$1');
|
|
}
|
|
};
|
|
|
|
// TODO: docs
|
|
function definedInScope(doclet, basename) {
|
|
return !!doclet && !!doclet.meta && !!doclet.meta.vars && !!basename &&
|
|
hasOwnProp.call(doclet.meta.vars, basename);
|
|
}
|
|
|
|
// TODO: docs
|
|
/**
|
|
* Given a node, determine what the node is a member of.
|
|
* @param {node} node
|
|
* @returns {string} The long name of the node that this is a member of.
|
|
*/
|
|
Parser.prototype.astnodeToMemberof = function(node) {
|
|
var basename;
|
|
var doclet;
|
|
var scope;
|
|
|
|
var result = '';
|
|
var type = node.type;
|
|
|
|
if ( (type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression ||
|
|
type === Syntax.VariableDeclarator) && node.enclosingScope ) {
|
|
doclet = this._getDoclet(node.enclosingScope.nodeId);
|
|
|
|
if (!doclet) {
|
|
result = jsdoc.name.ANONYMOUS_LONGNAME + jsdoc.name.INNER;
|
|
}
|
|
else {
|
|
result = (doclet.longname || doclet.name) + jsdoc.name.INNER;
|
|
}
|
|
}
|
|
else {
|
|
// check local references for aliases
|
|
scope = node;
|
|
basename = this.getBasename( jsdoc.src.astnode.nodeToString(node) );
|
|
|
|
// walk up the scope chain until we find the scope in which the node is defined
|
|
while (scope.enclosingScope) {
|
|
doclet = this._getDoclet(scope.enclosingScope.nodeId);
|
|
if ( doclet && definedInScope(doclet, basename) ) {
|
|
result = [doclet.meta.vars[basename], basename];
|
|
break;
|
|
}
|
|
else {
|
|
// move up
|
|
scope = scope.enclosingScope;
|
|
}
|
|
}
|
|
|
|
// do we know that it's a global?
|
|
doclet = this.refs[jsdoc.src.astnode.GLOBAL_NODE_ID];
|
|
if ( doclet && definedInScope(doclet, basename) ) {
|
|
result = [doclet.meta.vars[basename], basename];
|
|
}
|
|
|
|
// have we seen the node's parent? if so, use that
|
|
else if (node.parent) {
|
|
doclet = this._getDoclet(node.parent.nodeId);
|
|
|
|
// set the result if we found a doclet. (if we didn't, the AST node may describe a
|
|
// global symbol.)
|
|
if (doclet) {
|
|
result = doclet.longname || doclet.name;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
// TODO: docs
|
|
/**
|
|
* Resolve what "this" refers to relative to a node.
|
|
* @param {node} node - The "this" node
|
|
* @returns {string} The longname of the enclosing node.
|
|
*/
|
|
Parser.prototype.resolveThis = function(node) {
|
|
var doclet;
|
|
var result;
|
|
|
|
// In general, if there's an enclosing scope, we use the enclosing scope to resolve `this`.
|
|
// For object properties, we use the node's parent (the object) instead. This is a consequence
|
|
// of the source-rewriting hackery that we use to support the `@lends` tag.
|
|
if (node.type !== Syntax.Property && node.enclosingScope) {
|
|
doclet = this._getDoclet(node.enclosingScope.nodeId);
|
|
|
|
if (!doclet) {
|
|
result = jsdoc.name.ANONYMOUS_LONGNAME; // TODO handle global this?
|
|
}
|
|
else if (doclet['this']) {
|
|
result = doclet['this'];
|
|
}
|
|
// like: Foo.constructor = function(n) { /** blah */ this.name = n; }
|
|
else if (doclet.kind === 'function' && doclet.memberof) {
|
|
result = doclet.memberof;
|
|
}
|
|
// like: var foo = function(n) { /** blah */ this.bar = n; }
|
|
else if ( doclet.kind === 'member' && jsdoc.src.astnode.isAssignment(node) ) {
|
|
result = doclet.longname || doclet.name;
|
|
}
|
|
// walk up to the closest class we can find
|
|
else if (doclet.kind === 'class' || doclet.kind === 'module') {
|
|
result = doclet.longname || doclet.name;
|
|
}
|
|
else if (node.enclosingScope) {
|
|
result = this.resolveThis(node.enclosingScope);
|
|
}
|
|
}
|
|
else if (node.parent) {
|
|
doclet = this.refs[node.parent.nodeId];
|
|
|
|
// TODO: is this behavior correct? when do we get here?
|
|
if (!doclet) {
|
|
result = ''; // global?
|
|
}
|
|
else {
|
|
result = doclet.longname || doclet.name;
|
|
}
|
|
}
|
|
// TODO: is this behavior correct? when do we get here?
|
|
else {
|
|
result = ''; // global?
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
// TODO: docs
|
|
/**
|
|
* Given 'var foo = { x: 1 }', find foo from x.
|
|
*/
|
|
Parser.prototype.resolvePropertyParent = function(node) {
|
|
var doclet;
|
|
|
|
if (node.parent) {
|
|
doclet = this._getDoclet(node.parent.nodeId);
|
|
}
|
|
|
|
return doclet;
|
|
};
|
|
|
|
// TODO docs
|
|
/**
|
|
* 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.
|
|
*/
|
|
Parser.prototype.resolveVar = function(node, basename) {
|
|
var doclet;
|
|
var result;
|
|
|
|
var scope = node.enclosingScope;
|
|
|
|
if (!scope) {
|
|
result = ''; // global
|
|
}
|
|
else {
|
|
doclet = this._getDoclet(scope.nodeId);
|
|
if ( definedInScope(doclet, basename) ) {
|
|
result = doclet.longname;
|
|
}
|
|
else {
|
|
result = this.resolveVar(scope, basename);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
// TODO: docs
|
|
Parser.prototype.resolveEnum = function(e) {
|
|
var doclet = this.resolvePropertyParent(e.code.node.parent);
|
|
|
|
if (doclet && doclet.isEnum) {
|
|
if (!doclet.properties) {
|
|
doclet.properties = [];
|
|
}
|
|
|
|
// members of an enum inherit the enum's type
|
|
if (doclet.type && !e.doclet.type) {
|
|
e.doclet.type = doclet.type;
|
|
}
|
|
|
|
delete e.doclet.undocumented;
|
|
e.doclet.defaultvalue = e.doclet.meta.code.value;
|
|
|
|
// add a copy of the doclet to the parent's properties
|
|
doclet.properties.push( require('jsdoc/util/doop').doop(e.doclet) );
|
|
}
|
|
};
|
|
|
|
// TODO: document other events
|
|
/**
|
|
* Fired once for each 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.
|
|
*/
|