core/lib/tern/plugin/doc_comment.js
2017-05-14 13:22:31 +02:00

473 lines
17 KiB
JavaScript

// 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.mod.jsdocTypedefs = Object.create(null);
server.on("reset", function() {
server.mod.jsdocTypedefs = Object.create(null);
});
server.mod.docComment = {
weight: options && options.strong ? WG_STRONG : undefined,
fullDocs: options && options.fullDocs
};
server.on("postParse", postParse)
server.on("postInfer", postInfer)
server.on("postLoadDef", postLoadDef)
});
function postParse(ast, text) {
function attachComments(node) { comment.ensureCommentsBefore(text, node); }
walk.simple(ast, {
VariableDeclaration: attachComments,
FunctionDeclaration: attachComments,
MethodDefinition: attachComments,
Property: attachComments,
AssignmentExpression: function(node) {
if (node.operator == "=") attachComments(node);
},
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) {
var decl = node.declarations[0].id
if (node.commentsBefore && decl.type == "Identifier")
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.scope.fnType);
},
ClassDeclaration: function(node, scope) {
if (node.commentsBefore)
interpretComments(node, node.commentsBefore, scope,
scope.getProp(node.id.name),
node.objType);
},
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], name = infer.propName(prop)
if (name != "<i>" && prop.commentsBefore)
interpretComments(prop, prop.commentsBefore, scope, node.objType.getProp(name))
}
},
Class: function(node, scope) {
var proto = node.objType.getProp("prototype").getObjType()
if (!proto) return
for (var i = 0; i < node.body.body.length; i++) {
var method = node.body.body[i], name
if (!method.commentsBefore) continue
if (method.kind == "constructor")
interpretComments(method, method.commentsBefore, scope, node.objType)
else if ((name = infer.propName(method)) != "<i>")
interpretComments(method, method.commentsBefore, scope, proto.getProp(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.mod.jsdocTypedefs[name] =
maybeInstance(infer.def.parse(defs[name], orig, name), name);
}
// COMMENT INTERPRETATION
function stripLeadingChars(lines) {
for (var head, i = 1; i < lines.length; i++) {
var line = lines[i], lineHead = line.match(/^[\s\*]*/)[0];
if (lineHead != line) {
if (head == null) {
head = lineHead;
} else {
var same = 0;
while (same < head.length && head.charCodeAt(same) == lineHead.charCodeAt(same)) ++same;
if (same < head.length) head = head.slice(0, same)
}
}
}
lines = lines.map(function(line, i) {
line = line.replace(/\s+$/, "");
if (i == 0 && head != null) {
for (var j = 0; j < head.length; j++) {
var found = line.indexOf(head.slice(j));
if (found == 0) return line.slice(head.length - j);
}
}
if (head == null || i == 0) return line.replace(/^[\s\*]*/, "");
if (line.length < head.length) return "";
return line.slice(head.length);
});
while (lines.length && !lines[lines.length - 1]) lines.pop();
while (lines.length && !lines[0]) lines.shift();
return lines;
}
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;
}
for (var i = comments.length - 1; i >= 0; i--) {
var text = stripLeadingChars(comments[i].split(/\r\n?|\n/)).join("\n");
if (text) {
if (aval instanceof infer.AVal) aval.doc = text;
if (type) type.doc = text;
break;
}
}
}
// 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 parseTypeAtom(scope, str, pos) {
var result = parseTypeInner(scope, str, pos)
if (!result) return null
if (str.slice(result.end, result.end + 2) == "[]")
return {madeUp: result.madeUp, end: result.end + 2, type: new infer.Arr(result.type)}
else return result
}
function parseType(scope, str, pos) {
var type, union = false, madeUp = false;
for (;;) {
var inner = parseTypeAtom(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.mod.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|class|constructor)\s+(.*)/g, m;
while (m = decl.exec(comment)) {
if (m[1] == "class" || m[1] == "constructor") {
self = foundOne = true;
continue;
}
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 && parsed.type instanceof infer.Obj) {
var rest = text.slice(m.index + m[0].length)
while (m = /\s+@prop(?:erty)?\s+(.*)/.exec(rest)) {
var propType = parseTypeOuter(scope, m[1]), propName
if (propType && (propName = m[1].slice(propType.end).match(/^\s*(\S+)/)))
propType.type.propagate(parsed.type.defProp(propName[1]))
rest = rest.slice(m[0].length)
}
cx.parent.mod.jsdocTypedefs[name[1]] = parsed.type;
}
}
}
function propagateWithWeight(type, target) {
var weight = infer.cx().parent.mod.docComment.weight;
type.type.propagate(target, weight || (type.madeUp ? WG_MADEUP : undefined));
}
function isFunExpr(node) { return node.type == "FunctionExpression" || node.type == "ArrowFunctionExpression" }
function applyType(type, self, args, ret, node, aval) {
var fn;
if (node.type == "VariableDeclaration") {
var decl = node.declarations[0];
if (decl.init && isFunExpr(decl.init)) fn = decl.init.scope.fnType;
} else if (node.type == "FunctionDeclaration") {
fn = node.scope.fnType;
} else if (node.type == "AssignmentExpression") {
if (isFunExpr(node.right))
fn = node.right.scope.fnType;
} else if (node.type == "CallExpression") {
} else { // An object property
if (isFunExpr(node.value)) fn = node.value.scope.fnType;
else if (fn = aval.types && aval.types[0] && aval.types[0].args)
fn = aval.types[0];
}
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) {
if (fn.retval == infer.ANull) fn.retval = new infer.AVal;
propagateWithWeight(ret, fn.retval);
}
if (self === true) {
var proto = fn.getProp("prototype").getObjType();
self = proto && {type: infer.getInstance(proto, fn)};
}
if (self) propagateWithWeight(self, fn.self);
} else if (type) {
propagateWithWeight(type, aval);
}
};
});