// 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 != "" && (!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"; });