annotate-esprima/scripts/lib/esprima-master/assets/orion/contentassist/esprimaJsContentAssist.js
2014-04-15 11:04:48 +02:00

2451 lines
78 KiB
JavaScript
Executable file

/*******************************************************************************
* @license
* Copyright (c) 2012 VMware, Inc. All Rights Reserved.
* THIS FILE IS PROVIDED UNDER THE TERMS OF THE ECLIPSE PUBLIC LICENSE
* ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS FILE
* CONSTITUTES RECIPIENTS ACCEPTANCE OF THE AGREEMENT.
* You can obtain a current copy of the Eclipse Public License from
* http://www.opensource.org/licenses/eclipse-1.0.php
*
* Contributors:
* Andy Clement (VMware) - initial API and implementation
* Andrew Eisenberg (VMware) - implemented visitor pattern
******************************************************************************/
/*global define require eclipse esprima window */
define("plugins/esprima/esprimaJsContentAssist", ["plugins/esprima/esprimaVisitor", "plugins/esprima/types", "plugins/esprima/proposalUtils"],
function(mVisitor, mTypes, proposalUtils, scriptedLogger) {
/** @type {function(obj):Boolean} a safe way of checking for arrays */
var isArray = Array.isArray;
if (!isArray) {
isArray = function isArray(ary) {
return Object.prototype.toString.call(ary) === '[object Array]';
};
}
var RESERVED_WORDS = {
"break" : true, "case" : true, "catch" : true, "continue" : true, "debugger" : true, "default" : true, "delete" : true, "do" : true, "else" : true, "finally" : true,
"for" : true, "function" : true, "if" : true, "in" : true, "instanceof" : true, "new" : true, "return" : true, "switch" : true, "this" : true, "throw" : true, "try" : true, "typeof" : true,
"var" : true, "void" : true, "while" : true, "with" : true
};
function isReserverdWord(name) {
return RESERVED_WORDS[name] === true;
}
/**
* @param {String} char a string of at least one char14acter
* @return {boolean} true iff uppercase ascii character
*/
function isUpperCaseChar(c) {
if (c.length < 1) {
return false;
}
var charCode = c.charCodeAt(0);
if (isNaN(charCode)) {
return false;
}
return charCode >= 65 && charCode <= 90;
}
/**
* finds the right-most segment of a dotted MemberExpression
* if it is an identifier, or null otherwise
* @return {{name:String}}
*/
function findRightMost(node) {
if (!node) {
return null;
} else if (node.type === "Identifier") {
return node;
} else if (node.type === "MemberExpression") {
if (node.computed) {
if (node.property.type === "Literal" && typeof node.property.value === "string") {
return node.property;
} else {
// an array access
return node;
}
} else {
return findRightMost(node.property);
}
} else if (node.type === "ArrayExpression") {
return node;
} else {
return null;
}
}
/**
* Recursively generates a name based on the given expression
* @param {{type:String,name:String}} node
* @return {String}
*/
function findDottedName(node) {
if (!node) {
return "";
} else if (node.type === "Identifier") {
return node.name;
} else if (node.type === "MemberExpression") {
var left = findDottedName(node.object);
var right = findDottedName(node.property);
if (left.length > 0 && right.length > 0) {
return left + "." + right;
}
return left + right;
} else if (node.type === "CallExpression") {
return findDottedName(node.callee);
} else {
return "";
}
}
/**
* Convert an array of parameters into a string and also compute linked editing positions
* @param {String} name name of the function
* @param {String} type the type of the function using the following structure '?Type:arg1,arg2,...'
* @param {Number} offset offset
* @return {{ completion:String, positions:Array.<Number> }}
*/
function calculateFunctionProposal(name, type, offset) {
var paramsOffset = mTypes.findReturnTypeEnd(type), paramsStr, params;
paramsStr = paramsOffset > 0 ? type.substring(paramsOffset+1) : "";
params = paramsStr.split(",");
if (!paramsStr || params.length === 0) {
return {completion: name + "()", positions:null};
}
var positions = [];
var completion = name + '(';
var plen = params.length;
for (var p = 0; p < plen; p++) {
if (p > 0) {
completion += ', ';
}
var argName;
if (typeof params[p] === "string") {
// need this because jslintworker.js augments the String prototype with a name() function
// don't want confusion
argName = params[p];
var slashIndex = argName.indexOf('/');
if (slashIndex > 0) {
argName = argName.substring(0, slashIndex);
}
} else if (params[p].name) {
argName = params[p].name();
} else {
argName = params[p];
}
positions.push({offset:offset+completion.length+1, length: argName.length});
completion += argName;
}
completion += ')';
return {completion: completion, positions: positions.length === 0 ? null : positions};
}
/**
* checks that offset overlaps with the given range
* Since esprima ranges are zero-based, inclusive of
* the first char and exclusive of the last char, must
* use a +1 at the end.
* eg- (^ is the line start)
* ^x ---> range[0,0]
* ^ xx ---> range[2,3]
*/
function inRange(offset, range, includeEdge) {
return range[0] <= offset && (includeEdge ? range[1] >= offset : range[1] > offset);
}
/**
* checks that offset is before the range
* @return Boolean
*/
function isBefore(offset, range) {
if (!range) {
return true;
}
return offset < range[0];
}
/**
* Determines if the offset is inside this member expression, but after the '.' and before the
* start of the property.
* eg, the following returns true:
* foo .^bar
* foo . ^ bar
* The following returns false:
* foo ^. bar
* foo . b^ar
* @return Boolean
*/
function afterDot(offset, memberExpr, contents) {
// check for broken AST
var end;
if (memberExpr.property) {
end = memberExpr.property.range[0];
} else {
// no property expression, use the end of the memberExpr as the end to look at
// in this case assume that the member expression ends just after the dot
// this allows content assist invocations to work on the member expression when there
// is no property
end = memberExpr.range[1] + 2;
}
// we are not considered "after" the dot if the offset
// overlaps with the property expression or if the offset is
// after the end of the member expression
if (!inRange(offset-1, memberExpr.range) ||
inRange(offset-1, memberExpr.object.range) ||
offset > end) {
return false;
}
var dotLoc = memberExpr.object.range[1];
while (contents.charAt(dotLoc) !== "." && dotLoc < end) {
dotLoc++;
}
if (contents.charAt(dotLoc) !== ".") {
return false;
}
return dotLoc < offset;
}
/**
* @return "top" if we are at a start of a new expression fragment (eg- at an empty line,
* or a new parameter). "member" if we are after a dot in a member expression. false otherwise
* @return {Boolean|String}
*/
function shouldVisit(root, offset, prefix, contents) {
/**
* A visitor that finds the parent stack at the given location
* @param node the AST node being visited
* @param parents stack of parent nodes for the current node
* @param isInitialVisit true iff this is the first visit of the node, false if this is
* the end visit of the node
*/
var findParent = function(node, parents, isInitialVisit) {
// extras prop is where we stuff everything that we have added
if (!node.extras) {
node.extras = {};
}
if (!isInitialVisit) {
// if we have reached the end of an inRange block expression then
// this means we are completing on an empty expression
if (node.type === "Program" || (node.type === "BlockStatement") &&
inRange(offset, node.range)) {
throw "done";
}
parents.pop();
// return value is ignored
return false;
}
// the program node is always in range even if the range numbers do not line up
if ((node.range && inRange(offset-1, node.range)) || node.type === "Program") {
if (node.type === "Identifier") {
throw "done";
}
parents.push(node);
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression") &&
node.nody && isBefore(offset, node.body.range)) {
// completion occurs on the word "function"
throw "done";
}
// special case where we are completing immediately after a '.'
if (node.type === "MemberExpression" && !node.property && afterDot(offset, node, contents)) {
throw "done";
}
return true;
} else {
return false;
}
};
var parents = [];
try {
mVisitor.visit(root, parents, findParent, findParent);
} catch (done) {
if (done !== "done") {
// a real error
throw(done);
}
}
// determine if we need to defer infering the enclosing function block
var toDefer;
if (parents && parents.length) {
var parent = parents.pop();
for (var i = 0; i < parents.length; i++) {
if ((parents[i].type === "FunctionDeclaration" || parents[i].type === "FunctionExpression") &&
// don't defer if offset is over the function name
!(parents[i].id && inRange(offset, parents[i].id.range, true))) {
toDefer = parents[i];
break;
}
}
if (parent.type === "MemberExpression") {
if (parent.property && inRange(offset-1, parent.property.range)) {
// on the right hand side of a property, eg: foo.b^
return { kind : "member", toDefer : toDefer };
} else if (inRange(offset-1, parent.range) && afterDot(offset, parent, contents)) {
// on the right hand side of a dot with no text after, eg: foo.^
return { kind : "member", toDefer : toDefer };
}
} else if (parent.type === "Program" || parent.type === "BlockStatement") {
// completion at a new expression
if (!prefix) {
}
} else if (parent.type === "VariableDeclarator" && (!parent.init || isBefore(offset, parent.init.range))) {
// the name of a variable declaration
return false;
} else if ((parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression") &&
isBefore(offset, parent.body.range)) {
// a function declaration
return false;
}
}
return { kind : "top", toDefer : toDefer };
}
/**
* finds the final return statement of a function declaration
* @param node an ast statement node
* @return the lexically last ReturnStatment AST node if there is one, else
* null if there is no return statement
*/
function findReturn(node) {
if (!node) {
return null;
}
var type = node.type, maybe, i, last;
// since we are finding the last return statement, start from the end
switch(type) {
case "BlockStatement":
if (node.body && node.body.length > 0) {
last = node.body[node.body.length-1];
if (last.type === "ReturnStatement") {
return last;
} else {
return findReturn(last);
}
}
return null;
case "WhileStatement":
case "DoWhileStatement":
case "ForStatement":
case "ForInStatement":
case "CatchClause":
return findReturn(node.body);
case "IfStatement":
maybe = findReturn(node.alternate);
if (!maybe) {
maybe = findReturn(node.consequent);
}
return maybe;
case "TryStatement":
maybe = findReturn(node.finalizer);
var handlers = node.handlers;
if (!maybe && handlers) {
// start from the last handler
for (i = handlers.length-1; i >= 0; i--) {
maybe = findReturn(handlers[i]);
if (maybe) {
break;
}
}
}
if (!maybe) {
maybe = findReturn(node.block);
}
return maybe;
case "SwitchStatement":
var cases = node.cases;
if (cases) {
// start from the last handler
for (i = cases.length-1; i >= 0; i--) {
maybe = findReturn(cases[i]);
if (maybe) {
break;
}
}
}
return maybe;
case "SwitchCase":
if (node.consequent && node.consequent.length > 0) {
last = node.consequent[node.consequent.length-1];
if (last.type === "ReturnStatement") {
return last;
} else {
return findReturn(last);
}
}
return null;
case "ReturnStatement":
return node;
default:
// don't visit nested functions
// expression statements, variable declarations,
// or any other kind of node
return null;
}
}
/**
* updates a function type to include a new return type.
* function types are specified like this: ?returnType:[arg-n...]
* return type is the name of the return type, arg-n is the name of
* the nth argument.
*/
function updateReturnType(originalFunctionType, newReturnType) {
if (! originalFunctionType) {
// not a valid function type
return newReturnType;
}
var firstChar = originalFunctionType.charAt(0);
if (firstChar !== "?" && firstChar !== "*") {
// not a valid function type
return newReturnType;
}
var end = mTypes.findReturnTypeEnd(originalFunctionType);
if (end < 0) {
// not a valid function type
return newReturnType;
}
return firstChar + newReturnType + originalFunctionType.substring(end);
}
/**
* checks to see if this file looks like an AMD module
* Assumes that there are one or more calls to define at the top level
* and the first statement is a define call
* @return true iff there is a top-level call to 'define'
*/
function checkForAMD(node) {
var body = node.body;
if (body && body.length >= 1 && body[0]) {
if (body[0].type === "ExpressionStatement" &&
body[0].expression &&
body[0].expression.type === "CallExpression" &&
body[0].expression.callee.name === "define") {
// found it.
return body[0].expression;
}
}
return null;
}
/**
* checks to see if this file looks like a wrapped commonjs module
* Assumes that there are one or more calls to define at the top level
* and the first statement is a define call
* @return true iff there is a top-level call to 'define'
*/
function checkForCommonjs(node) {
var body = node.body;
if (body && body.length >= 1) {
for (var i = 0; i < body.length; i++) {
if (body[i] &&
body[i].type === "ExpressionStatement" &&
body[i].expression &&
body[i].expression.type === "CallExpression" &&
body[i].expression.callee.name === "define") {
var callee = body[i].expression;
if (callee["arguments"] &&
callee["arguments"].length === 1 &&
callee["arguments"][0].type === "FunctionExpression" &&
callee["arguments"][0].params.length === 3) {
var params = callee["arguments"][0].params;
if (params[0].name === "require" &&
params[1].name === "exports" &&
params[2].name === "module") {
// found it.
return body[i].expression;
}
}
}
}
}
return null;
}
/**
* if this method call ast node is a call to require with a single string constant
* argument, then look that constant up in the indexer to get a summary
* if a summary is found, then apply it to the current scope
*/
function extractRequireModule(call, env) {
if (!env.indexer) {
return;
}
if (call.type === "CallExpression" && call.callee.type === "Identifier" &&
call.callee.name === "require" && call["arguments"].length === 1) {
var arg = call["arguments"][0];
if (arg.type === "Literal" && typeof arg.value === "string") {
// we're in business
var summary = env.indexer.retrieveSummary(arg.value);
if (summary) {
var typeName;
var mergeTypeName;
if (typeof summary.provided === "string") {
mergeTypeName = typeName = summary.provided;
} else {
// module provides a composite type
// must create a type to add the summary to
mergeTypeName = typeName = env.newScope();
env.popScope();
}
env.mergeSummary(summary, mergeTypeName);
return typeName;
}
}
}
return;
}
/**
* checks to see if this function is a module definition
* and if so returns an array of module definitions
*
* if this is not a module definition, then just return an array of Object for each typ
*/
function findModuleDefinitions(fnode, env) {
var paramTypes = [], params = fnode.params, i;
if (params.length > 0) {
if (!fnode.extras) {
fnode.extras = {};
}
if (env.indexer && fnode.extras.amdDefn) {
var args = fnode.extras.amdDefn["arguments"];
// the function definition must be the last argument of the call to define or require
if (args.length > 1 && args[args.length-1] === fnode) {
// the module names could be the first or second argument
var moduleNames = null;
if (args.length === 3 && args[0].type === "Literal" && args[1].type === "ArrayExpression") {
moduleNames = args[1].elements;
} else if (args.length === 2 && args[0].type === "ArrayExpression") {
moduleNames = args[0].elements;
}
if (moduleNames) {
for (i = 0; i < params.length; i++) {
if (i < moduleNames.length && moduleNames[i].type === "Literal") {
// resolve the module name from the indexer
var summary = env.indexer.retrieveSummary(moduleNames[i].value);
if (summary) {
var typeName;
var mergeTypeName;
if (typeof summary.provided === "string") {
mergeTypeName = typeName = summary.provided;
} else {
// module provides a composite type
// must create a type to add the summary to
mergeTypeName = typeName = env.newScope();
env.popScope();
}
env.mergeSummary(summary, mergeTypeName);
paramTypes.push(typeName);
} else {
paramTypes.push(env.newFleetingObject());
}
} else {
paramTypes.push("Object");
}
}
}
}
}
}
if (paramTypes.length === 0) {
for (i = 0; i < params.length; i++) {
paramTypes.push(env.newFleetingObject());
}
}
return paramTypes;
}
/**
* Finds the closest doc comment to this node
* @param {{range:Array.<Number>}} node
* @param {Array.<{range:Array.<Number>}>} doccomments
* @return {{value:String,range:Array.<Number>}}
*/
function findAssociatedCommentBlock(node, doccomments) {
// look for closest doc comment that is before the start of this node
// just shift all the other ones
var candidate;
while (doccomments.length > 0 && doccomments[0].range[0] < node.range[0]) {
candidate = doccomments.shift();
}
return candidate || { value : null };
}
/**
* Extracts all doccomments that fall inside the given range.
* Side effect is to remove the array elements
* @param Array.<{range:Array.<Number}>> doccomments
* @param Array.<Number> range
* @return {{value:String,range:Array.<Number>}} array elements that are removed
*/
function extractDocComments(doccomments, range) {
var start = 0, end = 0, i, docStart, docEnd;
for (i = 0; i < doccomments.length; i++) {
docStart = doccomments[i].range[0];
docEnd = doccomments[i].range[1];
if (!isBefore(docStart, range) || !isBefore(docEnd, range)) {
break;
}
}
if (i < doccomments.length) {
start = i;
for (i = i; i < doccomments.length; i++) {
docStart = doccomments[i].range[0];
docEnd = doccomments[i].range[1];
if (!inRange(docStart, range, true) || !inRange(docEnd, range, true)) {
break;
}
}
end = i;
}
return doccomments.splice(start, end-start);
}
/**
* This function takes the current AST node and does the first inferencing step for it.
* @param node the AST node to visit
* @param env the context for the visitor. See computeProposals below for full description of contents
*/
function inferencer(node, env) {
var type = node.type, name, i, property, params, newTypeName, jsdocResult, jsdocType;
// extras prop is where we stuff everything that we have added
if (!node.extras) {
node.extras = {};
}
switch(type) {
case "Program":
// check for module kind
env.commonjsModule = checkForCommonjs(node);
if (!env.commonjsModule) {
// can't be both amd and commonjs
env.amdModule = checkForAMD(node);
}
break;
case "BlockStatement":
node.extras.inferredType = env.newScope();
if (node.extras.stop) {
// this BlockStatement inferencing is deferred until after the rest of the file is inferred
inferencerPostOp(node, env);
delete node.extras.stop;
return false;
}
break;
case "Literal":
break;
case "ArrayExpression":
node.extras.inferredType = "Array";
break;
case "ObjectExpression":
if (node.extras.fname) {
// this object expression is contained inside another object expression
env.pushName(node.extras.fname);
}
// for object literals, create a new object type so that we can stuff new properties into it.
// we might be able to do better by walking into the object and inferring each RHS of a
// key-value pair
newTypeName = env.newObject(null, node.range);
node.extras.inferredType = newTypeName;
for (i = 0; i < node.properties.length; i++) {
property = node.properties[i];
// only remember if the property is an identifier
if (property.key && property.key.name) {
// first just add as an object property (or use jsdoc if exists).
// after finishing the ObjectExpression, go and update
// all of the variables to reflect their final inferred type
var docComment = findAssociatedCommentBlock(property.key, env.comments);
jsdocResult = mTypes.parseJSDocComment(docComment);
jsdocType = mTypes.convertJsDocType(jsdocResult.type, env);
var keyType = jsdocType ? jsdocType : "Object";
env.addVariable(property.key.name, node, keyType, property.key.range, docComment.range);
if (!property.key.extras) {
property.key.extras = {};
}
property.key.extras.associatedComment = docComment;
// remember that this is the LHS so that we don't add the identifier to global scope
property.key.extras.isLHS = property.key.extras.isDecl = true;
if (property.value.type === "FunctionExpression" || property.value.type === "ObjectExpression") {
if (!property.value.extras) {
property.value.extras = {};
}
// RHS is a function, remember the name in case it is a constructor
property.value.extras.fname = property.key.name;
property.value.extras.cname = env.getQualifiedName() + property.key.name;
if (property.value.type === "FunctionExpression") {
// now remember the jsdocResult so it doesn't need to be recomputed
property.value.extras.jsdocResult = jsdocResult;
}
}
}
}
break;
case "FunctionDeclaration":
case "FunctionExpression":
var nameRange;
if (node.id) {
// true for function declarations
name = node.id.name;
nameRange = node.id.range;
} else if (node.extras.fname) {
// true for rhs of assignment to function expression
name = node.extras.fname;
nameRange = node.range;
}
params = [];
if (node.params) {
for (i = 0; i < node.params.length; i++) {
params[i] = node.params[i].name;
}
}
if (node.extras.jsdocResult) {
jsdocResult = node.extras.jsdocResult;
docComment = { range : null };
} else {
docComment = node.extras.associatedComment || findAssociatedCommentBlock(node, env.comments);
jsdocResult = mTypes.parseJSDocComment(docComment);
}
// assume that function name that starts with capital is
// a constructor
var isConstuctor;
if (name && node.body && isUpperCaseChar(name)) {
if (node.extras.cname) {
// RHS of assignment
name = node.extras.cname;
}
// create new object so that there is a custom "this"
newTypeName = env.newObject(name, node.range);
isConstuctor = true;
} else {
var jsdocReturn = mTypes.convertJsDocType(jsdocResult.rturn, env);
if (jsdocReturn) {
// keep track of the return type for the way out
node.extras.jsdocReturn = jsdocReturn;
newTypeName = jsdocReturn;
node.extras.inferredType = jsdocReturn;
} else {
// temporarily use "undefined" as type, but this may change once we
// walk through to get to a return statement
newTypeName = "undefined";
}
isConstuctor = false;
}
if (!node.body.extras) {
node.body.extras = {};
}
node.body.extras.isConstructor = isConstuctor;
// add parameters to the current scope
var paramTypeSigs = [], paramSigs = [];
if (params.length > 0) {
var moduleDefs = findModuleDefinitions(node, env);
for (i = 0; i < params.length; i++) {
// choose jsdoc tags over module definitions
var jsDocParam = jsdocResult.params[params[i]];
var typeName = null;
if (jsDocParam) {
typeName = mTypes.convertJsDocType(jsDocParam, env);
}
if (!typeName) {
typeName = moduleDefs[i];
}
paramTypeSigs.push(typeName);
paramSigs.push(params[i] + "/" + typeName);
}
}
var functionTypeName = (isConstuctor ? "*" : "?") + newTypeName + ":" + paramSigs.join(",");
if (isConstuctor) {
env.createConstructor(functionTypeName, newTypeName);
// assume that constructor will be available from global scope using qualified name
// this is not correct in all cases
env.addOrSetGlobalVariable(name, functionTypeName, nameRange, docComment.range);
}
node.extras.inferredType = functionTypeName;
if (name && !isBefore(env.offset, node.range)) {
// if we have a name, then add it to the scope
env.addVariable(name, node.extras.target, functionTypeName, nameRange, docComment.range);
}
// now add the scope for inside the function
env.newScope();
env.addVariable("arguments", node.extras.target, "Arguments", node.range);
// now determine if we need to add 'this'. If this function has an appliesTo, the we know it is being assigned as a property onto something else
// the 'something else' is the 'this' type.
// eg- var obj={};var obj.fun=function() { ... };
var appliesTo = node.extras.appliesTo;
if (appliesTo) {
var appliesToOwner = appliesTo.extras.target;
if (appliesToOwner) {
var ownerTypeName = env.scope(appliesToOwner);
// for the special case of adding to the prototype, we want to make sure that we also add to the 'this' of
// the instantiated types
if (mTypes.isPrototype(ownerTypeName)) {
ownerTypeName = mTypes.extractReturnType(ownerTypeName);
}
env.addVariable("this", node.extras.target, ownerTypeName, nameRange, docComment.range);
}
}
// add variables for all parameters
for (i = 0; i < params.length; i++) {
env.addVariable(params[i], node.extras.target, paramTypeSigs[i], node.params[i].range);
}
break;
case "VariableDeclarator":
if (node.id.name) {
// remember that the identifier is an LHS
// so, don't create a type for it
if (!node.id.extras) {
node.id.extras = {};
}
node.id.extras.isLHS = node.id.extras.isDecl = true;
if (node.init && !node.init.extras) {
node.init.extras = {};
}
if (node.init && node.init.type === "FunctionExpression") {
// RHS is a function, remember the name in case it is a constructor
node.init.extras.fname = node.id.name;
node.init.extras.cname = env.getQualifiedName() + node.id.name;
node.init.extras.fnameRange = node.id.range;
} else {
// not the RHS of a function, check for jsdoc comments
var docComment = findAssociatedCommentBlock(node, env.comments);
jsdocResult = mTypes.parseJSDocComment(docComment);
jsdocType = mTypes.convertJsDocType(jsdocResult.type, env);
node.extras.docRange = docComment.range;
if (jsdocType) {
node.extras.inferredType = jsdocType;
node.extras.jsdocType = jsdocType;
env.addVariable(node.id.name, node.extras.target, jsdocType, node.id.range, docComment.range);
}
}
}
env.pushName(node.id.name);
break;
case "AssignmentExpression":
var rightMost = findRightMost(node.left);
var qualName = env.getQualifiedName() + findDottedName(node.left);
if (rightMost && (rightMost.type === "Identifier" || rightMost.type === "Literal")) {
if (!rightMost.extras) {
rightMost.extras = {};
}
if (node.right.type === "FunctionExpression") {
// RHS is a function, remember the name in case it is a constructor
if (!node.right.extras) {
node.right.extras = {};
}
node.right.extras.appliesTo = rightMost;
node.right.extras.fname = rightMost.name;
node.right.extras.cname = qualName;
node.right.extras.fnameRange = rightMost.range;
if (!node.left.extras) {
node.left.extras = {};
}
}
var docComment = findAssociatedCommentBlock(node, env.comments);
jsdocResult = mTypes.parseJSDocComment(docComment);
jsdocType = mTypes.convertJsDocType(jsdocResult.type, env);
node.extras.docRange = docComment.range;
if (jsdocType) {
node.extras.inferredType = jsdocType;
node.extras.jsdocType = jsdocType;
env.addVariable(rightMost.name, node.extras.target, jsdocType, rightMost.range, docComment.range);
}
}
env.pushName(qualName);
break;
case "CatchClause":
// create a new scope for the catch parameter
node.extras.inferredType = env.newScope();
if (node.param) {
if (!node.param.extras) {
node.param.extras = {};
}
node.param.extras.inferredType = "Error";
env.addVariable(node.param.name, node.extras.target, "Error", node.param.range);
}
break;
case "MemberExpression":
if (node.property) {
if (!node.computed || // like this: foo.at
(node.computed && node.property.type === "Literal" && typeof node.property.value === "string")) { // like this: foo['at']
// keep track of the target of the property expression
// so that its type can be used as the seed for finding properties
if (!node.property.extras) {
node.property.extras = {};
}
node.property.extras.target = node.object;
} else { // like this: foo[at] or foo[0]
// do nothing
}
}
break;
case "CallExpression":
if (node.callee.name === "define" || node.callee.name === "require") {
// check for AMD definition
var args = node["arguments"];
if (args.length > 1 &&
args[args.length-1].type === "FunctionExpression" &&
args[args.length-2].type === "ArrayExpression") {
// assume definition
if (!args[args.length-1].extras) {
args[args.length-1].extras = {};
}
args[args.length-1].extras.amdDefn = node;
}
}
break;
}
// defer the inferencing of the function's children containing the offset.
if (node === env.defer) {
node.extras.associatedComment = findAssociatedCommentBlock(node, env.comments);
node.extras.inferredType = node.extras.inferredType || "Object"; // will be filled in later
// need to remember the scope to place this function in for later
node.extras.scope = env.scope(node.extras.target);
// need to infer the body of this function later
node.body.extras.stop = true;
}
return true;
}
/**
* called as the post operation for the proposalGenerator visitor.
* Finishes off the inferencing and adds all proposals
*/
function inferencerPostOp(node, env) {
var type = node.type, name, inferredType, newTypeName, rightMost, kvps, i;
switch(type) {
case "Program":
if (env.defer) {
// finally, we can infer the deferred target function
// now use the comments that we deferred until later
env.comments = env.deferredComments;
var defer = env.defer;
env.defer = null;
env.targetType = null;
env.pushScope(defer.extras.scope);
mVisitor.visit(defer.body, env, inferencer, inferencerPostOp);
env.popScope();
}
// in case we haven't stored target yet, do so now.
env.storeTarget();
// TODO FIXADE for historical reasons we end visit by throwing exception. Should chamge
throw env.targetType;
case "BlockStatement":
case "CatchClause":
if (inRange(env.offset, node.range)) {
// if we've gotten here and we are still in range, then
// we are completing as a top-level entity with no prefix
env.storeTarget();
}
env.popScope();
break;
case "MemberExpression":
if (afterDot(env.offset, node, env.contents)) {
// completion after a dot with no prefix
env.storeTarget(env.scope(node.object));
}
// for arrays, inferred type is the dereferncing of the array type
// for non-arrays inferred type is the type of the property expression
if (mTypes.isArrayType(node.object.extras.inferredType) && node.computed) {
// inferred type of expression is the type of the dereferenced array
node.extras.inferredType = mTypes.extractArrayParameterType(node.object.extras.inferredType);
} else if (node.computed && node.property && node.property.type !== "Literal") {
// we don't infer parameterized objects, but we have something like this: 'foo[at]' just assume type is object
node.extras.inferredType = "Object";
} else {
// a regular member expression: foo.bar or foo['bar']
// node.propery will be null for mal-formed asts
node.extras.inferredType = node.property ? node.property.extras.inferredType : node.object.extras.inferredType;
}
break;
case "CallExpression":
// first check to see if this is a require call
var fnType = extractRequireModule(node, env);
// otherwise, apply the function
if (!fnType) {
fnType = node.callee.extras.inferredType;
fnType = mTypes.extractReturnType(fnType);
}
node.extras.inferredType = fnType;
break;
case "NewExpression":
// FIXADE we have a problem here.
// constructors that are called like this: new foo.Bar() should have an inferred type of foo.Bar,
// This ensures that another constructor new baz.Bar() doesn't conflict. However,
// we are only taking the final prefix and assuming that it is unique.
node.extras.inferredType = mTypes.extractReturnType(node.callee.extras.inferredType);
break;
case "ObjectExpression":
// now that we know all the types of the values, use that to populate the types of the keys
kvps = node.properties;
for (i = 0; i < kvps.length; i++) {
if (kvps[i].hasOwnProperty("key")) {
// only do this for keys that are identifiers
// set the proper inferred type for the key node
// and also update the variable
name = kvps[i].key.name;
if (name) {
// now check for the special case where the rhs value is an identifier.
// we want to shortcut the navigation and go through to the definition
// of the identifier, BUT only do this if the identifier points to a function
// and the key and value names match.
var range = null;
if (name === kvps[i].value.name) {
var def = env.lookupName(kvps[i].value.name, null, false, true);
if (def && def.range && (mTypes.isFunctionOrConstructor(def.typeName))) {
range = def.range;
}
}
if (!range) {
range = kvps[i].key.range;
}
inferredType = kvps[i].value.extras.inferredType;
var docComment = kvps[i].key.extras.associatedComment;
env.addVariable(name, node, inferredType, range, docComment.range);
if (inRange(env.offset-1, kvps[i].key.range)) {
// We found it! rmember for later, but continue to the end of file anyway
env.storeTarget(env.scope(node));
}
}
}
}
if (node.extras.fname) {
// this object expression is contained inside another object expression
env.popName();
}
env.popScope();
break;
case "LogicalExpression":
case "BinaryExpression":
switch (node.operator) {
case '+':
// special case: if either side is a string, then result is a string
if (node.left.extras.inferredType === "String" ||
node.right.extras.inferredType === "String") {
node.extras.inferredType = "String";
} else {
node.extras.inferredType = "Number";
}
break;
case '-':
case '/':
case '*':
case '%':
case '&':
case '|':
case '^':
case '<<':
case '>>':
case '>>>':
// Numeric and bitwise operations always return a number
node.extras.inferredType = "Number";
break;
case '&&':
case '||':
// will be the type of the left OR the right
// for now arbitrarily choose the left
node.extras.inferredType = node.left.extras.inferredType;
break;
case '!==':
case '!=':
case '===':
case '==':
case '<':
case '<=':
case '>':
case '>=':
node.extras.inferredType = "Boolean";
break;
default:
node.extras.inferredType = "Object";
}
break;
case "UpdateExpression":
case "UnaryExpression":
if (node.operator === '!') {
node.extras.inferredType = "Boolean";
} else {
// includes all unary operations and update operations
// ++ -- - and ~
node.extras.inferredType = "Number";
}
break;
case "FunctionDeclaration":
case "FunctionExpression":
env.popScope();
if (node.body) {
var fnameRange;
if (node.body.extras.isConstructor) {
if (node.id) {
fnameRange = node.id.range;
} else {
fnameRange = node.extras.fnameRange;
}
// an extra scope was created for the implicit 'this'
env.popScope();
// now add a reference to the constructor
env.addOrSetVariable(mTypes.extractReturnType(node.extras.inferredType), node.extras.target, node.extras.inferredType, fnameRange);
} else {
// a regular function. if we don't already know the jsdoc return,
// try updating to a more explicit return type
if (!node.extras.jsdocReturn) {
var returnStatement = findReturn(node.body);
var returnType;
if (returnStatement && returnStatement.extras && returnStatement.extras.inferredType) {
returnType = returnStatement.extras.inferredType;
} else {
returnType = "undefined";
}
node.extras.inferredType = updateReturnType(node.extras.inferredType, returnType);
}
// if there is a name, then update that as well
var fname;
if (node.id) {
// true for function declarations
fname = node.id.name;
fnameRange = node.id.range;
} else if (node.extras.appliesTo) {
// true for rhs of assignment to function expression
fname = node.extras.fname;
fnameRange = node.extras.fnameRange;
}
if (fname) {
env.addOrSetVariable(fname, node.extras.target, node.extras.inferredType, fnameRange);
}
}
}
break;
case "VariableDeclarator":
if (node.init) {
inferredType = node.init.extras.inferredType;
} else {
inferredType = env.newFleetingObject();
}
node.id.extras.inferredType = inferredType;
if (!node.extras.jsdocType) {
node.extras.inferredType = inferredType;
env.addVariable(node.id.name, node.extras.target, inferredType, node.id.range, node.extras.docRange);
}
if (inRange(env.offset-1, node.id.range)) {
// We found it! rmember for later, but continue to the end of file anyway
env.storeTarget(env.scope(node.id.extras.target));
}
env.popName();
break;
case "Property":
node.extras.inferredType = node.key.extras.inferredType = node.value.extras.inferredType;
break;
case "AssignmentExpression":
if (node.extras.jsdocType) {
// use jsdoc instead of whatever we have inferred
inferredType = node.extras.jsdocType;
} else if (node.operator === '=') {
// standard assignment
inferredType = node.right.extras.inferredType;
} else {
// +=, -=, *=, /=, >>=, <<=, >>>=, &=, |=, or ^=.
if (node.operator === '+=' && node.left.extras.inferredType === 'String') {
inferredType = "String";
} else {
inferredType = "Number";
}
}
node.extras.inferredType = inferredType;
// when we have 'this.that.theOther.f' need to find the right-most identifier
rightMost = findRightMost(node.left);
if (rightMost && (rightMost.type === "Identifier" || rightMost.type === "Literal")) {
name = rightMost.name ? rightMost.name : rightMost.value;
rightMost.extras.inferredType = inferredType;
env.addOrSetVariable(name, rightMost.extras.target, inferredType, rightMost.range, node.extras.docRange);
if (inRange(env.offset-1, rightMost.range)) {
// We found it! remember for later, but continue to the end of file anyway
env.storeTarget(env.scope(rightMost.extras.target));
}
} else {
// might be an assignment to an array, like:
// foo[at] = bar;
if (node.left.computed) {
rightMost = findRightMost(node.left.object);
if (rightMost && !(rightMost.type === 'Identifier' && rightMost.name === 'prototype')) {
// yep...now go and update the type of the array
// (also don't turn refs to prototype into an array. this breaks things)
var arrayType = mTypes.parameterizeArray(inferredType);
node.left.extras.inferredType = inferredType;
node.left.object.extras.inferredType = arrayType;
env.addOrSetVariable(rightMost.name, rightMost.extras.target, arrayType, rightMost.range, node.extras.docRange);
}
}
}
env.popName();
break;
case 'Identifier':
name = node.name;
newTypeName = env.lookupName(name, node.extras.target);
if (newTypeName && !node.extras.isDecl) {
// name already exists but we are redeclaring it and so not being overridden
node.extras.inferredType = newTypeName;
if (inRange(env.offset, node.range, true)) {
// We found it! rmember for later, but continue to the end of file anyway
env.storeTarget(env.scope(node.extras.target));
}
} else if (!node.extras.isLHS) {
if (!inRange(env.offset, node.range, true) && !isReserverdWord(name)) {
// we have encountered a read of a variable/property that we have never seen before
if (node.extras.target) {
// this is a property on an object. just add to the target
env.addVariable(name, node.extras.target, env.newFleetingObject(), node.range);
} else {
// add as a global variable
node.extras.inferredType = env.addOrSetGlobalVariable(name, null, node.range).typeName;
}
} else {
// We found it! rmember for later, but continue to the end of file anyway
env.storeTarget(env.scope(node.extras.target));
}
} else {
// if this node is an LHS of an assign, don't store target yet,
// we need to first apply the RHS before applying.
// This will happen in the enclosing assignment or variable declarator
}
break;
case "ThisExpression":
node.extras.inferredType = env.lookupName("this");
if (inRange(env.offset-1, node.range)) {
// We found it! rmember for later, but continue to the end of file anyway
env.storeTarget(env.scope());
}
break;
case "ReturnStatement":
if (node.argument) {
node.extras.inferredType = node.argument.extras.inferredType;
}
break;
case "Literal":
if (node.extras.target && typeof node.value === "string") {
// we are inside a computed member expression.
// find the type of the property referred to if exists
name = node.value;
newTypeName = env.lookupName(name, node.extras.target);
node.extras.inferredType = newTypeName;
} else if (node.extras.target && typeof node.value === "number") {
// inside of an array access
node.extras.inferredType = "Number";
} else {
var oftype = (typeof node.value);
node.extras.inferredType = oftype[0].toUpperCase() + oftype.substring(1, oftype.length);
}
break;
case "ConditionalExpression":
var target = node.consequent ? node.consequent : node.alternate;
if (target) {
node.extras.inferredType = target.extras.inferredType;
}
break;
case "ArrayExpression":
// parameterize this array by the type of its first non-null element
if (node.elements) {
for (i = 0; i < node.elements.length; i++) {
if (node.elements[i]) {
node.extras.inferredType = mTypes.parameterizeArray(node.elements[i].extras.inferredType);
}
}
}
}
if (!node.extras.inferredType) {
node.extras.inferredType = "Object";
}
}
/**
* add variable names from inside a lint global directive
*/
function addLintGlobals(env, lintOptions) {
var i, globName;
if (lintOptions && isArray(lintOptions.global)) {
for (i = 0; i < lintOptions.global.length; i++) {
globName = lintOptions.global[i];
if (!env.lookupName(globName)) {
env.addOrSetVariable(globName);
}
}
}
var comments = env.comments;
if (comments) {
for (i = 0; i < comments.length; i++) {
var range = comments[i].range;
if (comments[i].type === "Block" && comments[i].value.substring(0, "global".length) === "global") {
var globals = comments[i].value;
var splits = globals.split(/\s+/);
// start with 1 to avoid 'global'
for (var j = 1; j < splits.length; j++) {
if (splits[j].length > 0) {
var colonIdx = splits[j].indexOf(':');
if (colonIdx >= 0) {
globName = splits[j].substring(0,colonIdx).trim();
} else {
globName = splits[j].trim();
}
if (!env.lookupName(globName)) {
env.addOrSetVariable(globName, null, null, range);
}
}
}
break;
}
}
}
}
/**
* Adds global variables defined in dependencies
*/
function addIndexedGlobals(env) {
// no indexer means that we should not consult indexes for extra type information
if (env.indexer) {
// get the list of summaries relevant for this file
// add it to the global scope
var summaries = env.indexer.retrieveGlobalSummaries();
for (var fileName in summaries) {
if (summaries.hasOwnProperty(fileName)) {
env.mergeSummary(summaries[fileName], env.globalTypeName());
}
}
}
}
/**
* the prefix of a completion should not be included in the completion itself
* must explicitly remove it
*/
function removePrefix(prefix, string) {
return string.substring(prefix.length);
}
/**
* Determines if the left type name is more general than the right type name.
* Generality (>) is defined as follows:
* undefined > Object > Generated empty type > all other types
*
* A generated empty type is a generated type that has only a $$proto property
* added to it. Additionally, the type specified in the $$proto property is
* either empty or is Object
*
* @param String leftTypeName
* @param String rightTypeName
* @param {{getAllTypes:function():Object}} env
*
* @return Boolean
*/
function leftTypeIsMoreGeneral(leftTypeName, rightTypeName, env) {
function isEmpty(generatedTypeName) {
if (generatedTypeName === "Object" || generatedTypeName === "undefined") {
return true;
} else if (leftTypeName.substring(0, mTypes.GEN_NAME.length) !== mTypes.GEN_NAME) {
return false;
}
var type = env.getAllTypes()[generatedTypeName];
var popCount = 0;
// type should have a $$proto only and nothing else if it is empty
for (var property in type) {
if (type.hasOwnProperty(property)) {
popCount++;
if (popCount > 1) {
break;
}
}
}
if (popCount === 1) {
// we have an empty object literal, must check parent
// must traverse prototype hierarchy to make sure empty
return isEmpty(type.$$proto.typeName);
}
return false;
}
function convertToNumber(typeName) {
if (typeName === "undefined") {
return 0;
} else if (typeName === "Object") {
return 1;
} else if (isEmpty(typeName)) {
return 2;
} else {
return 3;
}
}
if (!rightTypeName) {
return false;
}
var leftNum = convertToNumber(leftTypeName);
// avoid calculating the rightNum if possible
if (leftNum === 0) {
return rightTypeName !== "undefined";
} else if (leftNum === 1) {
return rightTypeName !== "undefined" && rightTypeName !== "Object";
} else if (leftNum === 2) {
return rightTypeName !== "undefined" && rightTypeName !== "Object" && !isEmpty(rightTypeName);
} else {
return false;
}
}
/**
* @return boolean true iff the type contains
* prop. prop must not be coming from Object
*/
function typeContainsProperty(type, prop) {
if (! (prop in type)) {
return false;
}
if (Object.hasOwnProperty(prop)) {
// the propery may be re-defined in the current type
// check that here
return !type.hasOwnProperty(prop);
}
return true;
}
/**
* Creates the environment object that stores type information
* Called differently depending on what job this content assistant is being called to do.
*/
function createEnvironment(options) {
var buffer = options.buffer, uid = options.uid, offset = options.offset, indexer = options.indexer, globalObjName = options.globalObjName;
if (!offset) {
offset = buffer.length+1;
}
// must copy comments because the array is mutable
var comments = [];
if (options.comments) {
for (var i = 0; i < options.comments.length; i++) {
comments[i] = options.comments[i];
}
}
// prefix for generating local types
// need to add a unique id for each file so that types defined in dependencies don't clash with types
// defined locally
var namePrefix = mTypes.GEN_NAME + uid + "~";
return {
/** Each element is the type of the current scope, which is a key into the types array */
_scopeStack : [globalObjName],
/**
* a map of all the types and their properties currently known
* when an indexer exists, local storage will be checked for extra type information
*/
_allTypes : new mTypes.Types(globalObjName),
/** a counter used for creating unique names for object literals and scopes */
_typeCount : 0,
_nameStack : [],
/** if this is an AMD module, then the value of this property is the 'define' call expression */
amdModule : null,
/** if this is a wrapped commonjs module, then the value of this property is the 'define' call expression */
commonjsModule : null,
/** the indexer for thie content assist invocation. Used to track down dependencies */
indexer: indexer,
/** the offset of content assist invocation */
offset : offset,
/** the entire contents being completed on */
contents : buffer,
uid : uid === 'local' ? null : uid,
/** List of comments in the AST*/
comments : comments,
newName: function() {
return namePrefix + this._typeCount++;
},
/**
* Creates a new empty scope and returns the name of the scope
* must call this.popScope() when finished with this scope
*/
newScope: function(range) {
// the prototype is always the currently top level scope
var targetType = this.scope();
var newScopeName = this.newName();
this._allTypes[newScopeName] = {
$$proto : new mTypes.Definition(targetType, range, this.uid)
};
this._scopeStack.push(newScopeName);
return newScopeName;
},
pushScope : function(scopeName) {
this._scopeStack.push(scopeName);
},
pushName : function(name) {
this._nameStack.push(name);
},
popName : function() {
this._nameStack.pop();
},
getQualifiedName : function() {
var name = this._nameStack.join('.');
return name.length > 0 ? name + '.' : name;
},
/**
* Creates a new empty object scope and returns the name of this object
* must call this.popScope() when finished
*/
newObject: function(newObjectName, range) {
// object needs its own scope
this.newScope();
// if no name passed in, create a new one
newObjectName = newObjectName? newObjectName : this.newName();
// assume that objects have their own "this" object
// prototype of Object
this._allTypes[newObjectName] = {
$$proto : new mTypes.Definition("Object", range, this.uid)
};
this.addVariable("this", null, newObjectName, range);
return newObjectName;
},
/**
* like a call to this.newObject(), but the
* object created has not scope added to the scope stack
*/
newFleetingObject : function(name, range) {
var newObjectName = name ? name : this.newName();
this._allTypes[newObjectName] = {
$$proto : new mTypes.Definition("Object", range, this.uid)
};
return newObjectName;
},
/** removes the current scope */
popScope: function() {
// Can't delete old scope since it may have been assigned somewhere
var oldScope = this._scopeStack.pop();
return oldScope;
},
/**
* @param {ASTNode|String} target
* returns the type name for the current scope
* if a target is passed in (optional), then use the
* inferred type of the target instead (if it exists)
*/
scope : function(target) {
if (typeof target === "string") {
return target;
}
if (target && target.extras.inferredType) {
// check for function literal
var inferredType = target.extras.inferredType;
// hmmmm... will be a problem here if there are nested ~protos
if (mTypes.isFunctionOrConstructor(inferredType) && !mTypes.isPrototype(inferredType)) {
var noArgsType = mTypes.removeParameters(inferredType);
if (this._allTypes[noArgsType]) {
return noArgsType;
} else {
return "Function";
}
} else if (mTypes.isArrayType(inferredType)) {
// TODO FIXADE we are losing parameterization here
return "Array";
} else {
return inferredType;
}
} else {
// grab topmost scope
return this._scopeStack[this._scopeStack.length -1];
}
},
globalScope : function() {
return this._allTypes[this._scopeStack[0]];
},
globalTypeName : function() {
return this._scopeStack[0];
},
/**
* adds the name to the target type.
* if target is passed in then use the type corresponding to
* the target, otherwise use the current scope
*
* Will not override an existing variable if the new typeName is "Object" or "undefined"
* Will not add to a built in type
*
* @param {String} name
* @param {String} typeName
* @param {Object} target
* @param {Array.<Number>} range
* @param {Array.<Number>} docRange
*/
addVariable : function(name, target, typeName, range, docRange) {
if (this._allTypes.Object["$_$" + name]) {
// this is a built in property of object. do not redefine
return;
}
var type = this._allTypes[this.scope(target)];
// do not allow augmenting built in types
if (!type.$$isBuiltin) {
// if new type name is not more general than old type, do not replace
if (typeContainsProperty(type, name) && leftTypeIsMoreGeneral(typeName, type[name].typeName, this)) {
// do nuthin
} else {
type[name] = new mTypes.Definition(typeName ? typeName : "Object", range, this.uid);
type[name].docRange = docRange;
return type[name];
}
}
},
addOrSetGlobalVariable : function(name, typeName, range, docRange) {
if (this._allTypes.Object["$_$" + name]) {
// this is a built in property of object. do not redefine
return;
}
return this.addOrSetVariable(name,
// mock an ast node with a global type
{ extras : { inferredType : this.globalTypeName() } }, typeName, range, docRange);
},
/**
* like add variable, but first checks the prototype hierarchy
* if exists in prototype hierarchy, then replace the type
*
* Will not override an existing variable if the new typeName is "Object" or "undefined"
*/
addOrSetVariable : function(name, target, typeName, range, docRange) {
if (name === 'prototype') {
name = '$$proto';
} else if (this._allTypes.Object["$_$" + name]) {
// this is a built in property of object. do not redefine
return;
}
var targetType = this.scope(target);
var current = this._allTypes[targetType], found = false;
// if no type provided, create a new type
typeName = typeName ? typeName : this.newFleetingObject();
var defn;
while (current) {
if (typeContainsProperty(current, name)) {
defn = current[name];
// found it, just overwrite
// do not allow overwriting of built in types
// 3 cases to avoid:
// 1. properties of builtin types cannot be set
// 2. builtin types cannot be redefined
// 3. new type name is more general than old type
if (!current.$$isBuiltin && current.hasOwnProperty(name) &&
!leftTypeIsMoreGeneral(typeName, defn.typeName, this)) {
// since we are just overwriting the type we do not want to change
// the path or the range
defn.typeName = typeName;
if (docRange) {
defn.docRange = docRange;
}
}
found = true;
break;
} else if (current.$$proto) {
current = this._allTypes[current.$$proto.typeName];
} else {
current = null;
}
}
if (!found) {
// not found, so just add to current scope
// do not allow overwriting of built in types
var type = this._allTypes[targetType];
if (!type.$$isBuiltin) {
defn = new mTypes.Definition(typeName, range, this.uid);
defn.docRange = docRange;
type[name] = defn;
}
}
return defn;
},
/** looks up the name in the hierarchy */
lookupName : function(name, target, applyFunction, includeDefinition) {
// translate function names on object into safe names
var swapper = function(name) {
switch (name) {
case "prototype":
return "$$proto";
case "toString":
case "hasOwnProperty":
case "toLocaleString":
case "valueOf":
case "isProtoTypeOf":
case "propertyIsEnumerable":
return "$_$" + name;
default:
return name;
}
};
var innerLookup = function(name, type, allTypes) {
var res = type[name];
var proto = type.$$proto;
if (res) {
return includeDefinition ? res : res.typeName;
} else if (proto) {
return innerLookup(name, allTypes[proto.typeName], allTypes);
} else {
return null;
}
};
var targetType = this._allTypes[this.scope(target)];
// uncomment this if we want to hide errors where there is an unknown type being placed on the scope stack
// if (!targetType) {
// targetType = this.globalScope()
// }
var res = innerLookup(swapper(name), targetType, this._allTypes);
return res;
},
/** removes the variable from the current type */
removeVariable : function(name, target) {
// do not allow deleting properties of built in types
var type = this._allTypes[this.scope(target)];
// 2 cases to avoid:
// 1. properties of builtin types cannot be deleted
// 2. builtin types cannot be deleted from global scope
if (!type.$$isBuiltin && type[name] && !(type[name] && !type.hasOwnProperty(name))) {
delete type[name];
}
},
/**
* adds a file summary to this module
*/
mergeSummary : function(summary, targetTypeName) {
// add the extra types that don't already exists
for (var type in summary.types) {
if (summary.types.hasOwnProperty(type) && !this._allTypes[type]) {
this._allTypes[type] = summary.types[type];
}
}
// now augment the target type with the provided properties
// but only if a composite type is exported
var targetType = this._allTypes[targetTypeName];
if (typeof summary.provided !== 'string') {
for (var providedProperty in summary.provided) {
if (summary.provided.hasOwnProperty(providedProperty)) {
// the targetType may already have the providedProperty defined
// but should override
targetType[providedProperty] = summary.provided[providedProperty];
}
}
}
},
/**
* takes the name of a constructor and converts it into a type.
* We need to ensure that ConstructorName.prototype = { ... } does the
* thing that we expect. This is why we set the $$proto property of the types
*/
createConstructor : function(constructorName, rawTypeName) {
// don't include the parameter names since we don't want them confusing things when exported
constructorName = mTypes.removeParameters(constructorName);
this.newFleetingObject(constructorName);
var flobj = this.newFleetingObject(constructorName + "~proto");
this._allTypes[constructorName].$$proto = new mTypes.Definition(flobj, null, this.uidj);
this._allTypes[rawTypeName].$$proto = new mTypes.Definition(constructorName, null, this.uid);
},
findType : function(typeName) {
if (mTypes.isArrayType(typeName)) {
// TODO is there anything we need to do here?
// parameterized array
typeName = "Array";
}
// trim arguments if a constructor, careful to avoid a constructor prototype
if (typeName.charAt(0) === "?") {
typeName = mTypes.removeParameters(typeName);
if (!this._allTypes[typeName]) {
// function type has not been explicitly added to list
// just return function instead
return this._allTypes.Function;
}
}
return this._allTypes[typeName];
},
getAllTypes : function() {
return this._allTypes;
},
/**
* This function stores the target type
* so it can be used as the result of this inferencing operation
*/
storeTarget : function(targetType) {
if (!this.targetType) {
if (!targetType) {
targetType = this.scope();
}
this.targetType = targetType;
this.targetFound = true;
}
}
};
}
function createProposalDescription(propName, propType, env) {
return propName + " : " + mTypes.createReadableType(propType, env);
}
function createInferredProposals(targetTypeName, env, completionKind, prefix, replaceStart, proposals, relevance) {
var prop, propName, propType, res, type = env.findType(targetTypeName), proto = type.$$proto;
if (!relevance) {
relevance = 100;
}
// start at the top of the prototype hierarchy so that duplicates can be removed
if (proto) {
createInferredProposals(proto.typeName, env, completionKind, prefix, replaceStart, proposals, relevance - 10);
}
// add a separator proposal
proposals['---dummy' + relevance] = {
proposal: '',
description: '---------------------------------',
relevance: relevance -1,
style: 'hr',
unselectable: true
};
// need to look at prototype for global and window objects
// so need to traverse one level up prototype hierarchy if
// the next level is not Object
var realProto = Object.getPrototypeOf(type);
var protoIsObject = !Object.getPrototypeOf(realProto);
for (prop in type) {
if (type.hasOwnProperty(prop) || (!protoIsObject && realProto.hasOwnProperty(prop))) {
if (prop.charAt(0) === "$" && prop.charAt(1) === "$") {
// special property
continue;
}
if (!proto && prop.indexOf("$_$") === 0) {
// no prototype that means we must decode the property name
propName = prop.substring(3);
} else {
propName = prop;
}
if (propName === "this" && completionKind === "member") {
// don't show "this" proposals for non-top-level locations
// (eg- this.this is wrong)
continue;
}
if (!type[prop].typeName) {
// minified files sometimes have invalid property names (eg- numbers). Ignore them)
continue;
}
if (proposalUtils.looselyMatches(prefix, propName)) {
propType = type[prop].typeName;
var first = propType.charAt(0);
if (first === "?" || first === "*") {
// we have a function
res = calculateFunctionProposal(propName,
propType, replaceStart - 1);
var funcDesc = res.completion + " : " + mTypes.createReadableType(propType, env);
proposals["$"+propName] = {
proposal: removePrefix(prefix, res.completion),
description: funcDesc,
positions: res.positions,
escapePosition: replaceStart + res.completion.length,
// prioritize methods over fields
relevance: relevance + 5,
style: 'emphasis'
};
} else {
proposals["$"+propName] = {
proposal: removePrefix(prefix, propName),
relevance: relevance,
description: createProposalDescription(propName, propType, env),
style: 'emphasis'
};
}
}
}
}
}
function createNoninferredProposals(environment, prefix, replaceStart, proposals) {
var proposalAdded = false;
// a property to return is one that is
// 1. defined on the type object
// 2. prefixed by the prefix
// 3. doesn't already exist
// 4. is not an internal property
function isInterestingProperty(type, prop) {
return type.hasOwnProperty(prop) && prop.indexOf(prefix) === 0 && !proposals['$' + prop] && prop !== '$$proto'&& prop !== '$$isBuiltin';
}
function forType(type) {
for (var prop in type) {
if (isInterestingProperty(type, prop)) {
var propType = type[prop].typeName;
var first = propType.charAt(0);
if (first === "?" || first === "*") {
var res = calculateFunctionProposal(prop, propType, replaceStart - 1);
proposals[prop] = {
proposal: removePrefix(prefix, res.completion),
description: createProposalDescription(prop, propType, environment),
positions: res.positions,
escapePosition: replaceStart + res.completion.length,
// prioritize methods over fields
relevance: -99,
style: 'noemphasis'
};
proposalAdded = true;
} else {
proposals[prop] = {
proposal: removePrefix(prefix, prop),
description: createProposalDescription(prop, propType, environment),
relevance: -100,
style: 'noemphasis'
};
proposalAdded = true;
}
}
}
}
var allTypes = environment.getAllTypes();
for (var typeName in allTypes) {
// need to traverse into the prototype
if (allTypes[typeName].$$proto) {
forType(allTypes[typeName]);
}
}
if (proposalAdded) {
proposals['---dummy'] = {
proposal: '',
description: 'Non-inferred proposals',
relevance: -98,
style: 'noemphasis',
unselectable: true
};
}
}
function findUnreachable(currentTypeName, allTypes, alreadySeen) {
if (currentTypeName.charAt(0) === '*') {
// constructors are not stored with their arguments so need to remove them in order to find them
currentTypeName = mTypes.removeParameters(currentTypeName);
}
var currentType = allTypes[currentTypeName];
if (currentType) {
for(var prop in currentType) {
if (currentType.hasOwnProperty(prop) && prop !== '$$isBuiltin' ) {
var propType = currentType[prop].typeName;
while (mTypes.isFunctionOrConstructor(propType) || mTypes.isArrayType(propType)) {
if (!alreadySeen[propType]) {
alreadySeen[propType] = true;
findUnreachable(propType, allTypes, alreadySeen);
}
if (mTypes.isFunctionOrConstructor(propType)) {
propType = mTypes.extractReturnType(propType);
} else if (mTypes.isArrayType(propType)) {
propType = mTypes.extractArrayParameterType(propType);
}
}
if (!alreadySeen[propType]) {
alreadySeen[propType] = true;
findUnreachable(propType, allTypes, alreadySeen);
}
}
}
}
}
/**
* filters types from the environment that should not be exported
*/
function filterTypes(environment, kind, moduleTypeName) {
var allTypes = environment.getAllTypes();
if (kind === "global") {
// for global dependencies must keep the global scope, but remove all builtin global variables
allTypes.clearDefaultGlobal();
} else {
delete allTypes.Global;
}
// recursively walk the type tree to find unreachable types and delete them, too
var reachable = { };
// if we have a function, then the function return type and its prototype are reachable
// also do same if parameterized array type
// in the module, so add them
if (mTypes.isFunctionOrConstructor(moduleTypeName) || mTypes.isArrayType(moduleTypeName)) {
var retType = moduleTypeName;
while (mTypes.isFunctionOrConstructor(retType) || mTypes.isArrayType(retType)) {
if (mTypes.isFunctionOrConstructor(retType)) {
retType = mTypes.removeParameters(retType);
reachable[retType] = true;
var constrType;
if (retType.charAt(0) === "?") {
// this is a function, not a constructor, but we also
// need to expose the constructor if one exists.
constrType = "*" + retType.substring(1);
reachable[constrType] = true;
} else {
constrType = retType;
}
// don't strictly need this if the protoype of the object has been changed, but OK to keep
reachable[constrType + "~proto"] = true;
retType = mTypes.extractReturnType(retType);
} else if (mTypes.isArrayType(retType)) {
retType = mTypes.extractArrayParameterType(retType);
if (retType) {
reachable[retType] = true;
} else {
retType = "Object";
}
}
}
reachable[retType] = true;
}
findUnreachable(moduleTypeName, allTypes, reachable);
for (var prop in allTypes) {
if (allTypes.hasOwnProperty(prop) && !reachable[prop]) {
delete allTypes[prop];
}
}
}
var browserRegExp = /browser\s*:\s*true/;
var nodeRegExp = /node\s*:\s*true/;
function findGlobalObject(comments, lintOptions) {
for (var i = 0; i < comments.length; i++) {
var comment = comments[i];
if (comment.type === "Block" && (comment.value.substring(0, "jslint".length) === "jslint" ||
comment.value.substring(0,"jshint".length) === "jshint")) {
// the lint options section. now look for the browser or node
if (comment.value.match(browserRegExp)) {
return "Window";
} else if (comment.value.match(nodeRegExp)) {
return "Module";
} else {
return "Global";
}
}
}
if (lintOptions && lintOptions.options) {
if (lintOptions.options.browser === true) {
return "Window";
} else if (lintOptions.options.node === true) {
return "Module";
}
}
return "Global";
}
function filterAndSortProposals(proposalsObj) {
// convert from object to array
var proposals = [];
for (var prop in proposalsObj) {
if (proposalsObj.hasOwnProperty(prop)) {
proposals.push(proposalsObj[prop]);
}
}
proposals.sort(function(l,r) {
// sort by relevance and then by name
if (l.relevance > r.relevance) {
return -1;
} else if (r.relevance > l.relevance) {
return 1;
}
var ldesc = l.description.toLowerCase();
var rdesc = r.description.toLowerCase();
if (ldesc < rdesc) {
return -1;
} else if (rdesc < ldesc) {
return 1;
}
return 0;
});
// filter trailing and leading dummies, as well as double dummies
var toRemove = [];
// now remove any leading or trailing dummy proposals as well as double dummies
var i = proposals.length -1;
while (i >= 0 && proposals[i].description.indexOf('---') === 0) {
toRemove[i] = true;
i--;
}
i = 0;
while (i < proposals.length && proposals[i].description.indexOf('---') === 0) {
toRemove[i] = true;
i++;
}
i += 1;
while (i < proposals.length) {
if (proposals[i].description.indexOf('---') === 0 && proposals[i-1].description.indexOf('---') === 0) {
toRemove[i] = true;
}
i++;
}
var newProposals = [];
for (i = 0; i < proposals.length; i++) {
if (!toRemove[i]) {
newProposals.push(proposals[i]);
}
}
return newProposals;
}
/**
* indexer is optional. When there is no indexer passed in
* the indexes will not be consulted for extra references
* @param {{hasDependency,performIndex,retrieveSummary,retrieveGlobalSummaries}} indexer
* @param {{global:[],options:{browser:Boolean}}=} lintOptions optional set of extra lint options that can be overridden in the source (jslint or jshint)
*/
function EsprimaJavaScriptContentAssistProvider(indexer, lintOptions) {
this.indexer = indexer;
this.lintOptions = lintOptions;
}
/**
* Main entry point to provider
*/
EsprimaJavaScriptContentAssistProvider.prototype = {
_doVisit : function(root, environment) {
// first augment the global scope with things we know
addLintGlobals(environment, this.lintOptions);
addIndexedGlobals(environment);
// now we can remove all non-doc comments from the comments list
var newComments = [];
for (var i = 0; i < environment.comments.length; i++) {
if (environment.comments[i].value.charAt(0) === '*') {
newComments.push(environment.comments[i]);
}
}
environment.comments = newComments;
try {
mVisitor.visit(root, environment, inferencer, inferencerPostOp);
} catch (done) {
if (typeof done !== "string") {
// a real error
throw done;
}
return done;
}
throw new Error("The visit function should always end with a throwable");
},
/**
* implements the Orion content assist API
*/
computeProposals: function(buffer, offset, context) {
if (context.selection && context.selection.start !== context.selection.end) {
// only propose if an empty selection.
return null;
}
try {
var root = mVisitor.parse(buffer);
if (!root) {
// assume a bad parse
return null;
}
// note that if selection has length > 0, then just ignore everything past the start
var completionKind = shouldVisit(root, offset, context.prefix, buffer);
if (completionKind) {
var environment = createEnvironment({ buffer: buffer, uid : "local", offset : offset, indexer : this.indexer, globalObjName : findGlobalObject(root.comments, this.lintOptions), comments : root.comments });
// must defer inferring the containing function block until the end
environment.defer = completionKind.toDefer;
if (environment.defer) {
// remove these comments from consideration until we are inferring the deferred
environment.deferredComments = extractDocComments(environment.comments, environment.defer.range);
}
var target = this._doVisit(root, environment);
var proposalsObj = { };
createInferredProposals(target, environment, completionKind.kind, context.prefix, offset - context.prefix.length, proposalsObj);
if (false && !context.inferredOnly) {
// include the entire universe as potential proposals
createNoninferredProposals(environment, context.prefix, offset - context.prefix.length, proposalsObj);
}
return filterAndSortProposals(proposalsObj);
} else {
// invalid completion location
return [];
}
} catch (e) {
if (typeof scriptedLogger !== "undefined") {
scriptedLogger.error(e.message, "CONTENT_ASSIST");
scriptedLogger.error(e.stack, "CONTENT_ASSIST");
}
throw (e);
}
},
_internalFindDefinition : function(buffer, offset, findName) {
var toLookFor;
var root = mVisitor.parse(buffer);
if (!root) {
// assume a bad parse
return null;
}
var funcList = [];
var environment = createEnvironment({ buffer: buffer, uid : "local", offset : offset, indexer : this.indexer, globalObjName : findGlobalObject(root.comments, this.lintOptions), comments : root.comments });
var findIdentifier = function(node) {
if ((node.type === "Identifier" || node.type === "ThisExpression") && inRange(offset, node.range, true)) {
toLookFor = node;
// cut visit short
throw "done";
}
// FIXADE esprima bug...some call expressions have incorrect slocs.
// This is fixed in trunk of esprima.
// after next upgrade of esprima if the following has correct slocs, then
// can remove the second part of the &&
// mUsers.getUser().name
if (node.range[0] > offset &&
(node.type === "ExpressionStatement" ||
node.type === "ReturnStatement" ||
node.type === "ifStatement" ||
node.type === "WhileStatement" ||
node.type === "Program")) {
// not at a valid hover location
throw "no hover";
}
// the last function pushed on is the one that we need to defer
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression") {
funcList.push(node);
}
return true;
};
try {
mVisitor.visit(root, {}, findIdentifier, function(node) {
if (node === funcList[funcList.length-1]) {
funcList.pop();
}
});
} catch (e) {
if (e === "no hover") {
// not at a valid hover location
return null;
} else if (e === "done") {
// valid hover...continue
} else {
// a real exception
throw e;
}
}
if (!toLookFor) {
// no hover target found
return null;
}
// must defer inferring the containing function block until the end
environment.defer = funcList.pop();
if (environment.defer && toLookFor === environment.defer.id) {
// don't defer if target is name of function
delete environment.defer;
}
if (environment.defer) {
// remove these comments from consideration until we are inferring the deferred
environment.deferredComments = extractDocComments(environment.comments, environment.defer.range);
}
var target = this._doVisit(root, environment);
var lookupName = toLookFor.type === "Identifier" ? toLookFor.name : 'this';
var maybeType = environment.lookupName(lookupName, toLookFor.extras.target || target, false, true);
if (maybeType) {
var hover = mTypes.styleAsProperty(lookupName, findName) + " : " + mTypes.createReadableType(maybeType.typeName, environment, true, 0, findName);
maybeType.hoverText = hover;
return maybeType;
} else {
return null;
}
},
/**
* Computes the hover information for the provided offset
*/
computeHover: function(buffer, offset) {
return this._internalFindDefinition(buffer, offset, true);
},
findDefinition : function(buffer, offset) {
return this._internalFindDefinition(buffer, offset, false);
},
/**
* Computes a summary of the file that is suitable to be stored locally and used as a dependency
* in another file
* @param {String} buffer
* @param {String} fileName
*/
computeSummary: function(buffer, fileName) {
var root = mVisitor.parse(buffer);
if (!root) {
// assume a bad parse
return null;
}
var environment = createEnvironment({ buffer: buffer, uid : fileName, globalObjName : findGlobalObject(root.comments, this.lintOptions), comments : root.comments, indexer : this.indexer });
try {
this._doVisit(root, environment);
} catch (e) {
if (typeof scriptedLogger !== "undefined") {
scriptedLogger.error("Problem with: " + fileName, "CONTENT_ASSIST");
scriptedLogger.error(e.message, "CONTENT_ASSIST");
scriptedLogger.error(e.stack, "CONTENT_ASSIST");
}
throw (e);
}
var provided;
var kind;
var modType;
if (environment.amdModule) {
// provide the exports of the AMD module
// the exports is the return value of the final argument
var args = environment.amdModule["arguments"];
if (args && args.length > 0) {
modType = mTypes.extractReturnType(args[args.length-1].extras.inferredType);
} else {
modType = "Object";
}
kind = "AMD";
} else if (environment.commonjsModule) {
// a wrapped commonjs module
// we have already checked the correctness of this function
var exportsParam = environment.commonjsModule["arguments"][0].params[1];
modType = exportsParam.extras.inferredType;
provided = provided = environment.findType(modType);
} else {
// assume a non-module
provided = environment.globalScope();
if (provided.exports) {
// actually, commonjs
kind = "commonjs";
modType = provided.exports.typeName;
} else {
kind = "global";
modType = environment.globalTypeName();
}
}
// simplify the exported type
if (mTypes.isFunctionOrConstructor(modType) || environment.findType(modType).$$isBuiltin) {
// this module provides a built in type or a function
provided = modType;
} else {
// this module provides a composite type
provided = environment.findType(modType);
}
// now filter the builtins since they are always available
filterTypes(environment, kind, modType);
var allTypes = environment.getAllTypes();
return {
provided : provided,
types : allTypes,
kind : kind
};
}
};
return {
EsprimaJavaScriptContentAssistProvider : EsprimaJavaScriptContentAssistProvider
};
});