You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

401 lines
14 KiB

// Parses comments above variable declarations, function declarations,
// and object properties as docstrings and JSDoc-style type
// annotations.
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
return mod(require("../lib/infer"), require("../lib/tern"), require("../lib/comment"),
require("acorn"), require("acorn/dist/walk"));
if (typeof define == "function" && define.amd) // AMD
return define(["../lib/infer", "../lib/tern", "../lib/comment", "acorn/dist/acorn", "acorn/dist/walk"], mod);
mod(tern, tern, tern.comment, acorn, acorn.walk);
})(function(infer, tern, comment, acorn, walk) {
"use strict";
var WG_MADEUP = 1, WG_STRONG = 101;
tern.registerPlugin("doc_comment", function(server, options) {
server.jsdocTypedefs = Object.create(null);
server.on("reset", function() {
server.jsdocTypedefs = Object.create(null);
});
server._docComment = {
weight: options && options.strong ? WG_STRONG : undefined,
fullDocs: options && options.fullDocs
};
return {
passes: {
postParse: postParse,
postInfer: postInfer,
postLoadDef: postLoadDef
}
};
});
function postParse(ast, text) {
function attachComments(node) { comment.ensureCommentsBefore(text, node); }
walk.simple(ast, {
VariableDeclaration: attachComments,
FunctionDeclaration: attachComments,
AssignmentExpression: function(node) {
if (node.operator == "=") attachComments(node);
},
ObjectExpression: function(node) {
for (var i = 0; i < node.properties.length; ++i)
attachComments(node.properties[i]);
},
CallExpression: function(node) {
if (isDefinePropertyCall(node)) attachComments(node);
}
});
}
function isDefinePropertyCall(node) {
return node.callee.type == "MemberExpression" &&
node.callee.object.name == "Object" &&
node.callee.property.name == "defineProperty" &&
node.arguments.length >= 3 &&
typeof node.arguments[1].value == "string";
}
function postInfer(ast, scope) {
jsdocParseTypedefs(ast.sourceFile.text, scope);
walk.simple(ast, {
VariableDeclaration: function(node, scope) {
if (node.commentsBefore)
interpretComments(node, node.commentsBefore, scope,
scope.getProp(node.declarations[0].id.name));
},
FunctionDeclaration: function(node, scope) {
if (node.commentsBefore)
interpretComments(node, node.commentsBefore, scope,
scope.getProp(node.id.name),
node.body.scope.fnType);
},
AssignmentExpression: function(node, scope) {
if (node.commentsBefore)
interpretComments(node, node.commentsBefore, scope,
infer.expressionType({node: node.left, state: scope}));
},
ObjectExpression: function(node, scope) {
for (var i = 0; i < node.properties.length; ++i) {
var prop = node.properties[i];
if (prop.commentsBefore)
interpretComments(prop, prop.commentsBefore, scope,
node.objType.getProp(prop.key.name));
}
},
CallExpression: function(node, scope) {
if (node.commentsBefore && isDefinePropertyCall(node)) {
var type = infer.expressionType({node: node.arguments[0], state: scope}).getObjType();
if (type && type instanceof infer.Obj) {
var prop = type.props[node.arguments[1].value];
if (prop) interpretComments(node, node.commentsBefore, scope, prop);
}
}
}
}, infer.searchVisitor, scope);
}
function postLoadDef(data) {
var defs = data["!typedef"];
var cx = infer.cx(), orig = data["!name"];
if (defs) for (var name in defs)
cx.parent.jsdocTypedefs[name] =
maybeInstance(infer.def.parse(defs[name], orig, name), name);
}
// COMMENT INTERPRETATION
function interpretComments(node, comments, scope, aval, type) {
jsdocInterpretComments(node, scope, aval, comments);
var cx = infer.cx();
if (!type && aval instanceof infer.AVal && aval.types.length) {
type = aval.types[aval.types.length - 1];
if (!(type instanceof infer.Obj) || type.origin != cx.curOrigin || type.doc)
type = null;
}
var result = comments[comments.length - 1];
if (cx.parent._docComment.fullDocs) {
result = result.trim().replace(/\n[ \t]*\* ?/g, "\n");
} else {
var dot = result.search(/\.\s/);
if (dot > 5) result = result.slice(0, dot + 1);
result = result.trim().replace(/\s*\n\s*\*\s*|\s{1,}/g, " ");
}
result = result.replace(/^\s*\*+\s*/, "");
if (aval instanceof infer.AVal) aval.doc = result;
if (type) type.doc = result;
}
// Parses a subset of JSDoc-style comments in order to include the
// explicitly defined types in the analysis.
function skipSpace(str, pos) {
while (/\s/.test(str.charAt(pos))) ++pos;
return pos;
}
function isIdentifier(string) {
if (!acorn.isIdentifierStart(string.charCodeAt(0))) return false;
for (var i = 1; i < string.length; i++)
if (!acorn.isIdentifierChar(string.charCodeAt(i))) return false;
return true;
}
function parseLabelList(scope, str, pos, close) {
var labels = [], types = [], madeUp = false;
for (var first = true; ; first = false) {
pos = skipSpace(str, pos);
if (first && str.charAt(pos) == close) break;
var colon = str.indexOf(":", pos);
if (colon < 0) return null;
var label = str.slice(pos, colon);
if (!isIdentifier(label)) return null;
labels.push(label);
pos = colon + 1;
var type = parseType(scope, str, pos);
if (!type) return null;
pos = type.end;
madeUp = madeUp || type.madeUp;
types.push(type.type);
pos = skipSpace(str, pos);
var next = str.charAt(pos);
++pos;
if (next == close) break;
if (next != ",") return null;
}
return {labels: labels, types: types, end: pos, madeUp: madeUp};
}
function parseType(scope, str, pos) {
var type, union = false, madeUp = false;
for (;;) {
var inner = parseTypeInner(scope, str, pos);
if (!inner) return null;
madeUp = madeUp || inner.madeUp;
if (union) inner.type.propagate(union);
else type = inner.type;
pos = skipSpace(str, inner.end);
if (str.charAt(pos) != "|") break;
pos++;
if (!union) {
union = new infer.AVal;
type.propagate(union);
type = union;
}
}
var isOptional = false;
if (str.charAt(pos) == "=") {
++pos;
isOptional = true;
}
return {type: type, end: pos, isOptional: isOptional, madeUp: madeUp};
}
function parseTypeInner(scope, str, pos) {
pos = skipSpace(str, pos);
var type, madeUp = false;
if (str.indexOf("function(", pos) == pos) {
var args = parseLabelList(scope, str, pos + 9, ")"), ret = infer.ANull;
if (!args) return null;
pos = skipSpace(str, args.end);
if (str.charAt(pos) == ":") {
++pos;
var retType = parseType(scope, str, pos + 1);
if (!retType) return null;
pos = retType.end;
ret = retType.type;
madeUp = retType.madeUp;
}
type = new infer.Fn(null, infer.ANull, args.types, args.labels, ret);
} else if (str.charAt(pos) == "[") {
var inner = parseType(scope, str, pos + 1);
if (!inner) return null;
pos = skipSpace(str, inner.end);
madeUp = inner.madeUp;
if (str.charAt(pos) != "]") return null;
++pos;
type = new infer.Arr(inner.type);
} else if (str.charAt(pos) == "{") {
var fields = parseLabelList(scope, str, pos + 1, "}");
if (!fields) return null;
type = new infer.Obj(true);
for (var i = 0; i < fields.types.length; ++i) {
var field = type.defProp(fields.labels[i]);
field.initializer = true;
fields.types[i].propagate(field);
}
pos = fields.end;
madeUp = fields.madeUp;
} else if (str.charAt(pos) == "(") {
var inner = parseType(scope, str, pos + 1);
if (!inner) return null;
pos = skipSpace(str, inner.end);
if (str.charAt(pos) != ")") return null;
++pos;
type = inner.type;
} else {
var start = pos;
if (!acorn.isIdentifierStart(str.charCodeAt(pos))) return null;
while (acorn.isIdentifierChar(str.charCodeAt(pos))) ++pos;
if (start == pos) return null;
var word = str.slice(start, pos);
if (/^(number|integer)$/i.test(word)) type = infer.cx().num;
else if (/^bool(ean)?$/i.test(word)) type = infer.cx().bool;
else if (/^string$/i.test(word)) type = infer.cx().str;
else if (/^(null|undefined)$/i.test(word)) type = infer.ANull;
else if (/^array$/i.test(word)) {
var inner = null;
if (str.charAt(pos) == "." && str.charAt(pos + 1) == "<") {
var inAngles = parseType(scope, str, pos + 2);
if (!inAngles) return null;
pos = skipSpace(str, inAngles.end);
madeUp = inAngles.madeUp;
if (str.charAt(pos++) != ">") return null;
inner = inAngles.type;
}
type = new infer.Arr(inner);
} else if (/^object$/i.test(word)) {
type = new infer.Obj(true);
if (str.charAt(pos) == "." && str.charAt(pos + 1) == "<") {
var key = parseType(scope, str, pos + 2);
if (!key) return null;
pos = skipSpace(str, key.end);
if (str.charAt(pos++) != ",") return null;
var val = parseType(scope, str, pos);
if (!val) return null;
pos = skipSpace(str, val.end);
madeUp = key.madeUp || val.madeUp;
if (str.charAt(pos++) != ">") return null;
val.type.propagate(type.defProp("<i>"));
}
} else {
while (str.charCodeAt(pos) == 46 ||
acorn.isIdentifierChar(str.charCodeAt(pos))) ++pos;
var path = str.slice(start, pos);
var cx = infer.cx(), defs = cx.parent && cx.parent.jsdocTypedefs, found;
if (defs && (path in defs)) {
type = defs[path];
} else if (found = infer.def.parsePath(path, scope).getObjType()) {
type = maybeInstance(found, path);
} else {
if (!cx.jsdocPlaceholders) cx.jsdocPlaceholders = Object.create(null);
if (!(path in cx.jsdocPlaceholders))
type = cx.jsdocPlaceholders[path] = new infer.Obj(null, path);
else
type = cx.jsdocPlaceholders[path];
madeUp = true;
}
}
}
return {type: type, end: pos, madeUp: madeUp};
}
function maybeInstance(type, path) {
if (type instanceof infer.Fn && /^[A-Z]/.test(path)) {
var proto = type.getProp("prototype").getObjType();
if (proto instanceof infer.Obj) return infer.getInstance(proto);
}
return type;
}
function parseTypeOuter(scope, str, pos) {
pos = skipSpace(str, pos || 0);
if (str.charAt(pos) != "{") return null;
var result = parseType(scope, str, pos + 1);
if (!result) return null;
var end = skipSpace(str, result.end);
if (str.charAt(end) != "}") return null;
result.end = end + 1;
return result;
}
function jsdocInterpretComments(node, scope, aval, comments) {
var type, args, ret, foundOne, self, parsed;
for (var i = 0; i < comments.length; ++i) {
var comment = comments[i];
var decl = /(?:\n|\$|\*)\s*@(type|param|arg(?:ument)?|returns?|this)\s+(.*)/g, m;
while (m = decl.exec(comment)) {
if (m[1] == "this" && (parsed = parseType(scope, m[2], 0))) {
self = parsed;
foundOne = true;
continue;
}
if (!(parsed = parseTypeOuter(scope, m[2]))) continue;
foundOne = true;
switch(m[1]) {
case "returns": case "return":
ret = parsed; break;
case "type":
type = parsed; break;
case "param": case "arg": case "argument":
var name = m[2].slice(parsed.end).match(/^\s*(\[?)\s*([^\]\s=]+)\s*(?:=[^\]]+\s*)?(\]?).*/);
if (!name) continue;
var argname = name[2] + (parsed.isOptional || (name[1] === '[' && name[3] === ']') ? "?" : "");
(args || (args = Object.create(null)))[argname] = parsed;
break;
}
}
}
if (foundOne) applyType(type, self, args, ret, node, aval);
};
function jsdocParseTypedefs(text, scope) {
var cx = infer.cx();
var re = /\s@typedef\s+(.*)/g, m;
while (m = re.exec(text)) {
var parsed = parseTypeOuter(scope, m[1]);
var name = parsed && m[1].slice(parsed.end).match(/^\s*(\S+)/);
if (name)
cx.parent.jsdocTypedefs[name[1]] = parsed.type;
}
}
function propagateWithWeight(type, target) {
var weight = infer.cx().parent._docComment.weight;
type.type.propagate(target, weight || (type.madeUp ? WG_MADEUP : undefined));
}
function applyType(type, self, args, ret, node, aval) {
var fn;
if (node.type == "VariableDeclaration") {
var decl = node.declarations[0];
if (decl.init && decl.init.type == "FunctionExpression") fn = decl.init.body.scope.fnType;
} else if (node.type == "FunctionDeclaration") {
fn = node.body.scope.fnType;
} else if (node.type == "AssignmentExpression") {
if (node.right.type == "FunctionExpression")
fn = node.right.body.scope.fnType;
} else if (node.type == "CallExpression") {
} else { // An object property
if (node.value.type == "FunctionExpression") fn = node.value.body.scope.fnType;
}
if (fn && (args || ret || self)) {
if (args) for (var i = 0; i < fn.argNames.length; ++i) {
var name = fn.argNames[i], known = args[name];
if (!known && (known = args[name + "?"]))
fn.argNames[i] += "?";
if (known) propagateWithWeight(known, fn.args[i]);
}
if (ret) propagateWithWeight(ret, fn.retval);
if (self) propagateWithWeight(self, fn.self);
} else if (type) {
propagateWithWeight(type, aval);
}
};
});