thirdparty/jsdoctoolkit/app/lib/JSDOC/Walker.js
author Mario Ferraro <fadinlight@gmail.com>
Sun, 25 Oct 2009 19:15:44 +0000
changeset 3041 c8f47f0b6697
permissions -rw-r--r--
JSDocToolkit 2.3.0 added to create documentation for JS files.

if (typeof JSDOC == "undefined") JSDOC = {};

/** @constructor */
JSDOC.Walker = function(/**JSDOC.TokenStream*/ts) {
	this.init();
	if (typeof ts != "undefined") {
		this.walk(ts);
	}
}

JSDOC.Walker.prototype.init = function() {
	this.ts = null;

	var globalSymbol = new JSDOC.Symbol("_global_", [], "GLOBAL", new JSDOC.DocComment(""));
	globalSymbol.isNamespace = true;
	globalSymbol.srcFile = "";
	globalSymbol.isPrivate = false;
	JSDOC.Parser.addSymbol(globalSymbol);
	this.lastDoc = null;
	this.token = null;
	
	/**
		The chain of symbols under which we are currently nested.
		@type Array
	*/
	this.namescope = [globalSymbol];
	this.namescope.last = function(n){ if (!n) n = 0; return this[this.length-(1+n)] || "" };
}

JSDOC.Walker.prototype.walk = function(/**JSDOC.TokenStream*/ts) {
	this.ts = ts;
	while (this.token = this.ts.look()) {
		if (this.token.popNamescope) {
			
			var symbol = this.namescope.pop();
			if (symbol.is("FUNCTION")) {
				if (this.ts.look(1).is("LEFT_PAREN") && symbol.comment.getTag("function").length == 0) {
					symbol.isa = "OBJECT";
				}
			}
		}
		this.step();
		if (!this.ts.next()) break;
	}
}

