From 74920747944f22683d7f49ad1e792874e9915df2 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Mon, 8 Jun 2015 23:43:46 +0100 Subject: [PATCH] infer types of bindings inside of conditionals based on usage --- src/babel/traversal/path/ancestry.js | 13 +++ src/babel/traversal/path/introspection.js | 27 +++++++ src/babel/traversal/path/resolution.js | 98 +++++++++++++++++++++-- src/babel/types/flow.js | 13 ++- 4 files changed, 144 insertions(+), 7 deletions(-) diff --git a/src/babel/traversal/path/ancestry.js b/src/babel/traversal/path/ancestry.js index 9bb56e4d49..5077a5ca66 100644 --- a/src/babel/traversal/path/ancestry.js +++ b/src/babel/traversal/path/ancestry.js @@ -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 */ diff --git a/src/babel/traversal/path/introspection.js b/src/babel/traversal/path/introspection.js index bd72f259de..719ffaea49 100644 --- a/src/babel/traversal/path/introspection.js +++ b/src/babel/traversal/path/introspection.js @@ -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"; +} diff --git a/src/babel/traversal/path/resolution.js b/src/babel/traversal/path/resolution.js index e4bc3b05ad..d0969e2b18 100644 --- a/src/babel/traversal/path/resolution.js +++ b/src/babel/traversal/path/resolution.js @@ -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() ]); } diff --git a/src/babel/types/flow.js b/src/babel/types/flow.js index 41781ecfe2..9414091ae8 100644 --- a/src/babel/types/flow.js +++ b/src/babel/types/flow.js @@ -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; }