c9-core/plugins/c9.ide.language.javascript..../infer.js

629 wiersze
23 KiB
JavaScript

/**
* Module that implements basic value inference. Type inference in Javascript
* doesn't make a whole lot of sense because it is so dynamic. Therefore, this
* analysis semi-evaluates the Javascript AST and attempts to do simple predictions
* of the values an expression, function or variable may contain.
*/
define(function(require, exports, module) {
var values = require("./values");
var Value = values.Value;
var ValueCollection = values.ValueCollection;
var FunctionValue = values.FunctionValue;
var instantiate = values.instantiate;
var valueFromJSON = values.fromJSON;
var lookupValue = values.lookupValue;
var scopeAnalyzer = require("plugins/c9.ide.language.javascript/scope_analyzer");
var Scope = scopeAnalyzer.Scope;
var Variable = scopeAnalyzer.Variable;
var PROPER = scopeAnalyzer.PROPER;
var MAYBE_PROPER = scopeAnalyzer.MAYBE_PROPER;
var NOT_PROPER = scopeAnalyzer.NOT_PROPER;
var KIND_PACKAGE = scopeAnalyzer.KIND_PACKAGE;
var KIND_DEFAULT = scopeAnalyzer.KIND_DEFAULT;
var path = require("./path");
var completeUtil = require("plugins/c9.ide.language/complete_util");
require('treehugger/traverse');
var registeredSummaries = {};
if (typeof window !== "undefined") {
completeUtil.fetchText("plugins/c9.ide.language.javascript.infer/builtin.jst", function(err, result) {
if (err) return console.error(err);
registeredSummaries.$builtin1$ = JSON.parse(result);
});
}
var filePath;
var basePath;
function registerSummary(guid, summary) {
if (!summary) {
if (registeredSummaries[guid])
delete registeredSummaries[guid];
return;
}
registeredSummaries[guid] = summary;
}
Variable.prototype.addValue = function(value) {
var values = this.values;
for (var i = 0; i < values.length; i++) {
if (values[i].guid === value.guid) {
return;
}
}
values.push(value);
};
/**
* Hints at what the value of a variable may be
* @param variable name
* @param val possible value
*/
Scope.prototype.hint = function(name, v, declarationConfidence, path, row, kind) {
var variable = this.get(name, kind);
if (!variable) {
// Not properly declared variable, implicitly declare it in the current scope
variable = this.declare(name);
}
for (var i = 0; i < variable.values.length; i++) {
if (variable.values[i].guid === v.guid) {
return;
}
}
variable.addValue(v);
return v;
};
Scope.prototype.hintMultiple = function(name, valueColl, declarationConfidence, path, row) {
var variable = this.get(name);
if (!variable) {
// Not properly declared variable, implicitly declare it in the current scope
variable = this.declare(name);
}
valueColl.forEach(function(v) {
for (var i = 0; i < variable.values.length; i++) {
if (variable.values[i].guid === v.guid) {
return;
}
}
variable.addValue(v);
});
};
/**
* Static evaluation of a function
*/
function evalFunction(scope, node, thisValues) {
node.rewrite(
'Function(name, fargs, body)', function(b, node) {
var val = new FunctionValue(b.name.value, node);
if (b.name.value)
scope.hint(b.name.value, val, PROPER, filePath, tryGetRow(node));
var proto = new Value("prototype", node);
val.hint('prototype', proto);
var localScope = this.getAnnotation("localScope");
localScope.fn = val;
localScope.declare("this", undefined, PROPER);
localScope.hint("this", proto, PROPER, filePath, tryGetRow(node));
if (thisValues)
localScope.hintMultiple("this", thisValues, PROPER);
b.fargs.forEach(function(farg, idx) {
var fargName = farg[0].value;
var fargVal;
if (localScope.fnFargs && localScope.fnFargs[idx] && localScope.fnFargs[idx].type) {
fargVal = lookupValue(localScope.fnFargs[idx].type);
}
else {
fargVal = new Value(fargName);
}
val.hint("arg" + idx, fargVal, NOT_PROPER);
localScope.declare(fargName);
localScope.hint(fargName, fargVal, PROPER);
});
Value.enterContext(b.name.value || 'fn');
staticEval(localScope, b.body);
Value.leaveContext();
}
);
}
function hintValue(node, asV, declarationConfidence) {
node.rewrite(
'Var(x)', function(b) {
var scope = this.getAnnotation("scope");
scope.hint(b.x.value, asV, declarationConfidence);
},
'PropAccess(e, x)', function(b) {
var vals = inferValues(b.e);
vals.forEach(function(v) {
v.hint(b.x.value, asV, declarationConfidence);
});
}
);
}
/**
* Statically evaluate the AST node, i.e.
* - A traversal over the AST picking up only certain statements that
* modify variables, properties etc.
*/
function staticEval(scope, node, newFilePath, newBasePath) {
if (newFilePath || newFilePath === "")
filePath = newFilePath;
if (newBasePath || newBasePath === "")
basePath = newBasePath;
node.traverseTopDown(
"Function(_, _, _)", function() {
evalFunction(scope, this);
return this; // Stop traversal
},
"VarDeclInit(name, e)", "ConstDeclInit(name, e)", "LetDeclInit(name, e)", function(b, node) {
staticEval(scope, b.e);
scope.hintMultiple(b.name.value, inferValues(b.e), PROPER, filePath, tryGetRow(node));
return this; // Stop traversal
},
'Assign(PropAccess(e1, prop), e2)', function(b, node) {
staticEval(scope, b.e1);
var vs = inferValues(b.e1);
var isImportant = false;
if (b.e2.cons === 'Function') {
// This is the SomeThing.prototype.myMethod = function() { ... } case
// Let's tell eval the function, hinting that "this" is in fact SomeThing.prototype
evalFunction(scope, b.e2, vs);
isImportant = true;
}
else if (b.e2.cons === 'ObjectInit') {
staticEval(scope, this[0]); // PropAccess(e1, prop)
var vs2 = inferValues(this[0]);
b.e2[0].filter(
'PropertyInit(_, Function(_, _, _))', function(b) {
// Eval as method of vs2
evalFunction(scope, this[1], vs2);
return this;
},
function(b) {
staticEval(scope, this);
}
);
isImportant = true;
}
else {
staticEval(scope, b.e2);
}
var vs3 = inferValues(b.e2);
if (isImportant) {
vs.values.forEach(function(v) {
v.hintMultiple(b.prop.value, vs3, MAYBE_PROPER, filePath, tryGetRow(node));
});
}
else {
vs.values.forEach(function(v) {
v.hintMultiple(b.prop.value, vs3, MAYBE_PROPER);
});
}
return this;
},
"Assign(Var(name), e)", function(b) {
staticEval(scope, this[0]);
staticEval(scope, b.e);
scope.hintMultiple(b.name.value, inferValues(b.e), MAYBE_PROPER);
return this;
},
"ObjectInit(inits)", function(b) {
// When finding an object literal with
var v = new Value("objLit");
var vals = new ValueCollection([v]);
b.inits.filter(
'PropertyInit(prop, Function(_, _, _))', function(b) {
evalFunction(scope, this[1], vals);
v.hintMultiple(b.prop.value, inferValues(this[1]), PROPER, filePath, tryGetRow(this));
},
'PropertyInit(prop, e)', function(b) {
staticEval(scope, b.e);
v.hintMultiple(b.prop.value, inferValues(b.e), PROPER, filePath, tryGetRow(this));
}
);
return this;
},
"OpAssign(op, Var(name), e)", function(b) {
// TODO: Make this type dependent
staticEval(scope, this[1]);
staticEval(scope, b.e);
scope.hintMultiple(b.name.value, inferValues(b.e), MAYBE_PROPER);
if (b.op.value === '*' || b.op.value === '/' || b.op.value === '%' || b.op.value === '-') {
scope.hint(b.name.value, lookupValue('es5:Number/prototype'));
} else if (b.op.value === '+') {
scope.hint(b.name.value, lookupValue('es5:Number/prototype'));
scope.hint(b.name.value, lookupValue('es5:String/prototype'));
}
return this;
},
"PropAccess(e, prop)", function(b) {
staticEval(scope, b.e);
var vs = inferValues(this);
if (!vs.isEmpty()) {
return; // property is defined
}
// Apparently there's a property used in the code that
// is defined elsewhere (or by some other means)
// let's add it to the object
vs = inferValues(b.e);
vs.forEach(function(v) {
v.hint(b.prop.value, new Value(b.prop.value), MAYBE_PROPER);
});
return this;
},
// (function() { ... }).call(Blabla.prototype) pattern
'Call(PropAccess(Function(name, fargs, body), "call"), args)', function(b) {
var fnNode = this[0][0]; // Function(name, ...)
staticEval(scope, b.args);
var objectValues = inferValues(b.args[0]);
var funScope = fnNode.getAnnotation("localScope");
var fargs = b.fargs;
evalFunction(scope, fnNode, objectValues);
for (var i = 0; i < b.args.length - 1; i++) {
inferValues(b.args[i + 1]).forEach(function(v) {
if (i < fargs.length)
funScope.hint(fargs[i].value, v, NOT_PROPER);
objectValues.forEach(function(objV) {
objV.hint('arg' + i, v, NOT_PROPER);
});
});
}
return this;
},
"Call(Var(name), args)", function(b) {
// It's called as a function, hint the inferencer!
var variable = scope.get(b.name.value);
if (!variable) {
// Not defined yet!? Declare it now
variable = scope.declare(b.name.value);
scope.hint(b.name.value, new FunctionValue(b.name.value, null, true), MAYBE_PROPER);
}
else {
var foundFunction = false;
variable.values.forEach(function(v) {
if (v instanceof FunctionValue)
foundFunction = true;
});
if (!foundFunction)
scope.hint(b.name.value, new FunctionValue(b.name.value, null, true), MAYBE_PROPER);
}
staticEval(scope, b.args);
// Now tell the function value about the argument types that were passed
for (var i = 0; i < b.args.length; i++) {
inferValues(b.args[i]).forEach(function(v) {
variable.values.forEach(function(fn) {
if (fn instanceof FunctionValue) {
fn.hint('arg' + i, v, NOT_PROPER);
}
});
});
}
// Ensure there's a return value there
variable.values.forEach(function(fn) {
if (fn instanceof FunctionValue && fn.get("return").isEmpty())
fn.hint('return', new Value("implReturn"), PROPER);
});
return this;
},
"Call(PropAccess(e, prop), args)", function(b) {
// property access is called as a function, let's hint that
staticEval(scope, b.e);
var eValues = inferValues(b.e);
var fnValues = inferValuesPropAccess(eValues, b.prop.value, new ValueCollection());
// Assign known information about the function to its arguments
fnValues.forEach(function(fn) {
if (fn instanceof FunctionValue) {
for (var i = 0; i < b.args.length; i++) {
var fargFargs = fn.fargs && fn.fargs[i] && fn.fargs[i].fargs;
var localScope = b.args[i].getAnnotation("localScope");
if (localScope)
localScope.fnFargs = fargFargs;
}
}
});
staticEval(scope, b.args);
if (fnValues.isEmpty()) {
eValues.forEach(function(v) {
v.hint(b.prop.value, new FunctionValue(b.prop.value, null, true), MAYBE_PROPER);
});
}
// Now tell the function value about the arguments passed
fnValues.forEach(function(fn) {
if (fn instanceof FunctionValue) {
for (var i = 0; i < b.args.length; i++) {
var vs = inferValues(b.args[i]);
vs.forEach(function(v) {
fn.hint('arg' + i, v, NOT_PROPER);
});
}
if (fn.get("return").isEmpty())
fn.hint('return', new Value("implReturn"), PROPER);
}
});
return this;
},
"Return(e)", function(b) {
staticEval(scope, b.e);
scope.fn && scope.fn.hintMultiple('return', inferValues(b.e), PROPER);
return this;
},
"Var(name)", function(b) {
var vs = scope.get(b.name.value);
if (!vs) {
// Implicitly declare it
scope.declare(b.name.value);
scope.hint(b.name.value, new Value(b.name.value, this), MAYBE_PROPER);
}
return this;
},
"ForIn(iter, _, _)", function(b) {
// Hint that iteration variable will be a string
b.iter.rewrite(
"Var(x)", function(b) {
scope.hint(b.x.value, lookupValue("es5:String"));
},
"VarDecls([VarDecl(x)])", function(b) {
scope.hint(b.x.value, lookupValue("es5:String"));
}
);
},
"Op(op, e1, e2)", function(b) {
staticEval(scope, b.e1);
staticEval(scope, b.e2);
switch (b.op.value) {
case '<':
case '<=':
case '>':
case '>=':
hintValue(b.e1, lookupValue("es5:Number"), NOT_PROPER);
hintValue(b.e1, lookupValue("es5:String"), NOT_PROPER);
hintValue(b.e2, lookupValue("es5:Number"), NOT_PROPER);
hintValue(b.e2, lookupValue("es5:String"), NOT_PROPER);
break;
}
return this;
}
);
return scope;
}
/**
* Attempts to infer the value, of possible values of expression `e`
* @param e AST node repersenting an expression
* @return a ValueCollection of possible values
*/
function inferValues(e) {
var values = new ValueCollection();
e.rewrite(
"String(_)", function() {
values.add(lookupValue("es5:String/prototype"));
return this;
},
"RegExp(_,_)", function() {
values.add(lookupValue("es5:RegExp/prototype"));
return this;
},
"Num(_)", function() {
values.add(lookupValue("es5:Number/prototype"));
return this;
},
"Var(\"true\")", function() {
values.add(lookupValue("es5:Boolean/prototype"));
return this;
},
"Var(\"false\")", function() {
values.add(lookupValue("es5:Boolean/prototype"));
return this;
},
"Array(_)", function() {
// TODO Do something with typed arrays
values.add(lookupValue("es5:Array/prototype"));
return this;
},
"Var(nm)", function(b) {
var scope = this.getAnnotation("scope");
if (!scope) {
for (var root = this; root.parent;) root = root.parent;
console.error("[infer] Cannot find scope of " + b.nm + "; analysis "
+ (root.getAnnotation("scope") ? "incomplete" : "may not have been performed yet"));
return;
}
var v = scope.get(b.nm.value) || scope.declare(b.nm.value);
if (v.kind === KIND_DEFAULT)
values.extend(v.values);
return this;
},
"ObjectInit(inits)", function(b) {
var v = instantiate(lookupValue("es5:Object"), undefined, this);
b.inits.filter('PropertyInit(prop, e)', function(b) {
v.hintMultiple(b.prop.value, inferValues(b.e), PROPER, filePath, tryGetRow(this));
});
values.add(v);
return this;
},
"New(e, args)", function(b) {
var vs = inferValues(b.e);
vs.forEach(function(fn) {
var value = instantiate(fn, undefined, undefined, b.e.cons === 'Var' && b.e[0].value);
values.add(value);
});
return this;
},
"Call(Var(\"require\"), [String(name)])", function(b) {
var scope = this[0].getAnnotation("scope");
if (!scope)
return;
var required = b.name.value;
if (path.isRelativePath(required) || path.isRelativePath(required)) {
required = path.canonicalizePath(required, basePath).replace(/^\//, "");
if (!required.match(/\.js$/))
required += ".js";
}
var result = scope.get(required, KIND_PACKAGE);
if (!result)
return;
values.extend(result.values);
return this;
},
"Call(PropAccess(e, method), args)", function(b) {
var objectValues = inferValues(b.e);
objectValues.forEach(function(objectValue) {
var methods = objectValue.get(b.method.value);
methods.forEach(function(fn) {
if (fn instanceof FunctionValue) {
values.extend(fn.get('return'));
}
});
});
if (values.isEmpty())
values.add(new Value("implRet"));
return this;
},
"Call(e, args)", function(b) {
var vs = inferValues(b.e);
vs.forEach(function(fn) {
if (fn instanceof FunctionValue) {
values.extend(fn.get('return'));
}
});
if (values.isEmpty())
values.add(new Value("implRet"));
return this;
},
"PropAccess(e, prop)", function(b) {
inferValuesPropAccess(inferValues(b.e), b.prop.value, values);
return this;
},
"Function(name, fargs, _)", function(b) {
values.add(this.getAnnotation("localScope").fn);
return this;
},
'Assign(e1, e2)', function(b) {
values = inferValues(b.e2);
},
'Op(op, e1, e2)', function(b) {
// Make this dependent on types of operands
switch (b.op.value) {
case '*':
case '/':
case '%':
case '-':
values.add(lookupValue('es5:Number/prototype'));
break;
case '+':
values.add(lookupValue('es5:String/prototype'));
values.add(lookupValue('es5:Number/prototype'));
break;
case '==':
case '===':
case '!==':
case '!=':
case '>':
case '>=':
case '<':
case '<=':
values.add(lookupValue('es5:Boolean/prototype'));
break;
case '||':
case '&&':
values.extend(inferValues(b.e1));
values.extend(inferValues(b.e2));
break;
default:
return false;
}
return this;
},
'PrefixOp(op, e)', function(b) {
switch (b.op.value) {
case '+':
case '-':
case '~':
values.add(lookupValue('es5:Number/prototype'));
break;
case '!':
values.add(lookupValue('es5:Boolean/prototype'));
break;
default:
return false;
}
return this;
}
);
return values;
}
function inferValuesPropAccess(values, propName, results) {
values.forEach(function(val) {
results.extend(val.get(propName));
});
return results;
}
function createRootScope(scope, summaries) {
if (!summaries)
summaries = registeredSummaries;
for (var p in summaries) {
if (!summaries.hasOwnProperty(p))
continue;
var summarySet = summaries[p];
for (var uri in summarySet) {
if (!summarySet.hasOwnProperty(uri))
continue;
var summary = summarySet[uri];
var TypeName = uri.split(':')[1];
if (summary.kind === "default")
summary.kind = KIND_DEFAULT;
scope.declare(TypeName, undefined, PROPER, summary.kind);
var value = valueFromJSON(summary);
scope.hint(TypeName, value, PROPER, summary.path, summary.row, summary.kind);
for (var j = 0; summary.altGuids && j < summary.altGuids.length; j++) {
var guid = summary.altGuids[j].split(':')[1];
scope.declare(guid, undefined, PROPER, summary.kind);
scope.hint(guid, value, PROPER, summary.path, summary.row, summary.kind);
}
if (summary.path) {
scope.declareAlias(summary.kind, TypeName, summary.path);
}
}
}
return scope;
}
function analyze(doc, ast, filePath, basePath, callback) {
scopeAnalyzer.analyze(doc.getValue(), ast, function() {
Value.enterContext('es5:unnamed');
var scope = ast.getAnnotation("scope");
values.reset();
createRootScope(scope);
Value.leaveContext();
Value.enterContext("local:");
staticEval(scope, ast, filePath, basePath);
callback();
}, true);
}
function tryGetRow(node) {
var pos = node.getPos();
return pos ? pos.sl : undefined;
}
exports.registerSummary = registerSummary;
exports.staticEval = staticEval;
exports.inferValues = inferValues;
exports.Scope = Scope;
exports.createRootScope = createRootScope;
exports.analyze = analyze;
});