JSDOC.Walker.prototype.step = function() {
	if (this.token.is("JSDOC")) { // it's a doc comment
	
		var doc = new JSDOC.DocComment(this.token.data);
		
				
		if (doc.getTag("exports").length > 0) {
			var exports = doc.getTag("exports")[0];

			exports.desc.match(/(\S+) as (\S+)/i);
			var n1 = RegExp.$1;
			var n2 = RegExp.$2;
			
			if (!n1 && n2) throw "@exports tag requires a value like: 'name as ns.name'";
			
			JSDOC.Parser.rename = (JSDOC.Parser.rename || {});	
			JSDOC.Parser.rename[n1] = n2
		}
		
		if (doc.getTag("lends").length > 0) {
			var lends = doc.getTag("lends")[0];

			var name = lends.desc
			if (!name) throw "@lends tag requires a value.";
			
			var symbol = new JSDOC.Symbol(name, [], "OBJECT", doc);
			
			this.namescope.push(symbol);
			
			var matching = this.ts.getMatchingToken("LEFT_CURLY");
			if (matching) matching.popNamescope = name;
			else LOG.warn("Mismatched } character. Can't parse code in file " + symbol.srcFile + ".");
			
			this.lastDoc = null;
			return true;
		}
		else if (doc.getTag("name").length > 0 && doc.getTag("overview").length == 0) { // it's a virtual symbol
			var virtualName = doc.getTag("name")[0].desc;
			if (!virtualName) throw "@name tag requires a value.";
			
			var symbol = new JSDOC.Symbol(virtualName, [], "VIRTUAL", doc);
			
			JSDOC.Parser.addSymbol(symbol);
			
			this.lastDoc = null;
			return true;
		}
		else if (doc.meta) { // it's a meta doclet
			if (doc.meta == "@+") JSDOC.DocComment.shared = doc.src;
			else if (doc.meta == "@-") JSDOC.DocComment.shared = "";
			else if (doc.meta == "nocode+") JSDOC.Parser.conf.ignoreCode = true;
			else if (doc.meta == "nocode-") JSDOC.Parser.conf.ignoreCode = JSDOC.opt.n;
			else throw "Unrecognized meta comment: "+doc.meta;
			
			this.lastDoc = null;
			return true;
		}
		else if (doc.getTag("overview").length > 0) { // it's a file overview
			symbol = new JSDOC.Symbol("", [], "FILE", doc);
			
			JSDOC.Parser.addSymbol(symbol);
			
			this.lastDoc = null;
			return true;
		}
		else {
			this.lastDoc = doc;
			return false;
		}
	}
	else if (!JSDOC.Parser.conf.ignoreCode) { // it's code
		if (this.token.is("NAME")) { // it's the name of something
			var symbol;
			var name = this.token.data;
			var doc = null; if (this.lastDoc) doc = this.lastDoc;
			var params = [];
		
			// it's inside an anonymous object
			if (this.ts.look(1).is("COLON") && this.ts.look(-1).is("LEFT_CURLY") && !(this.ts.look(-2).is("JSDOC") || this.namescope.last().comment.getTag("lends").length || this.ts.look(-2).is("ASSIGN") || this.ts.look(-2).is("COLON"))) {
				name = "$anonymous";
				name = this.namescope.last().alias+"-"+name
					
				params = [];
				
				symbol = new JSDOC.Symbol(name, params, "OBJECT", doc);

				JSDOC.Parser.addSymbol(symbol);
				
				this.namescope.push(symbol);
				
				var matching = this.ts.getMatchingToken(null, "RIGHT_CURLY");
				if (matching) matching.popNamescope = name;
				else LOG.warn("Mismatched } character. Can't parse code in file " + symbol.srcFile + ".");
			}
			// function foo() {}
			else if (this.ts.look(-1).is("FUNCTION") && this.ts.look(1).is("LEFT_PAREN")) {
				var isInner;
				
				if (this.lastDoc) doc = this.lastDoc;
				name = this.namescope.last().alias+"-"+name;
				if (!this.namescope.last().is("GLOBAL")) isInner = true;
				
				params = JSDOC.Walker.onParamList(this.ts.balance("LEFT_PAREN"));

				symbol = new JSDOC.Symbol(name, params, "FUNCTION", doc);
				if (isInner) symbol.isInner = true;

				if (this.ts.look(1).is("JSDOC")) {
					var inlineReturn = ""+this.ts.look(1).data;
					inlineReturn = inlineReturn.replace(/(^\/\*\* *| *\*\/$)/g, "");
					symbol.type = inlineReturn;
				}
				
				JSDOC.Parser.addSymbol(symbol);
				
				this.namescope.push(symbol);
				
				var matching = this.ts.getMatchingToken("LEFT_CURLY");
				if (matching) matching.popNamescope = name;
				else LOG.warn("Mismatched } character. Can't parse code in file " + symbol.srcFile + ".");
			}
			// foo = function() {}
			else if (this.ts.look(1).is("ASSIGN") && this.ts.look(2).is("FUNCTION")) {
				var isInner;
				if (this.ts.look(-1).is("VAR") || this.isInner) {
					name = this.namescope.last().alias+"-"+name
					if (!this.namescope.last().is("GLOBAL")) isInner = true;
				}
				else if (name.indexOf("this.") == 0) {
					name = this.resolveThis(name);
				}

				if (this.lastDoc) doc = this.lastDoc;
				params = JSDOC.Walker.onParamList(this.ts.balance("LEFT_PAREN"));
				
				symbol = new JSDOC.Symbol(name, params, "FUNCTION", doc);
				if (isInner) symbol.isInner = true;
				
				if (this.ts.look(1).is("JSDOC")) {
					var inlineReturn = ""+this.ts.look(1).data;
					inlineReturn = inlineReturn.replace(/(^\/\*\* *| *\*\/$)/g, "");
					symbol.type = inlineReturn;
				}
				
				JSDOC.Parser.addSymbol(symbol);
				
				this.namescope.push(symbol);
				
				var matching = this.ts.getMatchingToken("LEFT_CURLY");
				if (matching) matching.popNamescope = name;
				else LOG.warn("Mismatched } character. Can't parse code in file " + symbol.srcFile + ".");
			}
			// foo = new function() {} or foo = (function() {}
			else if (this.ts.look(1).is("ASSIGN") && (this.ts.look(2).is("NEW") || this.ts.look(2).is("LEFT_PAREN")) && this.ts.look(3).is("FUNCTION")) {
				var isInner;
				if (this.ts.look(-1).is("VAR") || this.isInner) {
					name = this.namescope.last().alias+"-"+name
					if (!this.namescope.last().is("GLOBAL")) isInner = true;
				}
				else if (name.indexOf("this.") == 0) {
					name = this.resolveThis(name);
				}

				this.ts.next(3); // advance past the "new" or "("
				
				if (this.lastDoc) doc = this.lastDoc;
				params = JSDOC.Walker.onParamList(this.ts.balance("LEFT_PAREN"));
				
				symbol = new JSDOC.Symbol(name, params, "OBJECT", doc);
				if (isInner) symbol.isInner = true;
				
				if (this.ts.look(1).is("JSDOC")) {
					var inlineReturn = ""+this.ts.look(1).data;
					inlineReturn = inlineReturn.replace(/(^\/\*\* *| *\*\/$)/g, "");
					symbol.type = inlineReturn;
				}
				
				JSDOC.Parser.addSymbol(symbol);
				
				symbol.scopeType = "INSTANCE";
				this.namescope.push(symbol);
				
				var matching = this.ts.getMatchingToken("LEFT_CURLY");
				if (matching) matching.popNamescope = name;
				else LOG.warn("Mismatched } character. Can't parse code in file " + symbol.srcFile + ".");
			}
			// foo: function() {}
			else if (this.ts.look(1).is("COLON") && this.ts.look(2).is("FUNCTION")) {
				name = (this.namescope.last().alias+"."+name).replace("#.", "#");
				
				if (this.lastDoc) doc = this.lastDoc;
				params = JSDOC.Walker.onParamList(this.ts.balance("LEFT_PAREN"));
				
				if (doc && doc.getTag("constructs").length) {
					name = name.replace(/\.prototype(\.|$)/, "#");
					
					if (name.indexOf("#") > -1) name = name.match(/(^[^#]+)/)[0];
					else name = this.namescope.last().alias;

					symbol = new JSDOC.Symbol(name, params, "CONSTRUCTOR", doc);
				}
				else {
					symbol = new JSDOC.Symbol(name, params, "FUNCTION", doc);
				}
				
				if (this.ts.look(1).is("JSDOC")) {
					var inlineReturn = ""+this.ts.look(1).data;
					inlineReturn = inlineReturn.replace(/(^\/\*\* *| *\*\/$)/g, "");
					symbol.type = inlineReturn;
				}
				
				JSDOC.Parser.addSymbol(symbol);
				
				this.namescope.push(symbol);
				
				var matching = this.ts.getMatchingToken("LEFT_CURLY");
				if (matching) matching.popNamescope = name;
				else LOG.warn("Mismatched } character. Can't parse code in file " + symbol.srcFile + ".");
			}
			// foo = {}
			else if (this.ts.look(1).is("ASSIGN") && this.ts.look(2).is("LEFT_CURLY")) {
				var isInner;
				if (this.ts.look(-1).is("VAR") || this.isInner) {
					name = this.namescope.last().alias+"-"+name
					if (!this.namescope.last().is("GLOBAL")) isInner = true;
				}
				else if (name.indexOf("this.") == 0) {
					name = this.resolveThis(name);
				}
				
				if (this.lastDoc) doc = this.lastDoc;
				
				symbol = new JSDOC.Symbol(name, params, "OBJECT", doc);
				if (isInner) symbol.isInner = true;
				
			
				if (doc) JSDOC.Parser.addSymbol(symbol);

				this.namescope.push(symbol);
				
				var matching = this.ts.getMatchingToken("LEFT_CURLY");
				if (matching) matching.popNamescope = name;
				else LOG.warn("Mismatched } character. Can't parse code in file " + symbol.srcFile + ".");
			}
			// var foo;
			else if (this.ts.look(1).is("SEMICOLON")) {
				var isInner;

				if (this.ts.look(-1).is("VAR") || this.isInner) {
					name = this.namescope.last().alias+"-"+name
					if (!this.namescope.last().is("GLOBAL")) isInner = true;
					
					if (this.lastDoc) doc = this.lastDoc;
				
					symbol = new JSDOC.Symbol(name, params, "OBJECT", doc);
					if (isInner) symbol.isInner = true;
					
				
					if (doc) JSDOC.Parser.addSymbol(symbol);
				}
			}
			// foo = x
			else if (this.ts.look(1).is("ASSIGN")) {				
				var isInner;
				if (this.ts.look(-1).is("VAR") || this.isInner) {
					name = this.namescope.last().alias+"-"+name
					if (!this.namescope.last().is("GLOBAL")) isInner = true;
				}
				else if (name.indexOf("this.") == 0) {
					name = this.resolveThis(name);
				}
				
				if (this.lastDoc) doc = this.lastDoc;
				
				symbol = new JSDOC.Symbol(name, params, "OBJECT", doc);
				if (isInner) symbol.isInner = true;
				
			
				if (doc) JSDOC.Parser.addSymbol(symbol);
			}
			// foo: {}
			else if (this.ts.look(1).is("COLON") && this.ts.look(2).is("LEFT_CURLY")) {
				name = (this.namescope.last().alias+"."+name).replace("#.", "#");
				
				if (this.lastDoc) doc = this.lastDoc;
				
				symbol = new JSDOC.Symbol(name, params, "OBJECT", doc);
				
			
				if (doc) JSDOC.Parser.addSymbol(symbol);
				
				this.namescope.push(symbol);
				
				var matching = this.ts.getMatchingToken("LEFT_CURLY");
				if (matching) matching.popNamescope = name;
				else LOG.warn("Mismatched } character. Can't parse code in file " + symbol.srcFile + ".");
			}
			// foo: x
			else if (this.ts.look(1).is("COLON")) {
				name = (this.namescope.last().alias+"."+name).replace("#.", "#");;
				
				if (this.lastDoc) doc = this.lastDoc;
				
				symbol = new JSDOC.Symbol(name, params, "OBJECT", doc);
				
			
				if (doc) JSDOC.Parser.addSymbol(symbol);
			}
			// foo(...)
			else if (this.ts.look(1).is("LEFT_PAREN")) {
				if (typeof JSDOC.PluginManager != "undefined") {
					var functionCall = {name: name};
				
					var cursor = this.ts.cursor;
					params = JSDOC.Walker.onParamList(this.ts.balance("LEFT_PAREN"));
					this.ts.cursor = cursor;
					
					for (var i = 0; i < params.length; i++)
						functionCall["arg" + (i + 1)] = params[i].name;
				
					JSDOC.PluginManager.run("onFunctionCall", functionCall);
					if (functionCall.doc) {
						this.ts.insertAhead(new JSDOC.Token(functionCall.doc, "COMM", "JSDOC"));
					}
				}
			}
			this.lastDoc = null;
		}
		else if (this.token.is("FUNCTION")) { // it's an anonymous function
			if (
				(!this.ts.look(-1).is("COLON") || !this.ts.look(-1).is("ASSIGN"))
				&& !this.ts.look(1).is("NAME")
			) {
				if (this.lastDoc) doc = this.lastDoc;
				
				name = "$anonymous";
				name = this.namescope.last().alias+"-"+name
				
				params = JSDOC.Walker.onParamList(this.ts.balance("LEFT_PAREN"));
				
				symbol = new JSDOC.Symbol(name, params, "FUNCTION", doc);
				
				JSDOC.Parser.addSymbol(symbol);
				
				this.namescope.push(symbol);
				
				var matching = this.ts.getMatchingToken("LEFT_CURLY");
				if (matching) matching.popNamescope = name;
				else LOG.warn("Mismatched } character. Can't parse code in file " + symbol.srcFile + ".");
			}
		}
	}
	return true;
}

/**
	Resolves what "this." means when it appears in a name.
	@param name The name that starts with "this.".
	@returns The name with "this." resolved.
 */
JSDOC.Walker.prototype.resolveThis = function(name) {
	name.match(/^this\.(.+)$/)
	var nameFragment = RegExp.$1;
	if (!nameFragment) return name;
	
	var symbol = this.namescope.last();
	var scopeType = symbol.scopeType || symbol.isa;

	// if we are in a constructor function, `this` means the instance
	if (scopeType == "CONSTRUCTOR") {
		name = symbol.alias+"#"+nameFragment;
	}
	
	// if we are in an anonymous constructor function, `this` means the instance
	else if (scopeType == "INSTANCE") {
		name = symbol.alias+"."+nameFragment;
	}
	
	// if we are in a function, `this` means the container (possibly the global)
	else if (scopeType == "FUNCTION") {
		// in a method of a prototype, so `this` means the constructor
		if (symbol.alias.match(/(^.*)[#.-][^#.-]+/)) {
			var parentName = RegExp.$1;
			var parent = JSDOC.Parser.symbols.getSymbol(parentName);

			if (!parent) {
				if (JSDOC.Lang.isBuiltin(parentName)) parent = JSDOC.Parser.addBuiltin(parentName);
				else {
					if (symbol.alias.indexOf("$anonymous") < 0) // these will be ignored eventually
						LOG.warn("Trying to document "+symbol.alias+" without first documenting "+parentName+".");
				}
			}
			if (parent) name = parentName+(parent.is("CONSTRUCTOR")?"#":".")+nameFragment;
		}
		else {
			parent = this.namescope.last(1);
			name = parent.alias+(parent.is("CONSTRUCTOR")?"#":".")+nameFragment;
		}
	}
	// otherwise it means the global
	else {
		name = nameFragment;
	}
	
	return name;
}

JSDOC.Walker.onParamList = function(/**Array*/paramTokens) {
	if (!paramTokens) {
		LOG.warn("Malformed parameter list. Can't parse code.");
		return [];
	}
	var params = [];
	for (var i = 0, l = paramTokens.length; i < l; i++) {
		if (paramTokens[i].is("JSDOC")) {
			var paramType = paramTokens[i].data.replace(/(^\/\*\* *| *\*\/$)/g, "");
			
			if (paramTokens[i+1] && paramTokens[i+1].is("NAME")) {
				i++;
				params.push({type: paramType, name: paramTokens[i].data});
			}
		}
		else if (paramTokens[i].is("NAME")) {
			params.push({name: paramTokens[i].data});
		}
	}
	return params;
}