tern.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993
  1. // The Tern server object
  2. // A server is a stateful object that manages the analysis for a
  3. // project, and defines an interface for querying the code in the
  4. // project.
  5. (function(root, mod) {
  6. if (typeof exports == "object" && typeof module == "object") // CommonJS
  7. return mod(exports, require("./infer"), require("./signal"),
  8. require("acorn"), require("acorn/dist/walk"));
  9. if (typeof define == "function" && define.amd) // AMD
  10. return define(["exports", "./infer", "./signal", "acorn/dist/acorn", "acorn/dist/walk"], mod);
  11. mod(root.tern || (root.tern = {}), tern, tern.signal, acorn, acorn.walk); // Plain browser env
  12. })(this, function(exports, infer, signal, acorn, walk) {
  13. "use strict";
  14. var plugins = Object.create(null);
  15. exports.registerPlugin = function(name, init) { plugins[name] = init; };
  16. var defaultOptions = exports.defaultOptions = {
  17. debug: false,
  18. async: false,
  19. getFile: function(_f, c) { if (this.async) c(null, null); },
  20. defs: [],
  21. plugins: {},
  22. fetchTimeout: 1000,
  23. dependencyBudget: 20000,
  24. reuseInstances: true,
  25. stripCRs: false
  26. };
  27. var queryTypes = {
  28. completions: {
  29. takesFile: true,
  30. run: findCompletions
  31. },
  32. properties: {
  33. run: findProperties
  34. },
  35. type: {
  36. takesFile: true,
  37. run: findTypeAt
  38. },
  39. documentation: {
  40. takesFile: true,
  41. run: findDocs
  42. },
  43. definition: {
  44. takesFile: true,
  45. run: findDef
  46. },
  47. refs: {
  48. takesFile: true,
  49. fullFile: true,
  50. run: findRefs
  51. },
  52. rename: {
  53. takesFile: true,
  54. fullFile: true,
  55. run: buildRename
  56. },
  57. files: {
  58. run: listFiles
  59. }
  60. };
  61. exports.defineQueryType = function(name, desc) { queryTypes[name] = desc; };
  62. function File(name, parent) {
  63. this.name = name;
  64. this.parent = parent;
  65. this.scope = this.text = this.ast = this.lineOffsets = null;
  66. }
  67. File.prototype.asLineChar = function(pos) { return asLineChar(this, pos); };
  68. function updateText(file, text, srv) {
  69. file.text = srv.options.stripCRs ? text.replace(/\r\n/g, "\n") : text;
  70. infer.withContext(srv.cx, function() {
  71. file.ast = infer.parse(file.text, srv.passes, {directSourceFile: file, allowReturnOutsideFunction: true});
  72. });
  73. file.lineOffsets = null;
  74. }
  75. var Server = exports.Server = function(options) {
  76. this.cx = null;
  77. this.options = options || {};
  78. for (var o in defaultOptions) if (!options.hasOwnProperty(o))
  79. options[o] = defaultOptions[o];
  80. this.handlers = Object.create(null);
  81. this.files = [];
  82. this.fileMap = Object.create(null);
  83. this.needsPurge = [];
  84. this.budgets = Object.create(null);
  85. this.uses = 0;
  86. this.pending = 0;
  87. this.asyncError = null;
  88. this.passes = Object.create(null);
  89. this.defs = options.defs.slice(0);
  90. for (var plugin in options.plugins) if (options.plugins.hasOwnProperty(plugin) && plugin in plugins) {
  91. var init = plugins[plugin](this, options.plugins[plugin]);
  92. if (init && init.defs) {
  93. if (init.loadFirst) this.defs.unshift(init.defs);
  94. else this.defs.push(init.defs);
  95. }
  96. if (init && init.passes) for (var type in init.passes) if (init.passes.hasOwnProperty(type))
  97. (this.passes[type] || (this.passes[type] = [])).push(init.passes[type]);
  98. }
  99. this.reset();
  100. };
  101. Server.prototype = signal.mixin({
  102. addFile: function(name, /*optional*/ text, parent) {
  103. // Don't crash when sloppy plugins pass non-existent parent ids
  104. if (parent && !(parent in this.fileMap)) parent = null;
  105. ensureFile(this, name, parent, text);
  106. },
  107. delFile: function(name) {
  108. var file = this.findFile(name);
  109. if (file) {
  110. this.needsPurge.push(file.name);
  111. this.files.splice(this.files.indexOf(file), 1);
  112. delete this.fileMap[name];
  113. }
  114. },
  115. reset: function() {
  116. this.signal("reset");
  117. this.cx = new infer.Context(this.defs, this);
  118. this.uses = 0;
  119. this.budgets = Object.create(null);
  120. for (var i = 0; i < this.files.length; ++i) {
  121. var file = this.files[i];
  122. file.scope = null;
  123. }
  124. },
  125. request: function(doc, c) {
  126. var inv = invalidDoc(doc);
  127. if (inv) return c(inv);
  128. var self = this;
  129. doRequest(this, doc, function(err, data) {
  130. c(err, data);
  131. if (self.uses > 40) {
  132. self.reset();
  133. analyzeAll(self, null, function(){});
  134. }
  135. });
  136. },
  137. findFile: function(name) {
  138. return this.fileMap[name];
  139. },
  140. flush: function(c) {
  141. var cx = this.cx;
  142. analyzeAll(this, null, function(err) {
  143. if (err) return c(err);
  144. infer.withContext(cx, c);
  145. });
  146. },
  147. startAsyncAction: function() {
  148. ++this.pending;
  149. },
  150. finishAsyncAction: function(err) {
  151. if (err) this.asyncError = err;
  152. if (--this.pending === 0) this.signal("everythingFetched");
  153. }
  154. });
  155. function doRequest(srv, doc, c) {
  156. if (doc.query && !queryTypes.hasOwnProperty(doc.query.type))
  157. return c("No query type '" + doc.query.type + "' defined");
  158. var query = doc.query;
  159. // Respond as soon as possible when this just uploads files
  160. if (!query) c(null, {});
  161. var files = doc.files || [];
  162. if (files.length) ++srv.uses;
  163. for (var i = 0; i < files.length; ++i) {
  164. var file = files[i];
  165. if (file.type == "delete")
  166. srv.delFile(file.name);
  167. else
  168. ensureFile(srv, file.name, null, file.type == "full" ? file.text : null);
  169. }
  170. var timeBudget = typeof doc.timeout == "number" ? [doc.timeout] : null;
  171. if (!query) {
  172. analyzeAll(srv, timeBudget, function(){});
  173. return;
  174. }
  175. var queryType = queryTypes[query.type];
  176. if (queryType.takesFile) {
  177. if (typeof query.file != "string") return c(".query.file must be a string");
  178. if (!/^#/.test(query.file)) ensureFile(srv, query.file, null);
  179. }
  180. analyzeAll(srv, timeBudget, function(err) {
  181. if (err) return c(err);
  182. var file = queryType.takesFile && resolveFile(srv, files, query.file);
  183. if (queryType.fullFile && file.type == "part")
  184. return c("Can't run a " + query.type + " query on a file fragment");
  185. function run() {
  186. var result;
  187. try {
  188. result = queryType.run(srv, query, file);
  189. } catch (e) {
  190. if (srv.options.debug && e.name != "TernError") console.error(e.stack);
  191. return c(e);
  192. }
  193. c(null, result);
  194. }
  195. infer.withContext(srv.cx, timeBudget ? function() { infer.withTimeout(timeBudget[0], run); } : run);
  196. });
  197. }
  198. function analyzeFile(srv, file) {
  199. infer.withContext(srv.cx, function() {
  200. file.scope = srv.cx.topScope;
  201. srv.signal("beforeLoad", file);
  202. infer.analyze(file.ast, file.name, file.scope, srv.passes);
  203. srv.signal("afterLoad", file);
  204. });
  205. return file;
  206. }
  207. function ensureFile(srv, name, parent, text) {
  208. var known = srv.findFile(name);
  209. if (known) {
  210. if (text != null) {
  211. if (known.scope) {
  212. srv.needsPurge.push(name);
  213. known.scope = null;
  214. }
  215. updateText(known, text, srv);
  216. }
  217. if (parentDepth(srv, known.parent) > parentDepth(srv, parent)) {
  218. known.parent = parent;
  219. if (known.excluded) known.excluded = null;
  220. }
  221. return;
  222. }
  223. var file = new File(name, parent);
  224. srv.files.push(file);
  225. srv.fileMap[name] = file;
  226. if (text != null) {
  227. updateText(file, text, srv);
  228. } else if (srv.options.async) {
  229. srv.startAsyncAction();
  230. srv.options.getFile(name, function(err, text) {
  231. updateText(file, text || "", srv);
  232. srv.finishAsyncAction(err);
  233. });
  234. } else {
  235. updateText(file, srv.options.getFile(name) || "", srv);
  236. }
  237. }
  238. function fetchAll(srv, c) {
  239. var done = true, returned = false;
  240. srv.files.forEach(function(file) {
  241. if (file.text != null) return;
  242. if (srv.options.async) {
  243. done = false;
  244. srv.options.getFile(file.name, function(err, text) {
  245. if (err && !returned) { returned = true; return c(err); }
  246. updateText(file, text || "", srv);
  247. fetchAll(srv, c);
  248. });
  249. } else {
  250. try {
  251. updateText(file, srv.options.getFile(file.name) || "", srv);
  252. } catch (e) { return c(e); }
  253. }
  254. });
  255. if (done) c();
  256. }
  257. function waitOnFetch(srv, timeBudget, c) {
  258. var done = function() {
  259. srv.off("everythingFetched", done);
  260. clearTimeout(timeout);
  261. analyzeAll(srv, timeBudget, c);
  262. };
  263. srv.on("everythingFetched", done);
  264. var timeout = setTimeout(done, srv.options.fetchTimeout);
  265. }
  266. function analyzeAll(srv, timeBudget, c) {
  267. if (srv.pending) return waitOnFetch(srv, timeBudget, c);
  268. var e = srv.fetchError;
  269. if (e) { srv.fetchError = null; return c(e); }
  270. if (srv.needsPurge.length > 0) infer.withContext(srv.cx, function() {
  271. infer.purge(srv.needsPurge);
  272. srv.needsPurge.length = 0;
  273. });
  274. var done = true;
  275. // The second inner loop might add new files. The outer loop keeps
  276. // repeating both inner loops until all files have been looked at.
  277. for (var i = 0; i < srv.files.length;) {
  278. var toAnalyze = [];
  279. for (; i < srv.files.length; ++i) {
  280. var file = srv.files[i];
  281. if (file.text == null) done = false;
  282. else if (file.scope == null && !file.excluded) toAnalyze.push(file);
  283. }
  284. toAnalyze.sort(function(a, b) {
  285. return parentDepth(srv, a.parent) - parentDepth(srv, b.parent);
  286. });
  287. for (var j = 0; j < toAnalyze.length; j++) {
  288. var file = toAnalyze[j];
  289. if (file.parent && !chargeOnBudget(srv, file)) {
  290. file.excluded = true;
  291. } else if (timeBudget) {
  292. var startTime = +new Date;
  293. infer.withTimeout(timeBudget[0], function() { analyzeFile(srv, file); });
  294. timeBudget[0] -= +new Date - startTime;
  295. } else {
  296. analyzeFile(srv, file);
  297. }
  298. }
  299. }
  300. if (done) c();
  301. else waitOnFetch(srv, timeBudget, c);
  302. }
  303. function firstLine(str) {
  304. var end = str.indexOf("\n");
  305. if (end < 0) return str;
  306. return str.slice(0, end);
  307. }
  308. function findMatchingPosition(line, file, near) {
  309. var pos = Math.max(0, near - 500), closest = null;
  310. if (!/^\s*$/.test(line)) for (;;) {
  311. var found = file.indexOf(line, pos);
  312. if (found < 0 || found > near + 500) break;
  313. if (closest == null || Math.abs(closest - near) > Math.abs(found - near))
  314. closest = found;
  315. pos = found + line.length;
  316. }
  317. return closest;
  318. }
  319. function scopeDepth(s) {
  320. for (var i = 0; s; ++i, s = s.prev) {}
  321. return i;
  322. }
  323. function ternError(msg) {
  324. var err = new Error(msg);
  325. err.name = "TernError";
  326. return err;
  327. }
  328. function resolveFile(srv, localFiles, name) {
  329. var isRef = name.match(/^#(\d+)$/);
  330. if (!isRef) return srv.findFile(name);
  331. var file = localFiles[isRef[1]];
  332. if (!file || file.type == "delete") throw ternError("Reference to unknown file " + name);
  333. if (file.type == "full") return srv.findFile(file.name);
  334. // This is a partial file
  335. var realFile = file.backing = srv.findFile(file.name);
  336. var offset;
  337. file.offset = offset = resolvePos(realFile, file.offsetLines == null ? file.offset : {line: file.offsetLines, ch: 0}, true);
  338. var line = firstLine(file.text);
  339. var foundPos = findMatchingPosition(line, realFile.text, offset);
  340. var pos = foundPos == null ? Math.max(0, realFile.text.lastIndexOf("\n", offset)) : foundPos;
  341. var inObject, atFunction;
  342. infer.withContext(srv.cx, function() {
  343. infer.purge(file.name, pos, pos + file.text.length);
  344. var text = file.text, m;
  345. if (m = text.match(/(?:"([^"]*)"|([\w$]+))\s*:\s*function\b/)) {
  346. var objNode = walk.findNodeAround(file.backing.ast, pos, "ObjectExpression");
  347. if (objNode && objNode.node.objType)
  348. inObject = {type: objNode.node.objType, prop: m[2] || m[1]};
  349. }
  350. if (foundPos && (m = line.match(/^(.*?)\bfunction\b/))) {
  351. var cut = m[1].length, white = "";
  352. for (var i = 0; i < cut; ++i) white += " ";
  353. text = white + text.slice(cut);
  354. atFunction = true;
  355. }
  356. var scopeStart = infer.scopeAt(realFile.ast, pos, realFile.scope);
  357. var scopeEnd = infer.scopeAt(realFile.ast, pos + text.length, realFile.scope);
  358. var scope = file.scope = scopeDepth(scopeStart) < scopeDepth(scopeEnd) ? scopeEnd : scopeStart;
  359. file.ast = infer.parse(text, srv.passes, {directSourceFile: file, allowReturnOutsideFunction: true});
  360. infer.analyze(file.ast, file.name, scope, srv.passes);
  361. // This is a kludge to tie together the function types (if any)
  362. // outside and inside of the fragment, so that arguments and
  363. // return values have some information known about them.
  364. tieTogether: if (inObject || atFunction) {
  365. var newInner = infer.scopeAt(file.ast, line.length, scopeStart);
  366. if (!newInner.fnType) break tieTogether;
  367. if (inObject) {
  368. var prop = inObject.type.getProp(inObject.prop);
  369. prop.addType(newInner.fnType);
  370. } else if (atFunction) {
  371. var inner = infer.scopeAt(realFile.ast, pos + line.length, realFile.scope);
  372. if (inner == scopeStart || !inner.fnType) break tieTogether;
  373. var fOld = inner.fnType, fNew = newInner.fnType;
  374. if (!fNew || (fNew.name != fOld.name && fOld.name)) break tieTogether;
  375. for (var i = 0, e = Math.min(fOld.args.length, fNew.args.length); i < e; ++i)
  376. fOld.args[i].propagate(fNew.args[i]);
  377. fOld.self.propagate(fNew.self);
  378. fNew.retval.propagate(fOld.retval);
  379. }
  380. }
  381. });
  382. return file;
  383. }
  384. // Budget management
  385. function astSize(node) {
  386. var size = 0;
  387. walk.simple(node, {Expression: function() { ++size; }});
  388. return size;
  389. }
  390. function parentDepth(srv, parent) {
  391. var depth = 0;
  392. while (parent) {
  393. parent = srv.findFile(parent).parent;
  394. ++depth;
  395. }
  396. return depth;
  397. }
  398. function budgetName(srv, file) {
  399. for (;;) {
  400. var parent = srv.findFile(file.parent);
  401. if (!parent.parent) break;
  402. file = parent;
  403. }
  404. return file.name;
  405. }
  406. function chargeOnBudget(srv, file) {
  407. var bName = budgetName(srv, file);
  408. var size = astSize(file.ast);
  409. var known = srv.budgets[bName];
  410. if (known == null)
  411. known = srv.budgets[bName] = srv.options.dependencyBudget;
  412. if (known < size) return false;
  413. srv.budgets[bName] = known - size;
  414. return true;
  415. }
  416. // Query helpers
  417. function isPosition(val) {
  418. return typeof val == "number" || typeof val == "object" &&
  419. typeof val.line == "number" && typeof val.ch == "number";
  420. }
  421. // Baseline query document validation
  422. function invalidDoc(doc) {
  423. if (doc.query) {
  424. if (typeof doc.query.type != "string") return ".query.type must be a string";
  425. if (doc.query.start && !isPosition(doc.query.start)) return ".query.start must be a position";
  426. if (doc.query.end && !isPosition(doc.query.end)) return ".query.end must be a position";
  427. }
  428. if (doc.files) {
  429. if (!Array.isArray(doc.files)) return "Files property must be an array";
  430. for (var i = 0; i < doc.files.length; ++i) {
  431. var file = doc.files[i];
  432. if (typeof file != "object") return ".files[n] must be objects";
  433. else if (typeof file.name != "string") return ".files[n].name must be a string";
  434. else if (file.type == "delete") continue;
  435. else if (typeof file.text != "string") return ".files[n].text must be a string";
  436. else if (file.type == "part") {
  437. if (!isPosition(file.offset) && typeof file.offsetLines != "number")
  438. return ".files[n].offset must be a position";
  439. } else if (file.type != "full") return ".files[n].type must be \"full\" or \"part\"";
  440. }
  441. }
  442. }
  443. var offsetSkipLines = 25;
  444. function findLineStart(file, line) {
  445. var text = file.text, offsets = file.lineOffsets || (file.lineOffsets = [0]);
  446. var pos = 0, curLine = 0;
  447. var storePos = Math.min(Math.floor(line / offsetSkipLines), offsets.length - 1);
  448. var pos = offsets[storePos], curLine = storePos * offsetSkipLines;
  449. while (curLine < line) {
  450. ++curLine;
  451. pos = text.indexOf("\n", pos) + 1;
  452. if (pos === 0) return null;
  453. if (curLine % offsetSkipLines === 0) offsets.push(pos);
  454. }
  455. return pos;
  456. }
  457. var resolvePos = exports.resolvePos = function(file, pos, tolerant) {
  458. if (typeof pos != "number") {
  459. var lineStart = findLineStart(file, pos.line);
  460. if (lineStart == null) {
  461. if (tolerant) pos = file.text.length;
  462. else throw ternError("File doesn't contain a line " + pos.line);
  463. } else {
  464. pos = lineStart + pos.ch;
  465. }
  466. }
  467. if (pos > file.text.length) {
  468. if (tolerant) pos = file.text.length;
  469. else throw ternError("Position " + pos + " is outside of file.");
  470. }
  471. return pos;
  472. };
  473. function asLineChar(file, pos) {
  474. if (!file) return {line: 0, ch: 0};
  475. var offsets = file.lineOffsets || (file.lineOffsets = [0]);
  476. var text = file.text, line, lineStart;
  477. for (var i = offsets.length - 1; i >= 0; --i) if (offsets[i] <= pos) {
  478. line = i * offsetSkipLines;
  479. lineStart = offsets[i];
  480. }
  481. for (;;) {
  482. var eol = text.indexOf("\n", lineStart);
  483. if (eol >= pos || eol < 0) break;
  484. lineStart = eol + 1;
  485. ++line;
  486. }
  487. return {line: line, ch: pos - lineStart};
  488. }
  489. var outputPos = exports.outputPos = function(query, file, pos) {
  490. if (query.lineCharPositions) {
  491. var out = asLineChar(file, pos);
  492. if (file.type == "part")
  493. out.line += file.offsetLines != null ? file.offsetLines : asLineChar(file.backing, file.offset).line;
  494. return out;
  495. } else {
  496. return pos + (file.type == "part" ? file.offset : 0);
  497. }
  498. };
  499. // Delete empty fields from result objects
  500. function clean(obj) {
  501. for (var prop in obj) if (obj[prop] == null) delete obj[prop];
  502. return obj;
  503. }
  504. function maybeSet(obj, prop, val) {
  505. if (val != null) obj[prop] = val;
  506. }
  507. // Built-in query types
  508. function compareCompletions(a, b) {
  509. if (typeof a != "string") { a = a.name; b = b.name; }
  510. var aUp = /^[A-Z]/.test(a), bUp = /^[A-Z]/.test(b);
  511. if (aUp == bUp) return a < b ? -1 : a == b ? 0 : 1;
  512. else return aUp ? 1 : -1;
  513. }
  514. function isStringAround(node, start, end) {
  515. return node.type == "Literal" && typeof node.value == "string" &&
  516. node.start == start - 1 && node.end <= end + 1;
  517. }
  518. function pointInProp(objNode, point) {
  519. for (var i = 0; i < objNode.properties.length; i++) {
  520. var curProp = objNode.properties[i];
  521. if (curProp.key.start <= point && curProp.key.end >= point)
  522. return curProp;
  523. }
  524. }
  525. var jsKeywords = ("break do instanceof typeof case else new var " +
  526. "catch finally return void continue for switch while debugger " +
  527. "function this with default if throw delete in try").split(" ");
  528. function findCompletions(srv, query, file) {
  529. if (query.end == null) throw ternError("missing .query.end field");
  530. if (srv.passes.completion) for (var i = 0; i < srv.passes.completion.length; i++) {
  531. var result = srv.passes.completion[i](file, query);
  532. if (result) return result;
  533. }
  534. var wordStart = resolvePos(file, query.end), wordEnd = wordStart, text = file.text;
  535. while (wordStart && acorn.isIdentifierChar(text.charCodeAt(wordStart - 1))) --wordStart;
  536. if (query.expandWordForward !== false)
  537. while (wordEnd < text.length && acorn.isIdentifierChar(text.charCodeAt(wordEnd))) ++wordEnd;
  538. var word = text.slice(wordStart, wordEnd), completions = [], ignoreObj;
  539. if (query.caseInsensitive) word = word.toLowerCase();
  540. var wrapAsObjs = query.types || query.depths || query.docs || query.urls || query.origins;
  541. function gather(prop, obj, depth, addInfo) {
  542. // 'hasOwnProperty' and such are usually just noise, leave them
  543. // out when no prefix is provided.
  544. if ((objLit || query.omitObjectPrototype !== false) && obj == srv.cx.protos.Object && !word) return;
  545. if (query.filter !== false && word &&
  546. (query.caseInsensitive ? prop.toLowerCase() : prop).indexOf(word) !== 0) return;
  547. if (ignoreObj && ignoreObj.props[prop]) return;
  548. for (var i = 0; i < completions.length; ++i) {
  549. var c = completions[i];
  550. if ((wrapAsObjs ? c.name : c) == prop) return;
  551. }
  552. var rec = wrapAsObjs ? {name: prop} : prop;
  553. completions.push(rec);
  554. if (obj && (query.types || query.docs || query.urls || query.origins)) {
  555. var val = obj.props[prop];
  556. infer.resetGuessing();
  557. var type = val.getType();
  558. rec.guess = infer.didGuess();
  559. if (query.types)
  560. rec.type = infer.toString(val);
  561. if (query.docs)
  562. maybeSet(rec, "doc", val.doc || type && type.doc);
  563. if (query.urls)
  564. maybeSet(rec, "url", val.url || type && type.url);
  565. if (query.origins)
  566. maybeSet(rec, "origin", val.origin || type && type.origin);
  567. }
  568. if (query.depths) rec.depth = depth;
  569. if (wrapAsObjs && addInfo) addInfo(rec);
  570. }
  571. var hookname, prop, objType, isKey;
  572. var exprAt = infer.findExpressionAround(file.ast, null, wordStart, file.scope);
  573. var memberExpr, objLit;
  574. // Decide whether this is an object property, either in a member
  575. // expression or an object literal.
  576. if (exprAt) {
  577. if (exprAt.node.type == "MemberExpression" && exprAt.node.object.end < wordStart) {
  578. memberExpr = exprAt;
  579. } else if (isStringAround(exprAt.node, wordStart, wordEnd)) {
  580. var parent = infer.parentNode(exprAt.node, file.ast);
  581. if (parent.type == "MemberExpression" && parent.property == exprAt.node)
  582. memberExpr = {node: parent, state: exprAt.state};
  583. } else if (exprAt.node.type == "ObjectExpression") {
  584. var objProp = pointInProp(exprAt.node, wordEnd);
  585. if (objProp) {
  586. objLit = exprAt;
  587. prop = isKey = objProp.key.name;
  588. } else if (!word && !/:\s*$/.test(file.text.slice(0, wordStart))) {
  589. objLit = exprAt;
  590. prop = isKey = true;
  591. }
  592. }
  593. }
  594. if (objLit) {
  595. // Since we can't use the type of the literal itself to complete
  596. // its properties (it doesn't contain the information we need),
  597. // we have to try asking the surrounding expression for type info.
  598. objType = infer.typeFromContext(file.ast, objLit);
  599. ignoreObj = objLit.node.objType;
  600. } else if (memberExpr) {
  601. prop = memberExpr.node.property;
  602. prop = prop.type == "Literal" ? prop.value.slice(1) : prop.name;
  603. memberExpr.node = memberExpr.node.object;
  604. objType = infer.expressionType(memberExpr);
  605. } else if (text.charAt(wordStart - 1) == ".") {
  606. var pathStart = wordStart - 1;
  607. while (pathStart && (text.charAt(pathStart - 1) == "." || acorn.isIdentifierChar(text.charCodeAt(pathStart - 1)))) pathStart--;
  608. var path = text.slice(pathStart, wordStart - 1);
  609. if (path) {
  610. objType = infer.def.parsePath(path, file.scope).getObjType();
  611. prop = word;
  612. }
  613. }
  614. if (prop != null) {
  615. srv.cx.completingProperty = prop;
  616. if (objType) infer.forAllPropertiesOf(objType, gather);
  617. if (!completions.length && query.guess !== false && objType && objType.guessProperties)
  618. objType.guessProperties(function(p, o, d) {if (p != prop && p != "✖") gather(p, o, d);});
  619. if (!completions.length && word.length >= 2 && query.guess !== false)
  620. for (var prop in srv.cx.props) gather(prop, srv.cx.props[prop][0], 0);
  621. hookname = "memberCompletion";
  622. } else {
  623. infer.forAllLocalsAt(file.ast, wordStart, file.scope, gather);
  624. if (query.includeKeywords) jsKeywords.forEach(function(kw) {
  625. gather(kw, null, 0, function(rec) { rec.isKeyword = true; });
  626. });
  627. hookname = "variableCompletion";
  628. }
  629. if (srv.passes[hookname])
  630. srv.passes[hookname].forEach(function(hook) {hook(file, wordStart, wordEnd, gather);});
  631. if (query.sort !== false) completions.sort(compareCompletions);
  632. srv.cx.completingProperty = null;
  633. return {start: outputPos(query, file, wordStart),
  634. end: outputPos(query, file, wordEnd),
  635. isProperty: !!prop,
  636. isObjectKey: !!isKey,
  637. completions: completions};
  638. }
  639. function findProperties(srv, query) {
  640. var prefix = query.prefix, found = [];
  641. for (var prop in srv.cx.props)
  642. if (prop != "<i>" && (!prefix || prop.indexOf(prefix) === 0)) found.push(prop);
  643. if (query.sort !== false) found.sort(compareCompletions);
  644. return {completions: found};
  645. }
  646. var findExpr = exports.findQueryExpr = function(file, query, wide) {
  647. if (query.end == null) throw ternError("missing .query.end field");
  648. if (query.variable) {
  649. var scope = infer.scopeAt(file.ast, resolvePos(file, query.end), file.scope);
  650. return {node: {type: "Identifier", name: query.variable, start: query.end, end: query.end + 1},
  651. state: scope};
  652. } else {
  653. var start = query.start && resolvePos(file, query.start), end = resolvePos(file, query.end);
  654. var expr = infer.findExpressionAt(file.ast, start, end, file.scope);
  655. if (expr) return expr;
  656. expr = infer.findExpressionAround(file.ast, start, end, file.scope);
  657. if (expr && (expr.node.type == "ObjectExpression" || wide ||
  658. (start == null ? end : start) - expr.node.start < 20 || expr.node.end - end < 20))
  659. return expr;
  660. return null;
  661. }
  662. };
  663. function findExprOrThrow(file, query, wide) {
  664. var expr = findExpr(file, query, wide);
  665. if (expr) return expr;
  666. throw ternError("No expression at the given position.");
  667. }
  668. function ensureObj(tp) {
  669. if (!tp || !(tp = tp.getType()) || !(tp instanceof infer.Obj)) return null;
  670. return tp;
  671. }
  672. function findExprType(srv, query, file, expr) {
  673. var type;
  674. if (expr) {
  675. infer.resetGuessing();
  676. type = infer.expressionType(expr);
  677. }
  678. if (srv.passes["typeAt"]) {
  679. var pos = resolvePos(file, query.end);
  680. srv.passes["typeAt"].forEach(function(hook) {
  681. type = hook(file, pos, expr, type);
  682. });
  683. }
  684. if (!type) throw ternError("No type found at the given position.");
  685. var objProp;
  686. if (expr.node.type == "ObjectExpression" && query.end != null &&
  687. (objProp = pointInProp(expr.node, resolvePos(file, query.end)))) {
  688. var name = objProp.key.name;
  689. var fromCx = ensureObj(infer.typeFromContext(file.ast, expr));
  690. if (fromCx && fromCx.hasProp(name)) {
  691. type = fromCx.hasProp(name);
  692. } else {
  693. var fromLocal = ensureObj(type);
  694. if (fromLocal && fromLocal.hasProp(name))
  695. type = fromLocal.hasProp(name);
  696. }
  697. }
  698. return type;
  699. };
  700. function findTypeAt(srv, query, file) {
  701. var expr = findExpr(file, query), exprName;
  702. var type = findExprType(srv, query, file, expr), exprType = type;
  703. if (query.preferFunction)
  704. type = type.getFunctionType() || type.getType();
  705. else
  706. type = type.getType();
  707. if (expr) {
  708. if (expr.node.type == "Identifier")
  709. exprName = expr.node.name;
  710. else if (expr.node.type == "MemberExpression" && !expr.node.computed)
  711. exprName = expr.node.property.name;
  712. }
  713. if (query.depth != null && typeof query.depth != "number")
  714. throw ternError(".query.depth must be a number");
  715. var result = {guess: infer.didGuess(),
  716. type: infer.toString(exprType, query.depth),
  717. name: type && type.name,
  718. exprName: exprName};
  719. if (type) storeTypeDocs(type, result);
  720. if (!result.doc && exprType.doc) result.doc = exprType.doc;
  721. return clean(result);
  722. }
  723. function findDocs(srv, query, file) {
  724. var expr = findExpr(file, query);
  725. var type = findExprType(srv, query, file, expr);
  726. var result = {url: type.url, doc: type.doc, type: infer.toString(type)};
  727. var inner = type.getType();
  728. if (inner) storeTypeDocs(inner, result);
  729. return clean(result);
  730. }
  731. function storeTypeDocs(type, out) {
  732. if (!out.url) out.url = type.url;
  733. if (!out.doc) out.doc = type.doc;
  734. if (!out.origin) out.origin = type.origin;
  735. var ctor, boring = infer.cx().protos;
  736. if (!out.url && !out.doc && type.proto && (ctor = type.proto.hasCtor) &&
  737. type.proto != boring.Object && type.proto != boring.Function && type.proto != boring.Array) {
  738. out.url = ctor.url;
  739. out.doc = ctor.doc;
  740. }
  741. }
  742. var getSpan = exports.getSpan = function(obj) {
  743. if (!obj.origin) return;
  744. if (obj.originNode) {
  745. var node = obj.originNode;
  746. if (/^Function/.test(node.type) && node.id) node = node.id;
  747. return {origin: obj.origin, node: node};
  748. }
  749. if (obj.span) return {origin: obj.origin, span: obj.span};
  750. };
  751. var storeSpan = exports.storeSpan = function(srv, query, span, target) {
  752. target.origin = span.origin;
  753. if (span.span) {
  754. var m = /^(\d+)\[(\d+):(\d+)\]-(\d+)\[(\d+):(\d+)\]$/.exec(span.span);
  755. target.start = query.lineCharPositions ? {line: Number(m[2]), ch: Number(m[3])} : Number(m[1]);
  756. target.end = query.lineCharPositions ? {line: Number(m[5]), ch: Number(m[6])} : Number(m[4]);
  757. } else {
  758. var file = srv.findFile(span.origin);
  759. target.start = outputPos(query, file, span.node.start);
  760. target.end = outputPos(query, file, span.node.end);
  761. }
  762. };
  763. function findDef(srv, query, file) {
  764. var expr = findExpr(file, query);
  765. var type = findExprType(srv, query, file, expr);
  766. if (infer.didGuess()) return {};
  767. var span = getSpan(type);
  768. var result = {url: type.url, doc: type.doc, origin: type.origin};
  769. if (type.types) for (var i = type.types.length - 1; i >= 0; --i) {
  770. var tp = type.types[i];
  771. storeTypeDocs(tp, result);
  772. if (!span) span = getSpan(tp);
  773. }
  774. if (span && span.node) { // refers to a loaded file
  775. var spanFile = span.node.sourceFile || srv.findFile(span.origin);
  776. var start = outputPos(query, spanFile, span.node.start), end = outputPos(query, spanFile, span.node.end);
  777. result.start = start; result.end = end;
  778. result.file = span.origin;
  779. var cxStart = Math.max(0, span.node.start - 50);
  780. result.contextOffset = span.node.start - cxStart;
  781. result.context = spanFile.text.slice(cxStart, cxStart + 50);
  782. } else if (span) { // external
  783. result.file = span.origin;
  784. storeSpan(srv, query, span, result);
  785. }
  786. return clean(result);
  787. }
  788. function findRefsToVariable(srv, query, file, expr, checkShadowing) {
  789. var name = expr.node.name;
  790. for (var scope = expr.state; scope && !(name in scope.props); scope = scope.prev) {}
  791. if (!scope) throw ternError("Could not find a definition for " + name + " " + !!srv.cx.topScope.props.x);
  792. var type, refs = [];
  793. function storeRef(file) {
  794. return function(node, scopeHere) {
  795. if (checkShadowing) for (var s = scopeHere; s != scope; s = s.prev) {
  796. var exists = s.hasProp(checkShadowing);
  797. if (exists)
  798. throw ternError("Renaming `" + name + "` to `" + checkShadowing + "` would make a variable at line " +
  799. (asLineChar(file, node.start).line + 1) + " point to the definition at line " +
  800. (asLineChar(file, exists.name.start).line + 1));
  801. }
  802. refs.push({file: file.name,
  803. start: outputPos(query, file, node.start),
  804. end: outputPos(query, file, node.end)});
  805. };
  806. }
  807. if (scope.originNode) {
  808. type = "local";
  809. if (checkShadowing) {
  810. for (var prev = scope.prev; prev; prev = prev.prev)
  811. if (checkShadowing in prev.props) break;
  812. if (prev) infer.findRefs(scope.originNode, scope, checkShadowing, prev, function(node) {
  813. throw ternError("Renaming `" + name + "` to `" + checkShadowing + "` would shadow the definition used at line " +
  814. (asLineChar(file, node.start).line + 1));
  815. });
  816. }
  817. infer.findRefs(scope.originNode, scope, name, scope, storeRef(file));
  818. } else {
  819. type = "global";
  820. for (var i = 0; i < srv.files.length; ++i) {
  821. var cur = srv.files[i];
  822. infer.findRefs(cur.ast, cur.scope, name, scope, storeRef(cur));
  823. }
  824. }
  825. return {refs: refs, type: type, name: name};
  826. }
  827. function findRefsToProperty(srv, query, expr, prop) {
  828. var objType = infer.expressionType(expr).getObjType();
  829. if (!objType) throw ternError("Couldn't determine type of base object.");
  830. var refs = [];
  831. function storeRef(file) {
  832. return function(node) {
  833. refs.push({file: file.name,
  834. start: outputPos(query, file, node.start),
  835. end: outputPos(query, file, node.end)});
  836. };
  837. }
  838. for (var i = 0; i < srv.files.length; ++i) {
  839. var cur = srv.files[i];
  840. infer.findPropRefs(cur.ast, cur.scope, objType, prop.name, storeRef(cur));
  841. }
  842. return {refs: refs, name: prop.name};
  843. }
  844. function findRefs(srv, query, file) {
  845. var expr = findExprOrThrow(file, query, true);
  846. if (expr.node.type == "Identifier") {
  847. return findRefsToVariable(srv, query, file, expr);
  848. } else if (expr.node.type == "MemberExpression" && !expr.node.computed) {
  849. var p = expr.node.property;
  850. expr.node = expr.node.object;
  851. return findRefsToProperty(srv, query, expr, p);
  852. } else if (expr.node.type == "ObjectExpression") {
  853. var pos = resolvePos(file, query.end);
  854. for (var i = 0; i < expr.node.properties.length; ++i) {
  855. var k = expr.node.properties[i].key;
  856. if (k.start <= pos && k.end >= pos)
  857. return findRefsToProperty(srv, query, expr, k);
  858. }
  859. }
  860. throw ternError("Not at a variable or property name.");
  861. }
  862. function buildRename(srv, query, file) {
  863. if (typeof query.newName != "string") throw ternError(".query.newName should be a string");
  864. var expr = findExprOrThrow(file, query);
  865. if (!expr || expr.node.type != "Identifier") throw ternError("Not at a variable.");
  866. var data = findRefsToVariable(srv, query, file, expr, query.newName), refs = data.refs;
  867. delete data.refs;
  868. data.files = srv.files.map(function(f){return f.name;});
  869. var changes = data.changes = [];
  870. for (var i = 0; i < refs.length; ++i) {
  871. var use = refs[i];
  872. use.text = query.newName;
  873. changes.push(use);
  874. }
  875. return data;
  876. }
  877. function listFiles(srv) {
  878. return {files: srv.files.map(function(f){return f.name;})};
  879. }
  880. exports.version = "0.11.1";
  881. });