tern.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994
  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 = file.offset;
  337. if (file.offsetLines) offset = {line: file.offsetLines, ch: 0};
  338. file.offset = offset = resolvePos(realFile, file.offsetLines == null ? file.offset : {line: file.offsetLines, ch: 0}, true);
  339. var line = firstLine(file.text);
  340. var foundPos = findMatchingPosition(line, realFile.text, offset);
  341. var pos = foundPos == null ? Math.max(0, realFile.text.lastIndexOf("\n", offset)) : foundPos;
  342. var inObject, atFunction;
  343. infer.withContext(srv.cx, function() {
  344. infer.purge(file.name, pos, pos + file.text.length);
  345. var text = file.text, m;
  346. if (m = text.match(/(?:"([^"]*)"|([\w$]+))\s*:\s*function\b/)) {
  347. var objNode = walk.findNodeAround(file.backing.ast, pos, "ObjectExpression");
  348. if (objNode && objNode.node.objType)
  349. inObject = {type: objNode.node.objType, prop: m[2] || m[1]};
  350. }
  351. if (foundPos && (m = line.match(/^(.*?)\bfunction\b/))) {
  352. var cut = m[1].length, white = "";
  353. for (var i = 0; i < cut; ++i) white += " ";
  354. text = white + text.slice(cut);
  355. atFunction = true;
  356. }
  357. var scopeStart = infer.scopeAt(realFile.ast, pos, realFile.scope);
  358. var scopeEnd = infer.scopeAt(realFile.ast, pos + text.length, realFile.scope);
  359. var scope = file.scope = scopeDepth(scopeStart) < scopeDepth(scopeEnd) ? scopeEnd : scopeStart;
  360. file.ast = infer.parse(text, srv.passes, {directSourceFile: file, allowReturnOutsideFunction: true});
  361. infer.analyze(file.ast, file.name, scope, srv.passes);
  362. // This is a kludge to tie together the function types (if any)
  363. // outside and inside of the fragment, so that arguments and
  364. // return values have some information known about them.
  365. tieTogether: if (inObject || atFunction) {
  366. var newInner = infer.scopeAt(file.ast, line.length, scopeStart);
  367. if (!newInner.fnType) break tieTogether;
  368. if (inObject) {
  369. var prop = inObject.type.getProp(inObject.prop);
  370. prop.addType(newInner.fnType);
  371. } else if (atFunction) {
  372. var inner = infer.scopeAt(realFile.ast, pos + line.length, realFile.scope);
  373. if (inner == scopeStart || !inner.fnType) break tieTogether;
  374. var fOld = inner.fnType, fNew = newInner.fnType;
  375. if (!fNew || (fNew.name != fOld.name && fOld.name)) break tieTogether;
  376. for (var i = 0, e = Math.min(fOld.args.length, fNew.args.length); i < e; ++i)
  377. fOld.args[i].propagate(fNew.args[i]);
  378. fOld.self.propagate(fNew.self);
  379. fNew.retval.propagate(fOld.retval);
  380. }
  381. }
  382. });
  383. return file;
  384. }
  385. // Budget management
  386. function astSize(node) {
  387. var size = 0;
  388. walk.simple(node, {Expression: function() { ++size; }});
  389. return size;
  390. }
  391. function parentDepth(srv, parent) {
  392. var depth = 0;
  393. while (parent) {
  394. parent = srv.findFile(parent).parent;
  395. ++depth;
  396. }
  397. return depth;
  398. }
  399. function budgetName(srv, file) {
  400. for (;;) {
  401. var parent = srv.findFile(file.parent);
  402. if (!parent.parent) break;
  403. file = parent;
  404. }
  405. return file.name;
  406. }
  407. function chargeOnBudget(srv, file) {
  408. var bName = budgetName(srv, file);
  409. var size = astSize(file.ast);
  410. var known = srv.budgets[bName];
  411. if (known == null)
  412. known = srv.budgets[bName] = srv.options.dependencyBudget;
  413. if (known < size) return false;
  414. srv.budgets[bName] = known - size;
  415. return true;
  416. }
  417. // Query helpers
  418. function isPosition(val) {
  419. return typeof val == "number" || typeof val == "object" &&
  420. typeof val.line == "number" && typeof val.ch == "number";
  421. }
  422. // Baseline query document validation
  423. function invalidDoc(doc) {
  424. if (doc.query) {
  425. if (typeof doc.query.type != "string") return ".query.type must be a string";
  426. if (doc.query.start && !isPosition(doc.query.start)) return ".query.start must be a position";
  427. if (doc.query.end && !isPosition(doc.query.end)) return ".query.end must be a position";
  428. }
  429. if (doc.files) {
  430. if (!Array.isArray(doc.files)) return "Files property must be an array";
  431. for (var i = 0; i < doc.files.length; ++i) {
  432. var file = doc.files[i];
  433. if (typeof file != "object") return ".files[n] must be objects";
  434. else if (typeof file.name != "string") return ".files[n].name must be a string";
  435. else if (file.type == "delete") continue;
  436. else if (typeof file.text != "string") return ".files[n].text must be a string";
  437. else if (file.type == "part") {
  438. if (!isPosition(file.offset) && typeof file.offsetLines != "number")
  439. return ".files[n].offset must be a position";
  440. } else if (file.type != "full") return ".files[n].type must be \"full\" or \"part\"";
  441. }
  442. }
  443. }
  444. var offsetSkipLines = 25;
  445. function findLineStart(file, line) {
  446. var text = file.text, offsets = file.lineOffsets || (file.lineOffsets = [0]);
  447. var pos = 0, curLine = 0;
  448. var storePos = Math.min(Math.floor(line / offsetSkipLines), offsets.length - 1);
  449. var pos = offsets[storePos], curLine = storePos * offsetSkipLines;
  450. while (curLine < line) {
  451. ++curLine;
  452. pos = text.indexOf("\n", pos) + 1;
  453. if (pos === 0) return null;
  454. if (curLine % offsetSkipLines === 0) offsets.push(pos);
  455. }
  456. return pos;
  457. }
  458. var resolvePos = exports.resolvePos = function(file, pos, tolerant) {
  459. if (typeof pos != "number") {
  460. var lineStart = findLineStart(file, pos.line);
  461. if (lineStart == null) {
  462. if (tolerant) pos = file.text.length;
  463. else throw ternError("File doesn't contain a line " + pos.line);
  464. } else {
  465. pos = lineStart + pos.ch;
  466. }
  467. }
  468. if (pos > file.text.length) {
  469. if (tolerant) pos = file.text.length;
  470. else throw ternError("Position " + pos + " is outside of file.");
  471. }
  472. return pos;
  473. };
  474. function asLineChar(file, pos) {
  475. if (!file) return {line: 0, ch: 0};
  476. var offsets = file.lineOffsets || (file.lineOffsets = [0]);
  477. var text = file.text, line, lineStart;
  478. for (var i = offsets.length - 1; i >= 0; --i) if (offsets[i] <= pos) {
  479. line = i * offsetSkipLines;
  480. lineStart = offsets[i];
  481. }
  482. for (;;) {
  483. var eol = text.indexOf("\n", lineStart);
  484. if (eol >= pos || eol < 0) break;
  485. lineStart = eol + 1;
  486. ++line;
  487. }
  488. return {line: line, ch: pos - lineStart};
  489. }
  490. var outputPos = exports.outputPos = function(query, file, pos) {
  491. if (query.lineCharPositions) {
  492. var out = asLineChar(file, pos);
  493. if (file.type == "part")
  494. out.line += file.offsetLines != null ? file.offsetLines : asLineChar(file.backing, file.offset).line;
  495. return out;
  496. } else {
  497. return pos + (file.type == "part" ? file.offset : 0);
  498. }
  499. };
  500. // Delete empty fields from result objects
  501. function clean(obj) {
  502. for (var prop in obj) if (obj[prop] == null) delete obj[prop];
  503. return obj;
  504. }
  505. function maybeSet(obj, prop, val) {
  506. if (val != null) obj[prop] = val;
  507. }
  508. // Built-in query types
  509. function compareCompletions(a, b) {
  510. if (typeof a != "string") { a = a.name; b = b.name; }
  511. var aUp = /^[A-Z]/.test(a), bUp = /^[A-Z]/.test(b);
  512. if (aUp == bUp) return a < b ? -1 : a == b ? 0 : 1;
  513. else return aUp ? 1 : -1;
  514. }
  515. function isStringAround(node, start, end) {
  516. return node.type == "Literal" && typeof node.value == "string" &&
  517. node.start == start - 1 && node.end <= end + 1;
  518. }
  519. function pointInProp(objNode, point) {
  520. for (var i = 0; i < objNode.properties.length; i++) {
  521. var curProp = objNode.properties[i];
  522. if (curProp.key.start <= point && curProp.key.end >= point)
  523. return curProp;
  524. }
  525. }
  526. var jsKeywords = ("break do instanceof typeof case else new var " +
  527. "catch finally return void continue for switch while debugger " +
  528. "function this with default if throw delete in try").split(" ");
  529. function findCompletions(srv, query, file) {
  530. if (query.end == null) throw ternError("missing .query.end field");
  531. if (srv.passes.completion) for (var i = 0; i < srv.passes.completion.length; i++) {
  532. var result = srv.passes.completion[i](file, query);
  533. if (result) return result;
  534. }
  535. var wordStart = resolvePos(file, query.end), wordEnd = wordStart, text = file.text;
  536. while (wordStart && acorn.isIdentifierChar(text.charCodeAt(wordStart - 1))) --wordStart;
  537. if (query.expandWordForward !== false)
  538. while (wordEnd < text.length && acorn.isIdentifierChar(text.charCodeAt(wordEnd))) ++wordEnd;
  539. var word = text.slice(wordStart, wordEnd), completions = [], ignoreObj;
  540. if (query.caseInsensitive) word = word.toLowerCase();
  541. var wrapAsObjs = query.types || query.depths || query.docs || query.urls || query.origins;
  542. function gather(prop, obj, depth, addInfo) {
  543. // 'hasOwnProperty' and such are usually just noise, leave them
  544. // out when no prefix is provided.
  545. if ((objLit || query.omitObjectPrototype !== false) && obj == srv.cx.protos.Object && !word) return;
  546. if (query.filter !== false && word &&
  547. (query.caseInsensitive ? prop.toLowerCase() : prop).indexOf(word) !== 0) return;
  548. if (ignoreObj && ignoreObj.props[prop]) return;
  549. for (var i = 0; i < completions.length; ++i) {
  550. var c = completions[i];
  551. if ((wrapAsObjs ? c.name : c) == prop) return;
  552. }
  553. var rec = wrapAsObjs ? {name: prop} : prop;
  554. completions.push(rec);
  555. if (obj && (query.types || query.docs || query.urls || query.origins)) {
  556. var val = obj.props[prop];
  557. infer.resetGuessing();
  558. var type = val.getType();
  559. rec.guess = infer.didGuess();
  560. if (query.types)
  561. rec.type = infer.toString(val);
  562. if (query.docs)
  563. maybeSet(rec, "doc", val.doc || type && type.doc);
  564. if (query.urls)
  565. maybeSet(rec, "url", val.url || type && type.url);
  566. if (query.origins)
  567. maybeSet(rec, "origin", val.origin || type && type.origin);
  568. }
  569. if (query.depths) rec.depth = depth;
  570. if (wrapAsObjs && addInfo) addInfo(rec);
  571. }
  572. var hookname, prop, objType, isKey;
  573. var exprAt = infer.findExpressionAround(file.ast, null, wordStart, file.scope);
  574. var memberExpr, objLit;
  575. // Decide whether this is an object property, either in a member
  576. // expression or an object literal.
  577. if (exprAt) {
  578. if (exprAt.node.type == "MemberExpression" && exprAt.node.object.end < wordStart) {
  579. memberExpr = exprAt;
  580. } else if (isStringAround(exprAt.node, wordStart, wordEnd)) {
  581. var parent = infer.parentNode(exprAt.node, file.ast);
  582. if (parent.type == "MemberExpression" && parent.property == exprAt.node)
  583. memberExpr = {node: parent, state: exprAt.state};
  584. } else if (exprAt.node.type == "ObjectExpression") {
  585. var objProp = pointInProp(exprAt.node, wordEnd);
  586. if (objProp) {
  587. objLit = exprAt;
  588. prop = isKey = objProp.key.name;
  589. } else if (!word && !/:\s*$/.test(file.text.slice(0, wordStart))) {
  590. objLit = exprAt;
  591. prop = isKey = true;
  592. }
  593. }
  594. }
  595. if (objLit) {
  596. // Since we can't use the type of the literal itself to complete
  597. // its properties (it doesn't contain the information we need),
  598. // we have to try asking the surrounding expression for type info.
  599. objType = infer.typeFromContext(file.ast, objLit);
  600. ignoreObj = objLit.node.objType;
  601. } else if (memberExpr) {
  602. prop = memberExpr.node.property;
  603. prop = prop.type == "Literal" ? prop.value.slice(1) : prop.name;
  604. memberExpr.node = memberExpr.node.object;
  605. objType = infer.expressionType(memberExpr);
  606. } else if (text.charAt(wordStart - 1) == ".") {
  607. var pathStart = wordStart - 1;
  608. while (pathStart && (text.charAt(pathStart - 1) == "." || acorn.isIdentifierChar(text.charCodeAt(pathStart - 1)))) pathStart--;
  609. var path = text.slice(pathStart, wordStart - 1);
  610. if (path) {
  611. objType = infer.def.parsePath(path, file.scope).getObjType();
  612. prop = word;
  613. }
  614. }
  615. if (prop != null) {
  616. srv.cx.completingProperty = prop;
  617. if (objType) infer.forAllPropertiesOf(objType, gather);
  618. if (!completions.length && query.guess !== false && objType && objType.guessProperties)
  619. objType.guessProperties(function(p, o, d) {if (p != prop && p != "✖") gather(p, o, d);});
  620. if (!completions.length && word.length >= 2 && query.guess !== false)
  621. for (var prop in srv.cx.props) gather(prop, srv.cx.props[prop][0], 0);
  622. hookname = "memberCompletion";
  623. } else {
  624. infer.forAllLocalsAt(file.ast, wordStart, file.scope, gather);
  625. if (query.includeKeywords) jsKeywords.forEach(function(kw) {
  626. gather(kw, null, 0, function(rec) { rec.isKeyword = true; });
  627. });
  628. hookname = "variableCompletion";
  629. }
  630. if (srv.passes[hookname])
  631. srv.passes[hookname].forEach(function(hook) {hook(file, wordStart, wordEnd, gather);});
  632. if (query.sort !== false) completions.sort(compareCompletions);
  633. srv.cx.completingProperty = null;
  634. return {start: outputPos(query, file, wordStart),
  635. end: outputPos(query, file, wordEnd),
  636. isProperty: !!prop,
  637. isObjectKey: !!isKey,
  638. completions: completions};
  639. }
  640. function findProperties(srv, query) {
  641. var prefix = query.prefix, found = [];
  642. for (var prop in srv.cx.props)
  643. if (prop != "<i>" && (!prefix || prop.indexOf(prefix) === 0)) found.push(prop);
  644. if (query.sort !== false) found.sort(compareCompletions);
  645. return {completions: found};
  646. }
  647. var findExpr = exports.findQueryExpr = function(file, query, wide) {
  648. if (query.end == null) throw ternError("missing .query.end field");
  649. if (query.variable) {
  650. var scope = infer.scopeAt(file.ast, resolvePos(file, query.end), file.scope);
  651. return {node: {type: "Identifier", name: query.variable, start: query.end, end: query.end + 1},
  652. state: scope};
  653. } else {
  654. var start = query.start && resolvePos(file, query.start), end = resolvePos(file, query.end);
  655. var expr = infer.findExpressionAt(file.ast, start, end, file.scope);
  656. if (expr) return expr;
  657. expr = infer.findExpressionAround(file.ast, start, end, file.scope);
  658. if (expr && (expr.node.type == "ObjectExpression" || wide ||
  659. (start == null ? end : start) - expr.node.start < 20 || expr.node.end - end < 20))
  660. return expr;
  661. return null;
  662. }
  663. };
  664. function findExprOrThrow(file, query, wide) {
  665. var expr = findExpr(file, query, wide);
  666. if (expr) return expr;
  667. throw ternError("No expression at the given position.");
  668. }
  669. function ensureObj(tp) {
  670. if (!tp || !(tp = tp.getType()) || !(tp instanceof infer.Obj)) return null;
  671. return tp;
  672. }
  673. function findExprType(srv, query, file, expr) {
  674. var type;
  675. if (expr) {
  676. infer.resetGuessing();
  677. type = infer.expressionType(expr);
  678. }
  679. if (srv.passes["typeAt"]) {
  680. var pos = resolvePos(file, query.end);
  681. srv.passes["typeAt"].forEach(function(hook) {
  682. type = hook(file, pos, expr, type);
  683. });
  684. }
  685. if (!type) throw ternError("No type found at the given position.");
  686. var objProp;
  687. if (expr.node.type == "ObjectExpression" && query.end != null &&
  688. (objProp = pointInProp(expr.node, resolvePos(file, query.end)))) {
  689. var name = objProp.key.name;
  690. var fromCx = ensureObj(infer.typeFromContext(file.ast, expr));
  691. if (fromCx && fromCx.hasProp(name)) {
  692. type = fromCx.hasProp(name);
  693. } else {
  694. var fromLocal = ensureObj(type);
  695. if (fromLocal && fromLocal.hasProp(name))
  696. type = fromLocal.hasProp(name);
  697. }
  698. }
  699. return type;
  700. };
  701. function findTypeAt(srv, query, file) {
  702. var expr = findExpr(file, query), exprName;
  703. var type = findExprType(srv, query, file, expr), exprType = type;
  704. if (query.preferFunction)
  705. type = type.getFunctionType() || type.getType();
  706. else
  707. type = type.getType();
  708. if (expr) {
  709. if (expr.node.type == "Identifier")
  710. exprName = expr.node.name;
  711. else if (expr.node.type == "MemberExpression" && !expr.node.computed)
  712. exprName = expr.node.property.name;
  713. }
  714. if (query.depth != null && typeof query.depth != "number")
  715. throw ternError(".query.depth must be a number");
  716. var result = {guess: infer.didGuess(),
  717. type: infer.toString(exprType, query.depth),
  718. name: type && type.name,
  719. exprName: exprName};
  720. if (type) storeTypeDocs(type, result);
  721. if (!result.doc && exprType.doc) result.doc = exprType.doc;
  722. return clean(result);
  723. }
  724. function findDocs(srv, query, file) {
  725. var expr = findExpr(file, query);
  726. var type = findExprType(srv, query, file, expr);
  727. var result = {url: type.url, doc: type.doc, type: infer.toString(type)};
  728. var inner = type.getType();
  729. if (inner) storeTypeDocs(inner, result);
  730. return clean(result);
  731. }
  732. function storeTypeDocs(type, out) {
  733. if (!out.url) out.url = type.url;
  734. if (!out.doc) out.doc = type.doc;
  735. if (!out.origin) out.origin = type.origin;
  736. var ctor, boring = infer.cx().protos;
  737. if (!out.url && !out.doc && type.proto && (ctor = type.proto.hasCtor) &&
  738. type.proto != boring.Object && type.proto != boring.Function && type.proto != boring.Array) {
  739. out.url = ctor.url;
  740. out.doc = ctor.doc;
  741. }
  742. }
  743. var getSpan = exports.getSpan = function(obj) {
  744. if (!obj.origin) return;
  745. if (obj.originNode) {
  746. var node = obj.originNode;
  747. if (/^Function/.test(node.type) && node.id) node = node.id;
  748. return {origin: obj.origin, node: node};
  749. }
  750. if (obj.span) return {origin: obj.origin, span: obj.span};
  751. };
  752. var storeSpan = exports.storeSpan = function(srv, query, span, target) {
  753. target.origin = span.origin;
  754. if (span.span) {
  755. var m = /^(\d+)\[(\d+):(\d+)\]-(\d+)\[(\d+):(\d+)\]$/.exec(span.span);
  756. target.start = query.lineCharPositions ? {line: Number(m[2]), ch: Number(m[3])} : Number(m[1]);
  757. target.end = query.lineCharPositions ? {line: Number(m[5]), ch: Number(m[6])} : Number(m[4]);
  758. } else {
  759. var file = srv.findFile(span.origin);
  760. target.start = outputPos(query, file, span.node.start);
  761. target.end = outputPos(query, file, span.node.end);
  762. }
  763. };
  764. function findDef(srv, query, file) {
  765. var expr = findExpr(file, query);
  766. var type = findExprType(srv, query, file, expr);
  767. if (infer.didGuess()) return {};
  768. var span = getSpan(type);
  769. var result = {url: type.url, doc: type.doc, origin: type.origin};
  770. if (type.types) for (var i = type.types.length - 1; i >= 0; --i) {
  771. var tp = type.types[i];
  772. storeTypeDocs(tp, result);
  773. if (!span) span = getSpan(tp);
  774. }
  775. if (span && span.node) { // refers to a loaded file
  776. var spanFile = span.node.sourceFile || srv.findFile(span.origin);
  777. var start = outputPos(query, spanFile, span.node.start), end = outputPos(query, spanFile, span.node.end);
  778. result.start = start; result.end = end;
  779. result.file = span.origin;
  780. var cxStart = Math.max(0, span.node.start - 50);
  781. result.contextOffset = span.node.start - cxStart;
  782. result.context = spanFile.text.slice(cxStart, cxStart + 50);
  783. } else if (span) { // external
  784. result.file = span.origin;
  785. storeSpan(srv, query, span, result);
  786. }
  787. return clean(result);
  788. }
  789. function findRefsToVariable(srv, query, file, expr, checkShadowing) {
  790. var name = expr.node.name;
  791. for (var scope = expr.state; scope && !(name in scope.props); scope = scope.prev) {}
  792. if (!scope) throw ternError("Could not find a definition for " + name + " " + !!srv.cx.topScope.props.x);
  793. var type, refs = [];
  794. function storeRef(file) {
  795. return function(node, scopeHere) {
  796. if (checkShadowing) for (var s = scopeHere; s != scope; s = s.prev) {
  797. var exists = s.hasProp(checkShadowing);
  798. if (exists)
  799. throw ternError("Renaming `" + name + "` to `" + checkShadowing + "` would make a variable at line " +
  800. (asLineChar(file, node.start).line + 1) + " point to the definition at line " +
  801. (asLineChar(file, exists.name.start).line + 1));
  802. }
  803. refs.push({file: file.name,
  804. start: outputPos(query, file, node.start),
  805. end: outputPos(query, file, node.end)});
  806. };
  807. }
  808. if (scope.originNode) {
  809. type = "local";
  810. if (checkShadowing) {
  811. for (var prev = scope.prev; prev; prev = prev.prev)
  812. if (checkShadowing in prev.props) break;
  813. if (prev) infer.findRefs(scope.originNode, scope, checkShadowing, prev, function(node) {
  814. throw ternError("Renaming `" + name + "` to `" + checkShadowing + "` would shadow the definition used at line " +
  815. (asLineChar(file, node.start).line + 1));
  816. });
  817. }
  818. infer.findRefs(scope.originNode, scope, name, scope, storeRef(file));
  819. } else {
  820. type = "global";
  821. for (var i = 0; i < srv.files.length; ++i) {
  822. var cur = srv.files[i];
  823. infer.findRefs(cur.ast, cur.scope, name, scope, storeRef(cur));
  824. }
  825. }
  826. return {refs: refs, type: type, name: name};
  827. }
  828. function findRefsToProperty(srv, query, expr, prop) {
  829. var objType = infer.expressionType(expr).getObjType();
  830. if (!objType) throw ternError("Couldn't determine type of base object.");
  831. var refs = [];
  832. function storeRef(file) {
  833. return function(node) {
  834. refs.push({file: file.name,
  835. start: outputPos(query, file, node.start),
  836. end: outputPos(query, file, node.end)});
  837. };
  838. }
  839. for (var i = 0; i < srv.files.length; ++i) {
  840. var cur = srv.files[i];
  841. infer.findPropRefs(cur.ast, cur.scope, objType, prop.name, storeRef(cur));
  842. }
  843. return {refs: refs, name: prop.name};
  844. }
  845. function findRefs(srv, query, file) {
  846. var expr = findExprOrThrow(file, query, true);
  847. if (expr && expr.node.type == "Identifier") {
  848. return findRefsToVariable(srv, query, file, expr);
  849. } else if (expr && expr.node.type == "MemberExpression" && !expr.node.computed) {
  850. var p = expr.node.property;
  851. expr.node = expr.node.object;
  852. return findRefsToProperty(srv, query, expr, p);
  853. } else if (expr && expr.node.type == "ObjectExpression") {
  854. var pos = resolvePos(file, query.end);
  855. for (var i = 0; i < expr.node.properties.length; ++i) {
  856. var k = expr.node.properties[i].key;
  857. if (k.start <= pos && k.end >= pos)
  858. return findRefsToProperty(srv, query, expr, k);
  859. }
  860. }
  861. throw ternError("Not at a variable or property name.");
  862. }
  863. function buildRename(srv, query, file) {
  864. if (typeof query.newName != "string") throw ternError(".query.newName should be a string");
  865. var expr = findExprOrThrow(file, query);
  866. if (!expr || expr.node.type != "Identifier") throw ternError("Not at a variable.");
  867. var data = findRefsToVariable(srv, query, file, expr, query.newName), refs = data.refs;
  868. delete data.refs;
  869. data.files = srv.files.map(function(f){return f.name;});
  870. var changes = data.changes = [];
  871. for (var i = 0; i < refs.length; ++i) {
  872. var use = refs[i];
  873. use.text = query.newName;
  874. changes.push(use);
  875. }
  876. return data;
  877. }
  878. function listFiles(srv) {
  879. return {files: srv.files.map(function(f){return f.name;})};
  880. }
  881. exports.version = "0.11.1";
  882. });