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.
402 lines
14 KiB
402 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/acorn"), require("acorn/util/walk"));
|
|
if (typeof define == "function" && define.amd) // AMD
|
|
return define(["../lib/infer", "../lib/tern", "../lib/comment", "acorn/acorn", "acorn/util/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);
|
|
madeUp = madeUp || key.madeUp;
|
|
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);
|
|
}
|
|
};
|
|
});
|
|
|