123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993 |
- // The Tern server object
- // A server is a stateful object that manages the analysis for a
- // project, and defines an interface for querying the code in the
- // project.
- (function(root, mod) {
- if (typeof exports == "object" && typeof module == "object") // CommonJS
- return mod(exports, require("./infer"), require("./signal"),
- require("acorn"), require("acorn/dist/walk"));
- if (typeof define == "function" && define.amd) // AMD
- return define(["exports", "./infer", "./signal", "acorn/dist/acorn", "acorn/dist/walk"], mod);
- mod(root.tern || (root.tern = {}), tern, tern.signal, acorn, acorn.walk); // Plain browser env
- })(this, function(exports, infer, signal, acorn, walk) {
- "use strict";
- var plugins = Object.create(null);
- exports.registerPlugin = function(name, init) { plugins[name] = init; };
- var defaultOptions = exports.defaultOptions = {
- debug: false,
- async: false,
- getFile: function(_f, c) { if (this.async) c(null, null); },
- defs: [],
- plugins: {},
- fetchTimeout: 1000,
- dependencyBudget: 20000,
- reuseInstances: true,
- stripCRs: false
- };
- var queryTypes = {
- completions: {
- takesFile: true,
- run: findCompletions
- },
- properties: {
- run: findProperties
- },
- type: {
- takesFile: true,
- run: findTypeAt
- },
- documentation: {
- takesFile: true,
- run: findDocs
- },
- definition: {
- takesFile: true,
- run: findDef
- },
- refs: {
- takesFile: true,
- fullFile: true,
- run: findRefs
- },
- rename: {
- takesFile: true,
- fullFile: true,
- run: buildRename
- },
- files: {
- run: listFiles
- }
- };
- exports.defineQueryType = function(name, desc) { queryTypes[name] = desc; };
- function File(name, parent) {
- this.name = name;
- this.parent = parent;
- this.scope = this.text = this.ast = this.lineOffsets = null;
- }
- File.prototype.asLineChar = function(pos) { return asLineChar(this, pos); };
- function updateText(file, text, srv) {
- file.text = srv.options.stripCRs ? text.replace(/\r\n/g, "\n") : text;
- infer.withContext(srv.cx, function() {
- file.ast = infer.parse(file.text, srv.passes, {directSourceFile: file, allowReturnOutsideFunction: true});
- });
- file.lineOffsets = null;
- }
- var Server = exports.Server = function(options) {
- this.cx = null;
- this.options = options || {};
- for (var o in defaultOptions) if (!options.hasOwnProperty(o))
- options[o] = defaultOptions[o];
- this.handlers = Object.create(null);
- this.files = [];
- this.fileMap = Object.create(null);
- this.needsPurge = [];
- this.budgets = Object.create(null);
- this.uses = 0;
- this.pending = 0;
- this.asyncError = null;
- this.passes = Object.create(null);
- this.defs = options.defs.slice(0);
- for (var plugin in options.plugins) if (options.plugins.hasOwnProperty(plugin) && plugin in plugins) {
- var init = plugins[plugin](this, options.plugins[plugin]);
- if (init && init.defs) {
- if (init.loadFirst) this.defs.unshift(init.defs);
- else this.defs.push(init.defs);
- }
- if (init && init.passes) for (var type in init.passes) if (init.passes.hasOwnProperty(type))
- (this.passes[type] || (this.passes[type] = [])).push(init.passes[type]);
- }
- this.reset();
- };
- Server.prototype = signal.mixin({
- addFile: function(name, /*optional*/ text, parent) {
- // Don't crash when sloppy plugins pass non-existent parent ids
- if (parent && !(parent in this.fileMap)) parent = null;
- ensureFile(this, name, parent, text);
- },
- delFile: function(name) {
- var file = this.findFile(name);
- if (file) {
- this.needsPurge.push(file.name);
- this.files.splice(this.files.indexOf(file), 1);
- delete this.fileMap[name];
- }
- },
- reset: function() {
- this.signal("reset");
- this.cx = new infer.Context(this.defs, this);
- this.uses = 0;
- this.budgets = Object.create(null);
- for (var i = 0; i < this.files.length; ++i) {
- var file = this.files[i];
- file.scope = null;
- }
- },
- request: function(doc, c) {
- var inv = invalidDoc(doc);
- if (inv) return c(inv);
- var self = this;
- doRequest(this, doc, function(err, data) {
- c(err, data);
- if (self.uses > 40) {
- self.reset();
- analyzeAll(self, null, function(){});
- }
- });
- },
- findFile: function(name) {
- return this.fileMap[name];
- },
- flush: function(c) {
- var cx = this.cx;
- analyzeAll(this, null, function(err) {
- if (err) return c(err);
- infer.withContext(cx, c);
- });
- },
- startAsyncAction: function() {
- ++this.pending;
- },
- finishAsyncAction: function(err) {
- if (err) this.asyncError = err;
- if (--this.pending === 0) this.signal("everythingFetched");
- }
- });
- function doRequest(srv, doc, c) {
- if (doc.query && !queryTypes.hasOwnProperty(doc.query.type))
- return c("No query type '" + doc.query.type + "' defined");
- var query = doc.query;
- // Respond as soon as possible when this just uploads files
- if (!query) c(null, {});
- var files = doc.files || [];
- if (files.length) ++srv.uses;
- for (var i = 0; i < files.length; ++i) {
- var file = files[i];
- if (file.type == "delete")
- srv.delFile(file.name);
- else
- ensureFile(srv, file.name, null, file.type == "full" ? file.text : null);
- }
- var timeBudget = typeof doc.timeout == "number" ? [doc.timeout] : null;
- if (!query) {
- analyzeAll(srv, timeBudget, function(){});
- return;
- }
- var queryType = queryTypes[query.type];
- if (queryType.takesFile) {
- if (typeof query.file != "string") return c(".query.file must be a string");
- if (!/^#/.test(query.file)) ensureFile(srv, query.file, null);
- }
- analyzeAll(srv, timeBudget, function(err) {
- if (err) return c(err);
- var file = queryType.takesFile && resolveFile(srv, files, query.file);
- if (queryType.fullFile && file.type == "part")
- return c("Can't run a " + query.type + " query on a file fragment");
- function run() {
- var result;
- try {
- result = queryType.run(srv, query, file);
- } catch (e) {
- if (srv.options.debug && e.name != "TernError") console.error(e.stack);
- return c(e);
- }
- c(null, result);
- }
- infer.withContext(srv.cx, timeBudget ? function() { infer.withTimeout(timeBudget[0], run); } : run);
- });
- }
- function analyzeFile(srv, file) {
- infer.withContext(srv.cx, function() {
- file.scope = srv.cx.topScope;
- srv.signal("beforeLoad", file);
- infer.analyze(file.ast, file.name, file.scope, srv.passes);
- srv.signal("afterLoad", file);
- });
- return file;
- }
- function ensureFile(srv, name, parent, text) {
- var known = srv.findFile(name);
- if (known) {
- if (text != null) {
- if (known.scope) {
- srv.needsPurge.push(name);
- known.scope = null;
- }
- updateText(known, text, srv);
- }
- if (parentDepth(srv, known.parent) > parentDepth(srv, parent)) {
- known.parent = parent;
- if (known.excluded) known.excluded = null;
- }
- return;
- }
- var file = new File(name, parent);
- srv.files.push(file);
- srv.fileMap[name] = file;
- if (text != null) {
- updateText(file, text, srv);
- } else if (srv.options.async) {
- srv.startAsyncAction();
- srv.options.getFile(name, function(err, text) {
- updateText(file, text || "", srv);
- srv.finishAsyncAction(err);
- });
- } else {
- updateText(file, srv.options.getFile(name) || "", srv);
- }
- }
- function fetchAll(srv, c) {
- var done = true, returned = false;
- srv.files.forEach(function(file) {
- if (file.text != null) return;
- if (srv.options.async) {
- done = false;
- srv.options.getFile(file.name, function(err, text) {
- if (err && !returned) { returned = true; return c(err); }
- updateText(file, text || "", srv);
- fetchAll(srv, c);
- });
- } else {
- try {
- updateText(file, srv.options.getFile(file.name) || "", srv);
- } catch (e) { return c(e); }
- }
- });
- if (done) c();
- }
- function waitOnFetch(srv, timeBudget, c) {
- var done = function() {
- srv.off("everythingFetched", done);
- clearTimeout(timeout);
- analyzeAll(srv, timeBudget, c);
- };
- srv.on("everythingFetched", done);
- var timeout = setTimeout(done, srv.options.fetchTimeout);
- }
- function analyzeAll(srv, timeBudget, c) {
- if (srv.pending) return waitOnFetch(srv, timeBudget, c);
- var e = srv.fetchError;
- if (e) { srv.fetchError = null; return c(e); }
- if (srv.needsPurge.length > 0) infer.withContext(srv.cx, function() {
- infer.purge(srv.needsPurge);
- srv.needsPurge.length = 0;
- });
- var done = true;
- // The second inner loop might add new files. The outer loop keeps
- // repeating both inner loops until all files have been looked at.
- for (var i = 0; i < srv.files.length;) {
- var toAnalyze = [];
- for (; i < srv.files.length; ++i) {
- var file = srv.files[i];
- if (file.text == null) done = false;
- else if (file.scope == null && !file.excluded) toAnalyze.push(file);
- }
- toAnalyze.sort(function(a, b) {
- return parentDepth(srv, a.parent) - parentDepth(srv, b.parent);
- });
- for (var j = 0; j < toAnalyze.length; j++) {
- var file = toAnalyze[j];
- if (file.parent && !chargeOnBudget(srv, file)) {
- file.excluded = true;
- } else if (timeBudget) {
- var startTime = +new Date;
- infer.withTimeout(timeBudget[0], function() { analyzeFile(srv, file); });
- timeBudget[0] -= +new Date - startTime;
- } else {
- analyzeFile(srv, file);
- }
- }
- }
- if (done) c();
- else waitOnFetch(srv, timeBudget, c);
- }
- function firstLine(str) {
- var end = str.indexOf("\n");
- if (end < 0) return str;
- return str.slice(0, end);
- }
- function findMatchingPosition(line, file, near) {
- var pos = Math.max(0, near - 500), closest = null;
- if (!/^\s*$/.test(line)) for (;;) {
- var found = file.indexOf(line, pos);
- if (found < 0 || found > near + 500) break;
- if (closest == null || Math.abs(closest - near) > Math.abs(found - near))
- closest = found;
- pos = found + line.length;
- }
- return closest;
- }
- function scopeDepth(s) {
- for (var i = 0; s; ++i, s = s.prev) {}
- return i;
- }
- function ternError(msg) {
- var err = new Error(msg);
- err.name = "TernError";
- return err;
- }
- function resolveFile(srv, localFiles, name) {
- var isRef = name.match(/^#(\d+)$/);
- if (!isRef) return srv.findFile(name);
- var file = localFiles[isRef[1]];
- if (!file || file.type == "delete") throw ternError("Reference to unknown file " + name);
- if (file.type == "full") return srv.findFile(file.name);
- // This is a partial file
- var realFile = file.backing = srv.findFile(file.name);
- var offset;
- file.offset = offset = resolvePos(realFile, file.offsetLines == null ? file.offset : {line: file.offsetLines, ch: 0}, true);
- var line = firstLine(file.text);
- var foundPos = findMatchingPosition(line, realFile.text, offset);
- var pos = foundPos == null ? Math.max(0, realFile.text.lastIndexOf("\n", offset)) : foundPos;
- var inObject, atFunction;
- infer.withContext(srv.cx, function() {
- infer.purge(file.name, pos, pos + file.text.length);
- var text = file.text, m;
- if (m = text.match(/(?:"([^"]*)"|([\w$]+))\s*:\s*function\b/)) {
- var objNode = walk.findNodeAround(file.backing.ast, pos, "ObjectExpression");
- if (objNode && objNode.node.objType)
- inObject = {type: objNode.node.objType, prop: m[2] || m[1]};
- }
- if (foundPos && (m = line.match(/^(.*?)\bfunction\b/))) {
- var cut = m[1].length, white = "";
- for (var i = 0; i < cut; ++i) white += " ";
- text = white + text.slice(cut);
- atFunction = true;
- }
- var scopeStart = infer.scopeAt(realFile.ast, pos, realFile.scope);
- var scopeEnd = infer.scopeAt(realFile.ast, pos + text.length, realFile.scope);
- var scope = file.scope = scopeDepth(scopeStart) < scopeDepth(scopeEnd) ? scopeEnd : scopeStart;
- file.ast = infer.parse(text, srv.passes, {directSourceFile: file, allowReturnOutsideFunction: true});
- infer.analyze(file.ast, file.name, scope, srv.passes);
- // This is a kludge to tie together the function types (if any)
- // outside and inside of the fragment, so that arguments and
- // return values have some information known about them.
- tieTogether: if (inObject || atFunction) {
- var newInner = infer.scopeAt(file.ast, line.length, scopeStart);
- if (!newInner.fnType) break tieTogether;
- if (inObject) {
- var prop = inObject.type.getProp(inObject.prop);
- prop.addType(newInner.fnType);
- } else if (atFunction) {
- var inner = infer.scopeAt(realFile.ast, pos + line.length, realFile.scope);
- if (inner == scopeStart || !inner.fnType) break tieTogether;
- var fOld = inner.fnType, fNew = newInner.fnType;
- if (!fNew || (fNew.name != fOld.name && fOld.name)) break tieTogether;
- for (var i = 0, e = Math.min(fOld.args.length, fNew.args.length); i < e; ++i)
- fOld.args[i].propagate(fNew.args[i]);
- fOld.self.propagate(fNew.self);
- fNew.retval.propagate(fOld.retval);
- }
- }
- });
- return file;
- }
- // Budget management
- function astSize(node) {
- var size = 0;
- walk.simple(node, {Expression: function() { ++size; }});
- return size;
- }
- function parentDepth(srv, parent) {
- var depth = 0;
- while (parent) {
- parent = srv.findFile(parent).parent;
- ++depth;
- }
- return depth;
- }
- function budgetName(srv, file) {
- for (;;) {
- var parent = srv.findFile(file.parent);
- if (!parent.parent) break;
- file = parent;
- }
- return file.name;
- }
- function chargeOnBudget(srv, file) {
- var bName = budgetName(srv, file);
- var size = astSize(file.ast);
- var known = srv.budgets[bName];
- if (known == null)
- known = srv.budgets[bName] = srv.options.dependencyBudget;
- if (known < size) return false;
- srv.budgets[bName] = known - size;
- return true;
- }
- // Query helpers
- function isPosition(val) {
- return typeof val == "number" || typeof val == "object" &&
- typeof val.line == "number" && typeof val.ch == "number";
- }
- // Baseline query document validation
- function invalidDoc(doc) {
- if (doc.query) {
- if (typeof doc.query.type != "string") return ".query.type must be a string";
- if (doc.query.start && !isPosition(doc.query.start)) return ".query.start must be a position";
- if (doc.query.end && !isPosition(doc.query.end)) return ".query.end must be a position";
- }
- if (doc.files) {
- if (!Array.isArray(doc.files)) return "Files property must be an array";
- for (var i = 0; i < doc.files.length; ++i) {
- var file = doc.files[i];
- if (typeof file != "object") return ".files[n] must be objects";
- else if (typeof file.name != "string") return ".files[n].name must be a string";
- else if (file.type == "delete") continue;
- else if (typeof file.text != "string") return ".files[n].text must be a string";
- else if (file.type == "part") {
- if (!isPosition(file.offset) && typeof file.offsetLines != "number")
- return ".files[n].offset must be a position";
- } else if (file.type != "full") return ".files[n].type must be \"full\" or \"part\"";
- }
- }
- }
- var offsetSkipLines = 25;
- function findLineStart(file, line) {
- var text = file.text, offsets = file.lineOffsets || (file.lineOffsets = [0]);
- var pos = 0, curLine = 0;
- var storePos = Math.min(Math.floor(line / offsetSkipLines), offsets.length - 1);
- var pos = offsets[storePos], curLine = storePos * offsetSkipLines;
- while (curLine < line) {
- ++curLine;
- pos = text.indexOf("\n", pos) + 1;
- if (pos === 0) return null;
- if (curLine % offsetSkipLines === 0) offsets.push(pos);
- }
- return pos;
- }
- var resolvePos = exports.resolvePos = function(file, pos, tolerant) {
- if (typeof pos != "number") {
- var lineStart = findLineStart(file, pos.line);
- if (lineStart == null) {
- if (tolerant) pos = file.text.length;
- else throw ternError("File doesn't contain a line " + pos.line);
- } else {
- pos = lineStart + pos.ch;
- }
- }
- if (pos > file.text.length) {
- if (tolerant) pos = file.text.length;
- else throw ternError("Position " + pos + " is outside of file.");
- }
- return pos;
- };
- function asLineChar(file, pos) {
- if (!file) return {line: 0, ch: 0};
- var offsets = file.lineOffsets || (file.lineOffsets = [0]);
- var text = file.text, line, lineStart;
- for (var i = offsets.length - 1; i >= 0; --i) if (offsets[i] <= pos) {
- line = i * offsetSkipLines;
- lineStart = offsets[i];
- }
- for (;;) {
- var eol = text.indexOf("\n", lineStart);
- if (eol >= pos || eol < 0) break;
- lineStart = eol + 1;
- ++line;
- }
- return {line: line, ch: pos - lineStart};
- }
- var outputPos = exports.outputPos = function(query, file, pos) {
- if (query.lineCharPositions) {
- var out = asLineChar(file, pos);
- if (file.type == "part")
- out.line += file.offsetLines != null ? file.offsetLines : asLineChar(file.backing, file.offset).line;
- return out;
- } else {
- return pos + (file.type == "part" ? file.offset : 0);
- }
- };
- // Delete empty fields from result objects
- function clean(obj) {
- for (var prop in obj) if (obj[prop] == null) delete obj[prop];
- return obj;
- }
- function maybeSet(obj, prop, val) {
- if (val != null) obj[prop] = val;
- }
- // Built-in query types
- function compareCompletions(a, b) {
- if (typeof a != "string") { a = a.name; b = b.name; }
- var aUp = /^[A-Z]/.test(a), bUp = /^[A-Z]/.test(b);
- if (aUp == bUp) return a < b ? -1 : a == b ? 0 : 1;
- else return aUp ? 1 : -1;
- }
- function isStringAround(node, start, end) {
- return node.type == "Literal" && typeof node.value == "string" &&
- node.start == start - 1 && node.end <= end + 1;
- }
- function pointInProp(objNode, point) {
- for (var i = 0; i < objNode.properties.length; i++) {
- var curProp = objNode.properties[i];
- if (curProp.key.start <= point && curProp.key.end >= point)
- return curProp;
- }
- }
- var jsKeywords = ("break do instanceof typeof case else new var " +
- "catch finally return void continue for switch while debugger " +
- "function this with default if throw delete in try").split(" ");
- function findCompletions(srv, query, file) {
- if (query.end == null) throw ternError("missing .query.end field");
- if (srv.passes.completion) for (var i = 0; i < srv.passes.completion.length; i++) {
- var result = srv.passes.completion[i](file, query);
- if (result) return result;
- }
- var wordStart = resolvePos(file, query.end), wordEnd = wordStart, text = file.text;
- while (wordStart && acorn.isIdentifierChar(text.charCodeAt(wordStart - 1))) --wordStart;
- if (query.expandWordForward !== false)
- while (wordEnd < text.length && acorn.isIdentifierChar(text.charCodeAt(wordEnd))) ++wordEnd;
- var word = text.slice(wordStart, wordEnd), completions = [], ignoreObj;
- if (query.caseInsensitive) word = word.toLowerCase();
- var wrapAsObjs = query.types || query.depths || query.docs || query.urls || query.origins;
- function gather(prop, obj, depth, addInfo) {
- // 'hasOwnProperty' and such are usually just noise, leave them
- // out when no prefix is provided.
- if ((objLit || query.omitObjectPrototype !== false) && obj == srv.cx.protos.Object && !word) return;
- if (query.filter !== false && word &&
- (query.caseInsensitive ? prop.toLowerCase() : prop).indexOf(word) !== 0) return;
- if (ignoreObj && ignoreObj.props[prop]) return;
- for (var i = 0; i < completions.length; ++i) {
- var c = completions[i];
- if ((wrapAsObjs ? c.name : c) == prop) return;
- }
- var rec = wrapAsObjs ? {name: prop} : prop;
- completions.push(rec);
- if (obj && (query.types || query.docs || query.urls || query.origins)) {
- var val = obj.props[prop];
- infer.resetGuessing();
- var type = val.getType();
- rec.guess = infer.didGuess();
- if (query.types)
- rec.type = infer.toString(val);
- if (query.docs)
- maybeSet(rec, "doc", val.doc || type && type.doc);
- if (query.urls)
- maybeSet(rec, "url", val.url || type && type.url);
- if (query.origins)
- maybeSet(rec, "origin", val.origin || type && type.origin);
- }
- if (query.depths) rec.depth = depth;
- if (wrapAsObjs && addInfo) addInfo(rec);
- }
- var hookname, prop, objType, isKey;
- var exprAt = infer.findExpressionAround(file.ast, null, wordStart, file.scope);
- var memberExpr, objLit;
- // Decide whether this is an object property, either in a member
- // expression or an object literal.
- if (exprAt) {
- if (exprAt.node.type == "MemberExpression" && exprAt.node.object.end < wordStart) {
- memberExpr = exprAt;
- } else if (isStringAround(exprAt.node, wordStart, wordEnd)) {
- var parent = infer.parentNode(exprAt.node, file.ast);
- if (parent.type == "MemberExpression" && parent.property == exprAt.node)
- memberExpr = {node: parent, state: exprAt.state};
- } else if (exprAt.node.type == "ObjectExpression") {
- var objProp = pointInProp(exprAt.node, wordEnd);
- if (objProp) {
- objLit = exprAt;
- prop = isKey = objProp.key.name;
- } else if (!word && !/:\s*$/.test(file.text.slice(0, wordStart))) {
- objLit = exprAt;
- prop = isKey = true;
- }
- }
- }
- if (objLit) {
- // Since we can't use the type of the literal itself to complete
- // its properties (it doesn't contain the information we need),
- // we have to try asking the surrounding expression for type info.
- objType = infer.typeFromContext(file.ast, objLit);
- ignoreObj = objLit.node.objType;
- } else if (memberExpr) {
- prop = memberExpr.node.property;
- prop = prop.type == "Literal" ? prop.value.slice(1) : prop.name;
- memberExpr.node = memberExpr.node.object;
- objType = infer.expressionType(memberExpr);
- } else if (text.charAt(wordStart - 1) == ".") {
- var pathStart = wordStart - 1;
- while (pathStart && (text.charAt(pathStart - 1) == "." || acorn.isIdentifierChar(text.charCodeAt(pathStart - 1)))) pathStart--;
- var path = text.slice(pathStart, wordStart - 1);
- if (path) {
- objType = infer.def.parsePath(path, file.scope).getObjType();
- prop = word;
- }
- }
- if (prop != null) {
- srv.cx.completingProperty = prop;
- if (objType) infer.forAllPropertiesOf(objType, gather);
- if (!completions.length && query.guess !== false && objType && objType.guessProperties)
- objType.guessProperties(function(p, o, d) {if (p != prop && p != "✖") gather(p, o, d);});
- if (!completions.length && word.length >= 2 && query.guess !== false)
- for (var prop in srv.cx.props) gather(prop, srv.cx.props[prop][0], 0);
- hookname = "memberCompletion";
- } else {
- infer.forAllLocalsAt(file.ast, wordStart, file.scope, gather);
- if (query.includeKeywords) jsKeywords.forEach(function(kw) {
- gather(kw, null, 0, function(rec) { rec.isKeyword = true; });
- });
- hookname = "variableCompletion";
- }
- if (srv.passes[hookname])
- srv.passes[hookname].forEach(function(hook) {hook(file, wordStart, wordEnd, gather);});
- if (query.sort !== false) completions.sort(compareCompletions);
- srv.cx.completingProperty = null;
- return {start: outputPos(query, file, wordStart),
- end: outputPos(query, file, wordEnd),
- isProperty: !!prop,
- isObjectKey: !!isKey,
- completions: completions};
- }
- function findProperties(srv, query) {
- var prefix = query.prefix, found = [];
- for (var prop in srv.cx.props)
- if (prop != "<i>" && (!prefix || prop.indexOf(prefix) === 0)) found.push(prop);
- if (query.sort !== false) found.sort(compareCompletions);
- return {completions: found};
- }
- var findExpr = exports.findQueryExpr = function(file, query, wide) {
- if (query.end == null) throw ternError("missing .query.end field");
- if (query.variable) {
- var scope = infer.scopeAt(file.ast, resolvePos(file, query.end), file.scope);
- return {node: {type: "Identifier", name: query.variable, start: query.end, end: query.end + 1},
- state: scope};
- } else {
- var start = query.start && resolvePos(file, query.start), end = resolvePos(file, query.end);
- var expr = infer.findExpressionAt(file.ast, start, end, file.scope);
- if (expr) return expr;
- expr = infer.findExpressionAround(file.ast, start, end, file.scope);
- if (expr && (expr.node.type == "ObjectExpression" || wide ||
- (start == null ? end : start) - expr.node.start < 20 || expr.node.end - end < 20))
- return expr;
- return null;
- }
- };
- function findExprOrThrow(file, query, wide) {
- var expr = findExpr(file, query, wide);
- if (expr) return expr;
- throw ternError("No expression at the given position.");
- }
- function ensureObj(tp) {
- if (!tp || !(tp = tp.getType()) || !(tp instanceof infer.Obj)) return null;
- return tp;
- }
- function findExprType(srv, query, file, expr) {
- var type;
- if (expr) {
- infer.resetGuessing();
- type = infer.expressionType(expr);
- }
- if (srv.passes["typeAt"]) {
- var pos = resolvePos(file, query.end);
- srv.passes["typeAt"].forEach(function(hook) {
- type = hook(file, pos, expr, type);
- });
- }
- if (!type) throw ternError("No type found at the given position.");
- var objProp;
- if (expr.node.type == "ObjectExpression" && query.end != null &&
- (objProp = pointInProp(expr.node, resolvePos(file, query.end)))) {
- var name = objProp.key.name;
- var fromCx = ensureObj(infer.typeFromContext(file.ast, expr));
- if (fromCx && fromCx.hasProp(name)) {
- type = fromCx.hasProp(name);
- } else {
- var fromLocal = ensureObj(type);
- if (fromLocal && fromLocal.hasProp(name))
- type = fromLocal.hasProp(name);
- }
- }
- return type;
- };
- function findTypeAt(srv, query, file) {
- var expr = findExpr(file, query), exprName;
- var type = findExprType(srv, query, file, expr), exprType = type;
- if (query.preferFunction)
- type = type.getFunctionType() || type.getType();
- else
- type = type.getType();
- if (expr) {
- if (expr.node.type == "Identifier")
- exprName = expr.node.name;
- else if (expr.node.type == "MemberExpression" && !expr.node.computed)
- exprName = expr.node.property.name;
- }
- if (query.depth != null && typeof query.depth != "number")
- throw ternError(".query.depth must be a number");
- var result = {guess: infer.didGuess(),
- type: infer.toString(exprType, query.depth),
- name: type && type.name,
- exprName: exprName};
- if (type) storeTypeDocs(type, result);
- if (!result.doc && exprType.doc) result.doc = exprType.doc;
- return clean(result);
- }
- function findDocs(srv, query, file) {
- var expr = findExpr(file, query);
- var type = findExprType(srv, query, file, expr);
- var result = {url: type.url, doc: type.doc, type: infer.toString(type)};
- var inner = type.getType();
- if (inner) storeTypeDocs(inner, result);
- return clean(result);
- }
- function storeTypeDocs(type, out) {
- if (!out.url) out.url = type.url;
- if (!out.doc) out.doc = type.doc;
- if (!out.origin) out.origin = type.origin;
- var ctor, boring = infer.cx().protos;
- if (!out.url && !out.doc && type.proto && (ctor = type.proto.hasCtor) &&
- type.proto != boring.Object && type.proto != boring.Function && type.proto != boring.Array) {
- out.url = ctor.url;
- out.doc = ctor.doc;
- }
- }
- var getSpan = exports.getSpan = function(obj) {
- if (!obj.origin) return;
- if (obj.originNode) {
- var node = obj.originNode;
- if (/^Function/.test(node.type) && node.id) node = node.id;
- return {origin: obj.origin, node: node};
- }
- if (obj.span) return {origin: obj.origin, span: obj.span};
- };
- var storeSpan = exports.storeSpan = function(srv, query, span, target) {
- target.origin = span.origin;
- if (span.span) {
- var m = /^(\d+)\[(\d+):(\d+)\]-(\d+)\[(\d+):(\d+)\]$/.exec(span.span);
- target.start = query.lineCharPositions ? {line: Number(m[2]), ch: Number(m[3])} : Number(m[1]);
- target.end = query.lineCharPositions ? {line: Number(m[5]), ch: Number(m[6])} : Number(m[4]);
- } else {
- var file = srv.findFile(span.origin);
- target.start = outputPos(query, file, span.node.start);
- target.end = outputPos(query, file, span.node.end);
- }
- };
- function findDef(srv, query, file) {
- var expr = findExpr(file, query);
- var type = findExprType(srv, query, file, expr);
- if (infer.didGuess()) return {};
- var span = getSpan(type);
- var result = {url: type.url, doc: type.doc, origin: type.origin};
- if (type.types) for (var i = type.types.length - 1; i >= 0; --i) {
- var tp = type.types[i];
- storeTypeDocs(tp, result);
- if (!span) span = getSpan(tp);
- }
- if (span && span.node) { // refers to a loaded file
- var spanFile = span.node.sourceFile || srv.findFile(span.origin);
- var start = outputPos(query, spanFile, span.node.start), end = outputPos(query, spanFile, span.node.end);
- result.start = start; result.end = end;
- result.file = span.origin;
- var cxStart = Math.max(0, span.node.start - 50);
- result.contextOffset = span.node.start - cxStart;
- result.context = spanFile.text.slice(cxStart, cxStart + 50);
- } else if (span) { // external
- result.file = span.origin;
- storeSpan(srv, query, span, result);
- }
- return clean(result);
- }
- function findRefsToVariable(srv, query, file, expr, checkShadowing) {
- var name = expr.node.name;
- for (var scope = expr.state; scope && !(name in scope.props); scope = scope.prev) {}
- if (!scope) throw ternError("Could not find a definition for " + name + " " + !!srv.cx.topScope.props.x);
- var type, refs = [];
- function storeRef(file) {
- return function(node, scopeHere) {
- if (checkShadowing) for (var s = scopeHere; s != scope; s = s.prev) {
- var exists = s.hasProp(checkShadowing);
- if (exists)
- throw ternError("Renaming `" + name + "` to `" + checkShadowing + "` would make a variable at line " +
- (asLineChar(file, node.start).line + 1) + " point to the definition at line " +
- (asLineChar(file, exists.name.start).line + 1));
- }
- refs.push({file: file.name,
- start: outputPos(query, file, node.start),
- end: outputPos(query, file, node.end)});
- };
- }
- if (scope.originNode) {
- type = "local";
- if (checkShadowing) {
- for (var prev = scope.prev; prev; prev = prev.prev)
- if (checkShadowing in prev.props) break;
- if (prev) infer.findRefs(scope.originNode, scope, checkShadowing, prev, function(node) {
- throw ternError("Renaming `" + name + "` to `" + checkShadowing + "` would shadow the definition used at line " +
- (asLineChar(file, node.start).line + 1));
- });
- }
- infer.findRefs(scope.originNode, scope, name, scope, storeRef(file));
- } else {
- type = "global";
- for (var i = 0; i < srv.files.length; ++i) {
- var cur = srv.files[i];
- infer.findRefs(cur.ast, cur.scope, name, scope, storeRef(cur));
- }
- }
- return {refs: refs, type: type, name: name};
- }
- function findRefsToProperty(srv, query, expr, prop) {
- var objType = infer.expressionType(expr).getObjType();
- if (!objType) throw ternError("Couldn't determine type of base object.");
- var refs = [];
- function storeRef(file) {
- return function(node) {
- refs.push({file: file.name,
- start: outputPos(query, file, node.start),
- end: outputPos(query, file, node.end)});
- };
- }
- for (var i = 0; i < srv.files.length; ++i) {
- var cur = srv.files[i];
- infer.findPropRefs(cur.ast, cur.scope, objType, prop.name, storeRef(cur));
- }
- return {refs: refs, name: prop.name};
- }
- function findRefs(srv, query, file) {
- var expr = findExprOrThrow(file, query, true);
- if (expr.node.type == "Identifier") {
- return findRefsToVariable(srv, query, file, expr);
- } else if (expr.node.type == "MemberExpression" && !expr.node.computed) {
- var p = expr.node.property;
- expr.node = expr.node.object;
- return findRefsToProperty(srv, query, expr, p);
- } else if (expr.node.type == "ObjectExpression") {
- var pos = resolvePos(file, query.end);
- for (var i = 0; i < expr.node.properties.length; ++i) {
- var k = expr.node.properties[i].key;
- if (k.start <= pos && k.end >= pos)
- return findRefsToProperty(srv, query, expr, k);
- }
- }
- throw ternError("Not at a variable or property name.");
- }
- function buildRename(srv, query, file) {
- if (typeof query.newName != "string") throw ternError(".query.newName should be a string");
- var expr = findExprOrThrow(file, query);
- if (!expr || expr.node.type != "Identifier") throw ternError("Not at a variable.");
- var data = findRefsToVariable(srv, query, file, expr, query.newName), refs = data.refs;
- delete data.refs;
- data.files = srv.files.map(function(f){return f.name;});
- var changes = data.changes = [];
- for (var i = 0; i < refs.length; ++i) {
- var use = refs[i];
- use.text = query.newName;
- changes.push(use);
- }
- return data;
- }
- function listFiles(srv) {
- return {files: srv.files.map(function(f){return f.name;})};
- }
- exports.version = "0.11.1";
- });
|