infer types of bindings inside of conditionals based on usage

This commit is contained in:
Sebastian McKenzie 2015-06-08 23:43:46 +01:00
parent c4a491123e
commit 7492074794
4 changed files with 144 additions and 7 deletions

View File

@ -11,6 +11,19 @@ export function findParent(callback) {
return null;
}
/**
* Description
*/
export function getStatementParent() {
var path = this;
do {
if (Array.isArray(path.container)) {
return path;
}
} while(path = path.parentPath);
}
/**
* Description
*/

View File

@ -223,3 +223,30 @@ export function getSource() {
return "";
}
}
/**
* Description
*/
export function willIMaybeExecutesBefore(target) {
return this._guessExecutionStatusRelativeTo(target) !== "after";
}
export function _guessExecutionStatusRelativeTo(target) {
var self = this.getStatementParent();
target = target.getStatementParent();
var targetFuncParent = target.scope.getFunctionParent();
var selfFuncParent = self.scope.getFunctionParent();
if (targetFuncParent !== selfFuncParent) {
return "function";
}
do {
if (target.container === self.container) {
return target.key > self.key ? "before" : "after";
}
} while(self = self.parentPath);
return "before";
}

View File

@ -37,6 +37,8 @@ export function _resolve(dangerous?, resolved?): ?NodePath {
if (binding.path !== this) {
return binding.path.resolve(dangerous, resolved);
}
} else if (this.isTypeCastExpression()) {
return this.get("expression").resolve(dangerous, resolved);
} else if (dangerous && this.isMemberExpression()) {
// this is dangerous, as non-direct target assignments will mutate it's state
// making this resolution inaccurate
@ -74,8 +76,6 @@ export function _resolve(dangerous?, resolved?): ?NodePath {
/**
* Infer the type of the current `NodePath`.
*
* NOTE: This is not cached. Use `getTypeAnnotation()` which is cached.
*/
export function getTypeAnnotation(force) {
@ -86,12 +86,29 @@ export function getTypeAnnotation(force) {
return this.typeAnnotation = type;
}
export function _getTypeAnnotationBindingConstantViolations(name, types = []) {
export function _getTypeAnnotationBindingConstantViolations(path, name, types = []) {
var binding = this.scope.getBinding(name);
this.typeAnnotation = t.unionTypeAnnotation(types);
for (var constantViolation of (binding.constantViolations: Array)) {
var functions = [];
var constantViolations = getConstantViolationsBefore(binding, path, functions);
var testType = getTypeAnnotationBasedOnConditional(path, name);
if (testType) {
var testConstantViolations = getConstantViolationsBefore(binding, testType.ifStatement);
// remove constant violations observed before the IfStatement
constantViolations = constantViolations.filter((path) => testConstantViolations.indexOf(path) < 0);
// add back on functions which would have been removed by the last filter
constantViolations = constantViolations.concat(functions);
// clear current types and add in observed test type
types = [testType.typeAnnotation];
}
for (var constantViolation of (constantViolations: Array)) {
types.push(constantViolation.getTypeAnnotation());
}
@ -100,6 +117,75 @@ export function _getTypeAnnotationBindingConstantViolations(name, types = []) {
}
}
function getConstantViolationsBefore(binding, path, functions) {
var violations = binding.constantViolations.slice();
violations.push(binding.path);
return violations.filter((violation) => {
violation = violation.resolve();
var status = violation._guessExecutionStatusRelativeTo(path);
if (functions && status === "function") functions.push(violation);
return status !== "after";
});
}
function checkBinary(path, name) {
var right = path.get("right").resolve();
var left = path.get("left").resolve();
if (left.isIdentifier({ name })) {
return right.getTypeAnnotation();
} else if (right.isIdentifier({ name })) {
return left.getTypeAnnotation();
} else {
return;
}
}
function getParentConditional(path) {
var parentPath;
while (parentPath = path.parentPath) {
if (parentPath.isIfStatement() || parentPath.isConditionalExpression()) {
if (path.key === "test") {
return;
} else {
return parentPath;
}
} else {
path = parentPath;
}
}
}
function getTypeAnnotationBasedOnConditional(path, name) {
var ifStatement = getParentConditional(path);
if (!ifStatement) return;
var test = ifStatement.get("test");
var paths = [test];
var types = [];
do {
var path = paths.shift().resolve();
if (path.isLogicalExpression()) {
paths.push(path.get("left"), path.get("right"));
} else if (path.isBinaryExpression({ operator: "===" })) {
// todo: add in cases where operators imply a number
var type = checkBinary(path, name);
if (type) types.push(type);
}
} while(paths.length);
var typeAnnotation;
if (types.length) {
return {
typeAnnotation: t.createUnionTypeAnnotation(types),
ifStatement
};
} else {
return getTypeAnnotationBasedOnConditional(ifStatement, name);
}
}
/**
* todo: split up this method
*/
@ -143,7 +229,7 @@ export function _getTypeAnnotation(force?: boolean): ?Object {
// just because we're inferring a VariableDeclarator doesn't mean that it's the same
// binding path as it may have been shadowed
if (this.scope.getBinding(id.node.name).path === this) {
return this._getTypeAnnotationBindingConstantViolations(id.node.name, [init]);
return this._getTypeAnnotationBindingConstantViolations(this, id.node.name, [init]);
} else {
return init;
}
@ -198,7 +284,7 @@ export function _getTypeAnnotation(force?: boolean): ?Object {
if (binding.identifier.typeAnnotation) {
return binding.identifier.typeAnnotation;
} else {
return this._getTypeAnnotationBindingConstantViolations(node.name, [
return this._getTypeAnnotationBindingConstantViolations(this, node.name, [
binding.path.getTypeAnnotation()
]);
}

View File

@ -19,12 +19,20 @@ export function removeTypeDuplicates(nodes) {
var generics = {};
var bases = {};
// store union type groups to circular references
var typeGroups = [];
var types = [];
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (!node) continue;
// detect duplicates
if (types.indexOf(node) >= 0) {
continue;
}
// this type matches anything
if (t.isAnyTypeAnnotation(node)) {
return [node];
@ -38,7 +46,10 @@ export function removeTypeDuplicates(nodes) {
//
if (t.isUnionTypeAnnotation(node)) {
nodes = nodes.concat(node.types);
if (typeGroups.indexOf(node.types) < 0) {
nodes = nodes.concat(node.types);
typeGroups.push(node.types);
}
continue;
}