mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-01 14:45:29 +00:00
2f552a73d1
--HG-- extra : commitid : EVn7Fm4DN5H
1057 lines
36 KiB
JavaScript
Executable File
1057 lines
36 KiB
JavaScript
Executable File
// 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/acorn"), require("acorn/walk"));
|
|
if (typeof define == "function" && define.amd) // AMD
|
|
return define(["exports", "./infer", "./signal", "acorn/acorn", "acorn/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); },
|
|
normalizeFilename: function(name) { return name },
|
|
defs: [],
|
|
plugins: {},
|
|
fetchTimeout: 1000,
|
|
dependencyBudget: 20000,
|
|
reuseInstances: true,
|
|
stripCRs: false,
|
|
ecmaVersion: 6,
|
|
projectDir: "/"
|
|
};
|
|
|
|
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 parseFile(srv, file) {
|
|
var options = {
|
|
directSourceFile: file,
|
|
allowReturnOutsideFunction: true,
|
|
allowImportExportEverywhere: true,
|
|
ecmaVersion: srv.options.ecmaVersion
|
|
}
|
|
var text = srv.signalReturnFirst("preParse", file.text, options) || file.text
|
|
var ast = infer.parse(text, options)
|
|
srv.signal("postParse", ast, text)
|
|
return ast
|
|
}
|
|
|
|
function updateText(file, text, srv) {
|
|
file.text = srv.options.stripCRs ? text.replace(/\r\n/g, "\n") : text;
|
|
infer.withContext(srv.cx, function() {
|
|
file.ast = parseFile(srv, file)
|
|
});
|
|
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.projectDir = options.projectDir.replace(/\\/g, "/")
|
|
if (!/\/$/.test(this.projectDir)) this.projectDir += "/"
|
|
|
|
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.mod = {}
|
|
|
|
this.defs = options.defs.slice(0)
|
|
this.plugins = Object.create(null)
|
|
for (var plugin in options.plugins) if (options.plugins.hasOwnProperty(plugin))
|
|
this.loadPlugin(plugin, options.plugins[plugin])
|
|
|
|
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;
|
|
if (!(name in this.fileMap))
|
|
name = this.normalizeFilename(name)
|
|
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;
|
|
}
|
|
this.signal("postReset");
|
|
},
|
|
|
|
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");
|
|
},
|
|
|
|
addDefs: function(defs, toFront) {
|
|
if (toFront) this.defs.unshift(defs)
|
|
else this.defs.push(defs)
|
|
|
|
if (this.cx) this.reset()
|
|
},
|
|
|
|
loadPlugin: function(name, options) {
|
|
if (arguments.length == 1) options = this.options.plugins[name] || true
|
|
if (name in this.plugins || !(name in plugins) || !options) return
|
|
this.plugins[name] = true
|
|
var init = plugins[name](this, options)
|
|
|
|
// This is for backwards-compatibilty. Don't rely on it -- use addDef and on directly
|
|
if (!init) return
|
|
if (init.defs) this.addDefs(init.defs, init.loadFirst)
|
|
if (init.passes) for (var type in init.passes) if (init.passes.hasOwnProperty(type))
|
|
this.on(type, init.passes[type])
|
|
},
|
|
|
|
normalizeFilename: function(name) {
|
|
var norm = this.options.normalizeFilename(name).replace(/\\/g, "/")
|
|
if (norm.indexOf(this.projectDir) == 0) norm = norm.slice(this.projectDir.length)
|
|
return norm
|
|
}
|
|
});
|
|
|
|
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.resetGuessing()
|
|
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.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;
|
|
if (file.offsetLines) offset = {line: file.offsetLines, ch: 0};
|
|
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 += " ";
|
|
file.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 = parseFile(srv, file)
|
|
infer.analyze(file.ast, file.name, scope);
|
|
|
|
// 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(" ");
|
|
|
|
var addCompletion = exports.addCompletion = function(query, completions, name, aval, depth) {
|
|
var typeInfo = query.types || query.docs || query.urls || query.origins;
|
|
var wrapAsObjs = typeInfo || query.depths;
|
|
|
|
for (var i = 0; i < completions.length; ++i) {
|
|
var c = completions[i];
|
|
if ((wrapAsObjs ? c.name : c) == name) return;
|
|
}
|
|
var rec = wrapAsObjs ? {name: name} : name;
|
|
completions.push(rec);
|
|
|
|
if (aval && typeInfo) {
|
|
infer.resetGuessing();
|
|
var type = aval.getType();
|
|
rec.guess = infer.didGuess();
|
|
if (query.types)
|
|
rec.type = infer.toString(aval);
|
|
if (query.docs)
|
|
maybeSet(rec, "doc", parseDoc(query, aval.doc || type && type.doc));
|
|
if (query.urls)
|
|
maybeSet(rec, "url", aval.url || type && type.url);
|
|
if (query.origins)
|
|
maybeSet(rec, "origin", aval.origin || type && type.origin);
|
|
}
|
|
if (query.depths) rec.depth = depth || 0;
|
|
return rec;
|
|
};
|
|
|
|
function findCompletions(srv, query, file) {
|
|
if (query.end == null) throw ternError("missing .query.end field");
|
|
var fromPlugin = srv.signalReturnFirst("completion", file, query)
|
|
if (fromPlugin) return fromPlugin
|
|
|
|
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();
|
|
|
|
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;
|
|
var result = addCompletion(query, completions, prop, obj && obj.props[prop], depth);
|
|
if (addInfo && result && typeof result != "string") addInfo(result);
|
|
}
|
|
|
|
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) {
|
|
var exprNode = exprAt.node;
|
|
if (exprNode.type == "MemberExpression" && exprNode.object.end < wordStart) {
|
|
memberExpr = exprAt;
|
|
} else if (isStringAround(exprNode, wordStart, wordEnd)) {
|
|
var parent = infer.parentNode(exprNode, file.ast);
|
|
if (parent.type == "MemberExpression" && parent.property == exprNode)
|
|
memberExpr = {node: parent, state: exprAt.state};
|
|
} else if (exprNode.type == "ObjectExpression") {
|
|
var objProp = pointInProp(exprNode, 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";
|
|
}
|
|
srv.signal(hookname, 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);
|
|
}
|
|
var typeHandlers = srv.hasHandler("typeAt")
|
|
if (typeHandlers) {
|
|
var pos = resolvePos(file, query.end)
|
|
for (var i = 0; i < typeHandlers.length; i++)
|
|
type = typeHandlers[i](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,
|
|
doc: exprType.doc,
|
|
url: exprType.url};
|
|
if (type) storeTypeDocs(query, type, result);
|
|
|
|
return clean(result);
|
|
}
|
|
|
|
function parseDoc(query, doc) {
|
|
if (!doc) return null;
|
|
if (query.docFormat == "full") return doc;
|
|
var parabreak = /.\n[\s@\n]/.exec(doc);
|
|
if (parabreak) doc = doc.slice(0, parabreak.index + 1);
|
|
doc = doc.replace(/\n\s*/g, " ");
|
|
if (doc.length < 100) return doc;
|
|
var sentenceEnd = /[\.!?] [A-Z]/g;
|
|
sentenceEnd.lastIndex = 80;
|
|
var found = sentenceEnd.exec(doc);
|
|
if (found) doc = doc.slice(0, found.index + 1);
|
|
return doc;
|
|
}
|
|
|
|
function findDocs(srv, query, file) {
|
|
var expr = findExpr(file, query);
|
|
var type = findExprType(srv, query, file, expr);
|
|
var result = {url: type.url, doc: parseDoc(query, type.doc), type: infer.toString(type)};
|
|
var inner = type.getType();
|
|
if (inner) storeTypeDocs(query, inner, result);
|
|
return clean(result);
|
|
}
|
|
|
|
function storeTypeDocs(query, type, out) {
|
|
if (!out.url) out.url = type.url;
|
|
if (!out.doc) out.doc = parseDoc(query, 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 = parseDoc(query, 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: parseDoc(query, type.doc), origin: type.origin};
|
|
|
|
if (type.types) for (var i = type.types.length - 1; i >= 0; --i) {
|
|
var tp = type.types[i];
|
|
storeTypeDocs(query, 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);
|
|
|
|
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 && expr.node.type == "Identifier") {
|
|
return findRefsToVariable(srv, query, file, expr);
|
|
} else if (expr && 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 && 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.16.0";
|
|
});
|