/**
* @fileOverview
* @name JsParse
* @author Michael Mathews micmath@gmail.com
* @url $HeadURL: https://jsdoc-toolkit.googlecode.com/svn/tags/jsdoc_toolkit-1.4.0b/app/JsParse.js $
* @revision $Id: JsParse.js 329 2007-11-13 00:48:15Z micmath $
* @license X11/MIT License
* (See the accompanying README file for full details.)
*/
/**
* @class Find objects in tokenized JavaScript source code.
* @constructor
* @author Michael Mathews micmath@gmail.com
*/
function JsParse() {};
/**
* Populate the symbols array with symbols found in the
* given token stream.
* @param {TokenStream} tokenStream
*/
JsParse.prototype.parse = function(tokenStream) {
/**
* All symbols found in the tokenStream.
* @type Symbol[]
*/
this.symbols = [];
/**
* Overview found in the tokenStream.
* @type Symbol
*/
this.overview = null;
while(tokenStream.next()) {
if (this._findDocComment(tokenStream)) continue;
if (this._findFunction(tokenStream)) continue;
if (this._findVariable(tokenStream)) continue;
}
}
/**
* Try to find a JsDoc comment in the tokenStream.
* @param {TokenStream} ts
* @return {boolean} Was a JsDoc comment found?
*/
JsParse.prototype._findDocComment = function(ts) {
if (ts.look().is("JSDOC")) {
ts.look().data = ts.look().data.replace(/@namespace\b/, "@static\n@class");
var doc = ts.look().data;
if (doc.indexOf("/**#") == 0) {
new Symbol("", [], "META", doc);
delete ts.tokens[ts.cursor];
return true;
}
else if (/@(projectdescription|(file)?overview)\b/i.test(doc)) {
this.overview = new Symbol("", [], "FILE", doc);
delete ts.tokens[ts.cursor];
return true;
}
else if (/@name\s+([a-z0-9_$.]+)\s*/i.test(doc)) {
this.symbols.push(new Symbol(RegExp.$1, [], SYM.VIRTUAL, doc));
delete ts.tokens[ts.cursor];
return true;
}
else if (/@scope\s+([a-z0-9_$.]+)\s*/i.test(doc)) {
var scope = RegExp.$1;
if (scope) {
scope = scope.replace(/\.prototype\b/, "/");
this._onObLiteral(scope, new TokenStream(ts.balance("LEFT_CURLY")));
return true;
}
}
}
return false;
}
/**
* Try to find a function definition in the tokenStream
* @param {TokenStream} ts
* @return {boolean} Was a function definition found?
*/
JsParse.prototype._findFunction = function(ts) {
if (ts.look().is("NAME")) {
var name = ts.look().data;
var doc = "";
var isa = null;
var body = "";
var paramTokens = [];
var params = [];
// like function foo()
if (ts.look(-1).is("FUNCTION")) {
isa = SYM.FUNCTION;
if (ts.look(-2).is("JSDOC")) {
doc = ts.look(-2).data;
}
paramTokens = ts.balance("LEFT_PAREN");
body = ts.balance("LEFT_CURLY");
}
// like var foo = function()
else if (ts.look(1).is("ASSIGN") && ts.look(2).is("FUNCTION")) {
isa = SYM.FUNCTION;
if (ts.look(-1).is("VAR") && ts.look(-2).is("JSDOC")) {
doc = ts.look(-2).data;
}
else if (ts.look(-1).is("JSDOC")) {
doc = ts.look(-1).data;
}
paramTokens = ts.balance("LEFT_PAREN");
body = ts.balance("LEFT_CURLY");
// like foo = function(n) {return n}(42)
if (ts.look(1).is("LEFT_PAREN")) {
isa = SYM.OBJECT;
ts.balance("LEFT_PAREN");
if (doc) { // we only keep these if they're documented
name = name.replace(/\.prototype\.?/, "/");
if (!/\/$/.test(name)) { // assigning to prototype of already existing symbol
this.symbols.push(new Symbol(name, [], isa, doc));
}
}
this._onFnBody(name, new TokenStream(body));
return true;
}
}
// like var foo = new function()
else if (ts.look(1).is("ASSIGN") && ts.look(2).is("NEW") && ts.look(3).is("FUNCTION")) {
isa = SYM.OBJECT;
if (ts.look(-1).is("VAR") && ts.look(-2).is("JSDOC")) {
doc = ts.look(-2).data;
}
else if (ts.look(-1).is("JSDOC")) {
doc = ts.look(-1).data;
}
paramTokens = ts.balance("LEFT_PAREN");
body = ts.balance("LEFT_CURLY");
if (doc) { // we only keep these if they're documented
name = name.replace(/\.prototype\.?/, "/");
if (!/\/$/.test(name)) { // assigning to prototype of already existing symbol
this.symbols.push(new Symbol(name, [], isa, doc));
}
}
this._onFnBody(name, new TokenStream(body));
return true;
}
if (isa && name) {
if (isa == SYM.FUNCTION) {
for (var i = 0; i < paramTokens.length; i++) {
if (paramTokens[i].is("NAME"))
params.push(paramTokens[i].data);
}
}
// like Foo.bar.prototype.baz = function() {}
var ns = name;
if (name.indexOf(".prototype") > 0) {
isa = SYM.FUNCTION;
name = name.replace(/\.prototype\.?/, "/");
}
this.symbols.push(new Symbol(name, params, isa, doc));
if (body) {
if (ns.indexOf(".prototype") > 0) {
if (/@constructor\b/.test(doc)) {
ns = ns.replace(/\.prototype\.?/, "/");
}
else {
ns = ns.replace(/\.prototype\.[^.]+$/, "/");
}
}
this._onFnBody(ns, new TokenStream(body));
}
return true;
}
}
return false;
}
/**
* Try to find a variable definition in the tokenStream
* @param {TokenStream} ts
* @return {boolean} Was a variable definition found?
*/
JsParse.prototype._findVariable = function(ts) {
if (ts.look().is("NAME") && ts.look(1).is("ASSIGN")) {
// like var foo = 1
var name = ts.look().data;
isa = SYM.OBJECT;
var doc;
if (ts.look(-1).is("JSDOC")) doc = ts.look(-1).data;
else if (ts.look(-1).is("VAR") && ts.look(-2).is("JSDOC")) doc = ts.look(-2).data;
name = name.replace(/\.prototype\.?/, "/");
if (doc) { // we only keep these if they're documented
if (!/\/$/.test(name)) { // assigning to prototype of already existing symbol
this.symbols.push(new Symbol(name, [], isa, doc));
}
if (/@class\b/i.test(doc)) {
name = name +"/";
}
}
// like foo = {
if (ts.look(2).is("LEFT_CURLY")) {
this._onObLiteral(name, new TokenStream(ts.balance("LEFT_CURLY")));
}
return true;
}
return false;
}
/**
* Handle sub-parsing of the content within an object literal.
* @private
* @param {String} nspace The name attached to this object.
* @param {TokenStream} ts The content of the object literal.
*/
JsParse.prototype._onObLiteral = function(nspace, ts) {
while (ts.next()) {
if (this._findDocComment(ts)) {
}
else if (ts.look().is("NAME") && ts.look(1).is("COLON")) {
var name = nspace+((nspace.charAt(nspace.length-1)=="/")?"":".")+ts.look().data;
// like foo: function
if (ts.look(2).is("FUNCTION")) {
var isa = SYM.FUNCTION;
var doc = "";
if (ts.look(-1).is("JSDOC")) doc = ts.look(-1).data;
var paramTokens = ts.balance("LEFT_PAREN");
var params = [];
for (var i = 0; i < paramTokens.length; i++) {
if (paramTokens[i].is("NAME"))
params.push(paramTokens[i].data);
}
var body = ts.balance("LEFT_CURLY");
// like foo: function(n) {return n}(42)
if (ts.look(1).is("LEFT_PAREN")) {
isa = SYM.OBJECT;
ts.balance("LEFT_PAREN");
//if (doc) { // we only keep these if they're documented
//name = name.replace(/\.prototype\.?/, "/");
//if (!/\/$/.test(name)) { // assigning to prototype of already existing symbol
// this.symbols.push(new Symbol(name, [], isa, doc));
//}
//}
//this._onFnBody(name, new TokenStream(body));
//return true;
}
this.symbols.push(new Symbol(name, params, isa, doc));
// find methods in the body of this function
this._onFnBody(name, new TokenStream(body));
}
// like foo: {...}
else if (ts.look(2).is("LEFT_CURLY")) { // another nested object literal
if (ts.look(-1).is("JSDOC")) {
var isa = SYM.OBJECT;
var doc = ts.look(-1).data;
this.symbols.push(new Symbol(name, [], isa, doc));
}
this._onObLiteral(name, new TokenStream(ts.balance("LEFT_CURLY"))); // recursive
}
else { // like foo: 1, or foo: "one"
if (ts.look(-1).is("JSDOC")) { // we only grab these if they are documented
var isa = SYM.OBJECT;
var doc = ts.look(-1).data;
this.symbols.push(new Symbol(name, [], isa, doc));
}
while (!ts.look().is("COMMA")) { // skip to end of RH value ignoring things like bar({blah, blah})
if (ts.look().is("LEFT_PAREN")) ts.balance("LEFT_PAREN");
else if (ts.look().is("LEFT_CURLY")) ts.balance("LEFT_CURLY");
else if (!ts.next()) break;
}
}
}
}
}
/**
* Handle sub-parsing of the content within a function body.
* @private
* @param {String} nspace The name attached to this function.
* @param {TokenStream} fs The content of the function body.
*/
JsParse.prototype._onFnBody = function(nspace, fs) {
while (fs.look()) {
if (this._findDocComment(fs)) {
}
else if (fs.look().is("NAME") && fs.look(1).is("ASSIGN")) {
var name = fs.look().data;
// like this.foo =
if (name.indexOf("this.") == 0) {
// like this.foo = function
if (fs.look(2).is("FUNCTION")) {
var isa = SYM.FUNCTION;
var doc = (fs.look(-1).is("JSDOC"))? fs.look(-1).data : "";
name = name.replace(/^this\./, (nspace+"/").replace("//", "/"))
var paramTokens = fs.balance("LEFT_PAREN");
var params = [];
for (var i = 0; i < paramTokens.length; i++) {
if (paramTokens[i].is("NAME")) params.push(paramTokens[i].data);
}
body = fs.balance("LEFT_CURLY");
// like this.foo = function(n) {return n}(42)
if (fs.look(1).is("LEFT_PAREN")) { // false alarm, it's not really a named function definition
isa = SYM.OBJECT;
fs.balance("LEFT_PAREN");
if (doc) { // we only grab these if they are documented
this.symbols.push(
new Symbol(name, [], isa, doc)
);
}
break;
}
this.symbols.push(
new Symbol(name, params, isa, doc)
);
if (body) {
this._onFnBody(name, new TokenStream(body)); // recursive
}
}
else {
var isa = SYM.OBJECT;
var doc = (fs.look(-1).is("JSDOC"))? fs.look(-1).data : "";
name = name.replace(/^this\./, (nspace+"/").replace("//", "/"))
if (doc) {
this.symbols.push(
new Symbol(name, [], isa, doc)
);
}
// like this.foo = { ... }
if (fs.look(2).is("LEFT_CURLY")) {
var literal = fs.balance("LEFT_CURLY");
this._onObLiteral(name, new TokenStream(literal));
}
}
}
// like .prototype.foo =
else if (name.indexOf(nspace+".prototype.") == 0) {
this._findFunction(fs);
}
}
if (!fs.next()) break;
}
}