1
0

doc_comment.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. // Parses comments above variable declarations, function declarations,
  2. // and object properties as docstrings and JSDoc-style type
  3. // annotations.
  4. (function(mod) {
  5. if (typeof exports == "object" && typeof module == "object") // CommonJS
  6. return mod(require("../lib/infer"), require("../lib/tern"), require("../lib/comment"),
  7. require("acorn"), require("acorn/dist/walk"));
  8. if (typeof define == "function" && define.amd) // AMD
  9. return define(["../lib/infer", "../lib/tern", "../lib/comment", "acorn/dist/acorn", "acorn/dist/walk"], mod);
  10. mod(tern, tern, tern.comment, acorn, acorn.walk);
  11. })(function(infer, tern, comment, acorn, walk) {
  12. "use strict";
  13. var WG_MADEUP = 1, WG_STRONG = 101;
  14. tern.registerPlugin("doc_comment", function(server, options) {
  15. server.jsdocTypedefs = Object.create(null);
  16. server.on("reset", function() {
  17. server.jsdocTypedefs = Object.create(null);
  18. });
  19. server._docComment = {
  20. weight: options && options.strong ? WG_STRONG : undefined,
  21. fullDocs: options && options.fullDocs
  22. };
  23. return {
  24. passes: {
  25. postParse: postParse,
  26. postInfer: postInfer,
  27. postLoadDef: postLoadDef
  28. }
  29. };
  30. });
  31. function postParse(ast, text) {
  32. function attachComments(node) { comment.ensureCommentsBefore(text, node); }
  33. walk.simple(ast, {
  34. VariableDeclaration: attachComments,
  35. FunctionDeclaration: attachComments,
  36. AssignmentExpression: function(node) {
  37. if (node.operator == "=") attachComments(node);
  38. },
  39. ObjectExpression: function(node) {
  40. for (var i = 0; i < node.properties.length; ++i)
  41. attachComments(node.properties[i]);
  42. },
  43. CallExpression: function(node) {
  44. if (isDefinePropertyCall(node)) attachComments(node);
  45. }
  46. });
  47. }
  48. function isDefinePropertyCall(node) {
  49. return node.callee.type == "MemberExpression" &&
  50. node.callee.object.name == "Object" &&
  51. node.callee.property.name == "defineProperty" &&
  52. node.arguments.length >= 3 &&
  53. typeof node.arguments[1].value == "string";
  54. }
  55. function postInfer(ast, scope) {
  56. jsdocParseTypedefs(ast.sourceFile.text, scope);
  57. walk.simple(ast, {
  58. VariableDeclaration: function(node, scope) {
  59. if (node.commentsBefore)
  60. interpretComments(node, node.commentsBefore, scope,
  61. scope.getProp(node.declarations[0].id.name));
  62. },
  63. FunctionDeclaration: function(node, scope) {
  64. if (node.commentsBefore)
  65. interpretComments(node, node.commentsBefore, scope,
  66. scope.getProp(node.id.name),
  67. node.body.scope.fnType);
  68. },
  69. AssignmentExpression: function(node, scope) {
  70. if (node.commentsBefore)
  71. interpretComments(node, node.commentsBefore, scope,
  72. infer.expressionType({node: node.left, state: scope}));
  73. },
  74. ObjectExpression: function(node, scope) {
  75. for (var i = 0; i < node.properties.length; ++i) {
  76. var prop = node.properties[i];
  77. if (prop.commentsBefore)
  78. interpretComments(prop, prop.commentsBefore, scope,
  79. node.objType.getProp(prop.key.name));
  80. }
  81. },
  82. CallExpression: function(node, scope) {
  83. if (node.commentsBefore && isDefinePropertyCall(node)) {
  84. var type = infer.expressionType({node: node.arguments[0], state: scope}).getObjType();
  85. if (type && type instanceof infer.Obj) {
  86. var prop = type.props[node.arguments[1].value];
  87. if (prop) interpretComments(node, node.commentsBefore, scope, prop);
  88. }
  89. }
  90. }
  91. }, infer.searchVisitor, scope);
  92. }
  93. function postLoadDef(data) {
  94. var defs = data["!typedef"];
  95. var cx = infer.cx(), orig = data["!name"];
  96. if (defs) for (var name in defs)
  97. cx.parent.jsdocTypedefs[name] =
  98. maybeInstance(infer.def.parse(defs[name], orig, name), name);
  99. }
  100. // COMMENT INTERPRETATION
  101. function interpretComments(node, comments, scope, aval, type) {
  102. jsdocInterpretComments(node, scope, aval, comments);
  103. var cx = infer.cx();
  104. if (!type && aval instanceof infer.AVal && aval.types.length) {
  105. type = aval.types[aval.types.length - 1];
  106. if (!(type instanceof infer.Obj) || type.origin != cx.curOrigin || type.doc)
  107. type = null;
  108. }
  109. var result = comments[comments.length - 1];
  110. if (cx.parent._docComment.fullDocs) {
  111. result = result.trim().replace(/\n[ \t]*\* ?/g, "\n");
  112. } else {
  113. var dot = result.search(/\.\s/);
  114. if (dot > 5) result = result.slice(0, dot + 1);
  115. result = result.trim().replace(/\s*\n\s*\*\s*|\s{1,}/g, " ");
  116. }
  117. result = result.replace(/^\s*\*+\s*/, "");
  118. if (aval instanceof infer.AVal) aval.doc = result;
  119. if (type) type.doc = result;
  120. }
  121. // Parses a subset of JSDoc-style comments in order to include the
  122. // explicitly defined types in the analysis.
  123. function skipSpace(str, pos) {
  124. while (/\s/.test(str.charAt(pos))) ++pos;
  125. return pos;
  126. }
  127. function isIdentifier(string) {
  128. if (!acorn.isIdentifierStart(string.charCodeAt(0))) return false;
  129. for (var i = 1; i < string.length; i++)
  130. if (!acorn.isIdentifierChar(string.charCodeAt(i))) return false;
  131. return true;
  132. }
  133. function parseLabelList(scope, str, pos, close) {
  134. var labels = [], types = [], madeUp = false;
  135. for (var first = true; ; first = false) {
  136. pos = skipSpace(str, pos);
  137. if (first && str.charAt(pos) == close) break;
  138. var colon = str.indexOf(":", pos);
  139. if (colon < 0) return null;
  140. var label = str.slice(pos, colon);
  141. if (!isIdentifier(label)) return null;
  142. labels.push(label);
  143. pos = colon + 1;
  144. var type = parseType(scope, str, pos);
  145. if (!type) return null;
  146. pos = type.end;
  147. madeUp = madeUp || type.madeUp;
  148. types.push(type.type);
  149. pos = skipSpace(str, pos);
  150. var next = str.charAt(pos);
  151. ++pos;
  152. if (next == close) break;
  153. if (next != ",") return null;
  154. }
  155. return {labels: labels, types: types, end: pos, madeUp: madeUp};
  156. }
  157. function parseType(scope, str, pos) {
  158. var type, union = false, madeUp = false;
  159. for (;;) {
  160. var inner = parseTypeInner(scope, str, pos);
  161. if (!inner) return null;
  162. madeUp = madeUp || inner.madeUp;
  163. if (union) inner.type.propagate(union);
  164. else type = inner.type;
  165. pos = skipSpace(str, inner.end);
  166. if (str.charAt(pos) != "|") break;
  167. pos++;
  168. if (!union) {
  169. union = new infer.AVal;
  170. type.propagate(union);
  171. type = union;
  172. }
  173. }
  174. var isOptional = false;
  175. if (str.charAt(pos) == "=") {
  176. ++pos;
  177. isOptional = true;
  178. }
  179. return {type: type, end: pos, isOptional: isOptional, madeUp: madeUp};
  180. }
  181. function parseTypeInner(scope, str, pos) {
  182. pos = skipSpace(str, pos);
  183. var type, madeUp = false;
  184. if (str.indexOf("function(", pos) == pos) {
  185. var args = parseLabelList(scope, str, pos + 9, ")"), ret = infer.ANull;
  186. if (!args) return null;
  187. pos = skipSpace(str, args.end);
  188. if (str.charAt(pos) == ":") {
  189. ++pos;
  190. var retType = parseType(scope, str, pos + 1);
  191. if (!retType) return null;
  192. pos = retType.end;
  193. ret = retType.type;
  194. madeUp = retType.madeUp;
  195. }
  196. type = new infer.Fn(null, infer.ANull, args.types, args.labels, ret);
  197. } else if (str.charAt(pos) == "[") {
  198. var inner = parseType(scope, str, pos + 1);
  199. if (!inner) return null;
  200. pos = skipSpace(str, inner.end);
  201. madeUp = inner.madeUp;
  202. if (str.charAt(pos) != "]") return null;
  203. ++pos;
  204. type = new infer.Arr(inner.type);
  205. } else if (str.charAt(pos) == "{") {
  206. var fields = parseLabelList(scope, str, pos + 1, "}");
  207. if (!fields) return null;
  208. type = new infer.Obj(true);
  209. for (var i = 0; i < fields.types.length; ++i) {
  210. var field = type.defProp(fields.labels[i]);
  211. field.initializer = true;
  212. fields.types[i].propagate(field);
  213. }
  214. pos = fields.end;
  215. madeUp = fields.madeUp;
  216. } else if (str.charAt(pos) == "(") {
  217. var inner = parseType(scope, str, pos + 1);
  218. if (!inner) return null;
  219. pos = skipSpace(str, inner.end);
  220. if (str.charAt(pos) != ")") return null;
  221. ++pos;
  222. type = inner.type;
  223. } else {
  224. var start = pos;
  225. if (!acorn.isIdentifierStart(str.charCodeAt(pos))) return null;
  226. while (acorn.isIdentifierChar(str.charCodeAt(pos))) ++pos;
  227. if (start == pos) return null;
  228. var word = str.slice(start, pos);
  229. if (/^(number|integer)$/i.test(word)) type = infer.cx().num;
  230. else if (/^bool(ean)?$/i.test(word)) type = infer.cx().bool;
  231. else if (/^string$/i.test(word)) type = infer.cx().str;
  232. else if (/^(null|undefined)$/i.test(word)) type = infer.ANull;
  233. else if (/^array$/i.test(word)) {
  234. var inner = null;
  235. if (str.charAt(pos) == "." && str.charAt(pos + 1) == "<") {
  236. var inAngles = parseType(scope, str, pos + 2);
  237. if (!inAngles) return null;
  238. pos = skipSpace(str, inAngles.end);
  239. madeUp = inAngles.madeUp;
  240. if (str.charAt(pos++) != ">") return null;
  241. inner = inAngles.type;
  242. }
  243. type = new infer.Arr(inner);
  244. } else if (/^object$/i.test(word)) {
  245. type = new infer.Obj(true);
  246. if (str.charAt(pos) == "." && str.charAt(pos + 1) == "<") {
  247. var key = parseType(scope, str, pos + 2);
  248. if (!key) return null;
  249. pos = skipSpace(str, key.end);
  250. if (str.charAt(pos++) != ",") return null;
  251. var val = parseType(scope, str, pos);
  252. if (!val) return null;
  253. pos = skipSpace(str, val.end);
  254. madeUp = key.madeUp || val.madeUp;
  255. if (str.charAt(pos++) != ">") return null;
  256. val.type.propagate(type.defProp("<i>"));
  257. }
  258. } else {
  259. while (str.charCodeAt(pos) == 46 ||
  260. acorn.isIdentifierChar(str.charCodeAt(pos))) ++pos;
  261. var path = str.slice(start, pos);
  262. var cx = infer.cx(), defs = cx.parent && cx.parent.jsdocTypedefs, found;
  263. if (defs && (path in defs)) {
  264. type = defs[path];
  265. } else if (found = infer.def.parsePath(path, scope).getObjType()) {
  266. type = maybeInstance(found, path);
  267. } else {
  268. if (!cx.jsdocPlaceholders) cx.jsdocPlaceholders = Object.create(null);
  269. if (!(path in cx.jsdocPlaceholders))
  270. type = cx.jsdocPlaceholders[path] = new infer.Obj(null, path);
  271. else
  272. type = cx.jsdocPlaceholders[path];
  273. madeUp = true;
  274. }
  275. }
  276. }
  277. return {type: type, end: pos, madeUp: madeUp};
  278. }
  279. function maybeInstance(type, path) {
  280. if (type instanceof infer.Fn && /^[A-Z]/.test(path)) {
  281. var proto = type.getProp("prototype").getObjType();
  282. if (proto instanceof infer.Obj) return infer.getInstance(proto);
  283. }
  284. return type;
  285. }
  286. function parseTypeOuter(scope, str, pos) {
  287. pos = skipSpace(str, pos || 0);
  288. if (str.charAt(pos) != "{") return null;
  289. var result = parseType(scope, str, pos + 1);
  290. if (!result) return null;
  291. var end = skipSpace(str, result.end);
  292. if (str.charAt(end) != "}") return null;
  293. result.end = end + 1;
  294. return result;
  295. }
  296. function jsdocInterpretComments(node, scope, aval, comments) {
  297. var type, args, ret, foundOne, self, parsed;
  298. for (var i = 0; i < comments.length; ++i) {
  299. var comment = comments[i];
  300. var decl = /(?:\n|\$|\*)\s*@(type|param|arg(?:ument)?|returns?|this)\s+(.*)/g, m;
  301. while (m = decl.exec(comment)) {
  302. if (m[1] == "this" && (parsed = parseType(scope, m[2], 0))) {
  303. self = parsed;
  304. foundOne = true;
  305. continue;
  306. }
  307. if (!(parsed = parseTypeOuter(scope, m[2]))) continue;
  308. foundOne = true;
  309. switch(m[1]) {
  310. case "returns": case "return":
  311. ret = parsed; break;
  312. case "type":
  313. type = parsed; break;
  314. case "param": case "arg": case "argument":
  315. var name = m[2].slice(parsed.end).match(/^\s*(\[?)\s*([^\]\s=]+)\s*(?:=[^\]]+\s*)?(\]?).*/);
  316. if (!name) continue;
  317. var argname = name[2] + (parsed.isOptional || (name[1] === '[' && name[3] === ']') ? "?" : "");
  318. (args || (args = Object.create(null)))[argname] = parsed;
  319. break;
  320. }
  321. }
  322. }
  323. if (foundOne) applyType(type, self, args, ret, node, aval);
  324. };
  325. function jsdocParseTypedefs(text, scope) {
  326. var cx = infer.cx();
  327. var re = /\s@typedef\s+(.*)/g, m;
  328. while (m = re.exec(text)) {
  329. var parsed = parseTypeOuter(scope, m[1]);
  330. var name = parsed && m[1].slice(parsed.end).match(/^\s*(\S+)/);
  331. if (name)
  332. cx.parent.jsdocTypedefs[name[1]] = parsed.type;
  333. }
  334. }
  335. function propagateWithWeight(type, target) {
  336. var weight = infer.cx().parent._docComment.weight;
  337. type.type.propagate(target, weight || (type.madeUp ? WG_MADEUP : undefined));
  338. }
  339. function applyType(type, self, args, ret, node, aval) {
  340. var fn;
  341. if (node.type == "VariableDeclaration") {
  342. var decl = node.declarations[0];
  343. if (decl.init && decl.init.type == "FunctionExpression") fn = decl.init.body.scope.fnType;
  344. } else if (node.type == "FunctionDeclaration") {
  345. fn = node.body.scope.fnType;
  346. } else if (node.type == "AssignmentExpression") {
  347. if (node.right.type == "FunctionExpression")
  348. fn = node.right.body.scope.fnType;
  349. } else if (node.type == "CallExpression") {
  350. } else { // An object property
  351. if (node.value.type == "FunctionExpression") fn = node.value.body.scope.fnType;
  352. }
  353. if (fn && (args || ret || self)) {
  354. if (args) for (var i = 0; i < fn.argNames.length; ++i) {
  355. var name = fn.argNames[i], known = args[name];
  356. if (!known && (known = args[name + "?"]))
  357. fn.argNames[i] += "?";
  358. if (known) propagateWithWeight(known, fn.args[i]);
  359. }
  360. if (ret) propagateWithWeight(ret, fn.retval);
  361. if (self) propagateWithWeight(self, fn.self);
  362. } else if (type) {
  363. propagateWithWeight(type, aval);
  364. }
  365. };
  366. });