// 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); 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("")); } } 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); } }; });