diff --git a/src/babel/transformation/transformers/es6/for-of.js b/src/babel/transformation/transformers/es6/for-of.js index 82c4904d5c..ea928f1b6e 100644 --- a/src/babel/transformation/transformers/es6/for-of.js +++ b/src/babel/transformation/transformers/es6/for-of.js @@ -70,6 +70,10 @@ export function _ForOfStatementArray(node, scope, file) { loop.body.body.unshift(t.expressionStatement(t.assignmentExpression("=", left, iterationValue))); } + if (this.parentPath.isLabeledStatement()) { + loop = t.labeledStatement(this.parentPath.node.label, loop) + } + nodes.push(loop); return nodes; diff --git a/src/babel/transformation/transformers/es6/parameters.rest.js b/src/babel/transformation/transformers/es6/parameters.rest.js index 920caa6424..470a16fe9f 100644 --- a/src/babel/transformation/transformers/es6/parameters.rest.js +++ b/src/babel/transformation/transformers/es6/parameters.rest.js @@ -3,12 +3,14 @@ import * as util from "../../../util"; import * as t from "../../../types"; var memberExpressionOptimisationVisitor = { - enter(node, parent, scope, state) { + Scope(node, parent, scope, state) { // check if this scope has a local binding that will shadow the rest parameter - if (this.isScope() && !scope.bindingIdentifierEquals(state.name, state.outerBinding)) { - return this.skip(); + if (!scope.bindingIdentifierEquals(state.name, state.outerBinding)) { + this.skip(); } + }, + enter(node, parent, scope, state) { var stop = () => { state.canOptimise = false; this.stop(); @@ -31,8 +33,8 @@ var memberExpressionOptimisationVisitor = { if (!state.noOptimise && t.isMemberExpression(parent) && parent.computed) { // if we know that this member expression is referencing a number then we can safely // optimise it - var prop = parent.property; - if (isNumber(prop.value) || t.isUnaryExpression(prop) || t.isBinaryExpression(prop)) { + var prop = this.parentPath.get("property"); + if (prop.isTypeAnnotationGeneric("Number")) { state.candidates.push(this); return; } @@ -43,6 +45,8 @@ var memberExpressionOptimisationVisitor = { }; function optimizeMemberExpression(parent, offset) { + if (offset === 0) return; + var newExpr; var prop = parent.property; diff --git a/src/babel/transformation/transformers/minification/constant-folding.js b/src/babel/transformation/transformers/minification/constant-folding.js index fdc74b016b..480a18814e 100644 --- a/src/babel/transformation/transformers/minification/constant-folding.js +++ b/src/babel/transformation/transformers/minification/constant-folding.js @@ -23,7 +23,10 @@ export function AssignmentExpression() { export function IfStatement() { var evaluated = this.get("test").evaluate(); - if (!evaluated.confident) return this.skip(); + if (!evaluated.confident) { + // todo: deopt binding values for constant violations inside + return this.skip(); + } if (evaluated.value) { this.skipKey("alternate"); diff --git a/src/babel/traversal/path/evaluation.js b/src/babel/traversal/path/evaluation.js index bda8962ccb..67df206136 100644 --- a/src/babel/traversal/path/evaluation.js +++ b/src/babel/traversal/path/evaluation.js @@ -102,15 +102,15 @@ export function evaluate(): { confident: boolean; value: any } { if (path.isReferencedIdentifier()) { var binding = path.scope.getBinding(node.name); - if (binding && binding.hasValue) return binding.value; - } - - if ((path.isIdentifier() || path.isMemberExpression()) && path.isReferenced()) { - var resolved = path.resolve(); - if (resolved === path) { - return confident = false; + if (binding && binding.hasValue) { + return binding.value; } else { - return evaluate(resolved); + var resolved = path.resolve(); + if (resolved === path) { + return confident = false; + } else { + return evaluate(resolved); + } } } diff --git a/src/babel/traversal/path/lib/virtual-types.js b/src/babel/traversal/path/lib/virtual-types.js index 2f6b68104c..f272c524fc 100644 --- a/src/babel/traversal/path/lib/virtual-types.js +++ b/src/babel/traversal/path/lib/virtual-types.js @@ -18,6 +18,17 @@ export var ReferencedIdentifier = { } }; +export var Expression = { + types: ["Expression"], + checkPath(path) { + if (path.isIdentifier()) { + return path.isReferencedIdentifier(); + } else { + return t.isExpression(path.node); + } + } +}; + export var Scope = { types: ["Scopable"], checkPath(path) { diff --git a/src/babel/traversal/path/resolution.js b/src/babel/traversal/path/resolution.js index 3178bac4f9..cdf70f459a 100644 --- a/src/babel/traversal/path/resolution.js +++ b/src/babel/traversal/path/resolution.js @@ -2,53 +2,14 @@ import type NodePath from "./index"; import * as t from "../../types"; /** - * Description + * Resolve a "pointer" `NodePath` to it's absolute path. */ -export function getTypeAnnotation() { - return this.getTypeAnnotationInfo().annotation; +export function resolve(dangerous, resolved) { + return this._resolve(dangerous, resolved) || this; } -/** - * Description - */ - -export function getTypeAnnotationInfo(): { - inferred: boolean; - annotation: ?Object; -} { - if (this.typeInfo) { - return this.typeInfo; - } - - var info = this.typeInfo = { - inferred: false, - annotation: null - }; - - var type = this.node && this.node.typeAnnotation; - - if (!type) { - info.inferred = true; - type = this.inferTypeAnnotation(); - } - - if (t.isTypeAnnotation(type)) type = type.typeAnnotation; - info.annotation = type; - - return info; -} - -/** - * Resolves `NodePath` pointers until it resolves to an absolute path. ie. a data type instead of a - * call etc. If a data type can't be resolved then the last path we were at is returned. - */ - -export function resolve(resolved) { - return this._resolve(resolved) || this; -} - -export function _resolve(resolved?): ?NodePath { +export function _resolve(dangerous?, resolved?): ?NodePath { // detect infinite recursion // todo: possibly have a max length on this just to be safe if (resolved && resolved.indexOf(this) >= 0) return; @@ -59,7 +20,7 @@ export function _resolve(resolved?): ?NodePath { if (this.isVariableDeclarator()) { if (this.get("id").isIdentifier()) { - return this.get("init").resolve(resolved); + return this.get("init").resolve(dangerous, resolved); } else { // otherwise it's a request for a pattern and that's a bit more tricky } @@ -74,9 +35,9 @@ export function _resolve(resolved?): ?NodePath { if (binding.kind === "module") return; if (binding.path !== this) { - return binding.path.resolve(resolved); + return binding.path.resolve(dangerous, resolved); } - } else if (this.isMemberExpression()) { + } else if (dangerous && this.isMemberExpression()) { // this is dangerous, as non-direct target assignments will mutate it's state // making this resolution inaccurate @@ -85,7 +46,7 @@ export function _resolve(resolved?): ?NodePath { var targetName = targetKey.value; - var target = this.get("object").resolve(resolved); + var target = this.get("object").resolve(dangerous, resolved); if (target.isObjectExpression()) { var props = target.get("properties"); @@ -100,58 +61,105 @@ export function _resolve(resolved?): ?NodePath { // { "foo": "obj" } or { ["foo"]: "obj" } match = match || key.isLiteral({ value: targetName }); - if (match) return prop.get("value").resolve(resolved); + if (match) return prop.get("value").resolve(dangerous, resolved); } } else if (target.isArrayExpression() && !isNaN(+targetName)) { var elems = target.get("elements"); var elem = elems[targetName]; - if (elem) return elem.resolve(resolved); + if (elem) return elem.resolve(dangerous, resolved); } } } + /** * Infer the type of the current `NodePath`. * * NOTE: This is not cached. Use `getTypeAnnotation()` which is cached. */ -export function inferTypeAnnotation(force) { - return this._inferTypeAnnotation(force) || t.anyTypeAnnotation(); +export function getTypeAnnotation(force) { + if (this.typeAnnotation) return this.typeAnnotation; + + var type = this._getTypeAnnotation(force) || t.anyTypeAnnotation(); + if (t.isTypeAnnotation(type)) type = type.typeAnnotation; + return this.typeAnnotation = type; } -export function _inferTypeAnnotation(force?: boolean): ?Object { - var path = this.resolve(); +export function _getTypeAnnotationBindingConstantViolations(name, types = []) { + var binding = this.scope.getBinding(name); - var node = path.node; - - if (!node && path.key === "init" && path.parentPath.isVariableDeclarator()) { - return t.voidTypeAnnotation(); + for (var constantViolation of (binding.constantViolations: Array)) { + types.push(constantViolation.getTypeAnnotation()); } - if (!node) return; + if (types.length) { + return t.createUnionTypeAnnotation(types); + } +} + +export function _getTypeAnnotation(force?: boolean): ?Object { + var node = this.node; + + if (!node) { + // handle initializerless variables, add in checks for loop initializers too + if (this.key === "init" && this.parentPath.isVariableDeclarator()) { + var declar = this.parentPath.parentPath; + var declarParent = declar.parentPath; + + // for (var NODE in bar) {} + if (declar.key === "left" && declarParent.isForInStatement()) { + return t.stringTypeAnnotation(); + } + + // for (var NODE of bar) {} + if (declar.key === "left" && declarParent.isForOfStatement()) { + return t.anyTypeAnnotation(); + } + + return t.voidTypeAnnotation(); + } else { + return; + } + } if (node.typeAnnotation) { return node.typeAnnotation; } - if (path.isRestElement() || path.parentPath.isRestElement() || path.isArrayExpression()) { - return t.genericTypeAnnotation(t.identifier("Array")); + // + if (this.isVariableDeclarator()) { + var id = this.get("id"); + + if (id.isIdentifier()) { + return this._getTypeAnnotationBindingConstantViolations(id.node.name, [ + this.get("init").getTypeAnnotation() + ]); + } else { + return; + } } - if (path.parentPath.isTypeCastExpression()) { - return path.parentPath.inferTypeAnnotation(); + // + if (this.parentPath.isTypeCastExpression()) { + return this.parentPath.getTypeAnnotation(); } - if (path.isTypeCastExpression()) { + if (this.isTypeCastExpression()) { return node.typeAnnotation; } - if (path.parentPath.isReturnStatement() && !force) { - return path.parentPath.inferTypeAnnotation(); + // + if (this.isRestElement() || this.parentPath.isRestElement() || this.isArrayExpression()) { + return t.genericTypeAnnotation(t.identifier("Array"), t.typeParameterInstantiation([t.anyTypeAnnotation()])); } - if (path.isReturnStatement()) { + // + if (!force && this.parentPath.isReturnStatement()) { + return this.parentPath.getTypeAnnotation(); + } + + if (this.isReturnStatement()) { var funcPath = this.findParent((node, path) => path.isFunction()); if (!funcPath) return; @@ -159,40 +167,58 @@ export function _inferTypeAnnotation(force?: boolean): ?Object { if (returnType) { return returnType; } else { - return this.get("argument").inferTypeAnnotation(true); + return this.get("argument").getTypeAnnotation(true); } } - if (path.isNewExpression() && path.get("callee").isIdentifier()) { + // + if (this.isNewExpression() && this.get("callee").isIdentifier()) { // only resolve identifier callee return t.genericTypeAnnotation(node.callee); } - if (path.isReferencedIdentifier()) { + // + if (this.isReferencedIdentifier()) { + // check if a binding exists of this value and if so then return a union type of all + // possible types that the binding could be + var binding = this.scope.getBinding(node.name); + if (binding) { + if (binding.identifier.typeAnnotation) { + return binding.identifier.typeAnnotation; + } else { + return this._getTypeAnnotationBindingConstantViolations(node.name, [ + binding.path.getTypeAnnotation() + ]); + } + } + + // built-in values if (node.name === "undefined") { return t.voidTypeAnnotation(); - } else if (node.name === "NaN") { + } else if (node.name === "NaN" || node.name === "Infinity") { return t.numberTypeAnnotation(); + } else if (node.name === "arguments") { + // todo } } - if (path.isObjectExpression()) { + // + if (this.isObjectExpression()) { return t.genericTypeAnnotation(t.identifier("Object")); } - if (path.isFunction() && path.parentPath.isProperty({ kind: "get" })) { - return node.returnType; - } - - if (path.isFunction() || path.isClass()) { + // + if (this.isFunction() || this.isClass()) { return t.genericTypeAnnotation(t.identifier("Function")); } - if (path.isTemplateLiteral()) { + // + if (this.isTemplateLiteral()) { return t.stringTypeAnnotation(); } - if (path.isUnaryExpression()) { + // + if (this.isUnaryExpression()) { let operator = node.operator; if (operator === "void") { @@ -206,7 +232,8 @@ export function _inferTypeAnnotation(force?: boolean): ?Object { } } - if (path.isBinaryExpression()) { + // + if (this.isBinaryExpression()) { let operator = node.operator; if (t.NUMBER_BINARY_OPERATORS.indexOf(operator) >= 0) { @@ -214,17 +241,15 @@ export function _inferTypeAnnotation(force?: boolean): ?Object { } else if (t.BOOLEAN_BINARY_OPERATORS.indexOf(operator) >= 0) { return t.booleanTypeAnnotation(); } else if (operator === "+") { - var right = path.get("right").resolve(); - var left = path.get("left").resolve(); + var right = this.get("right"); + var left = this.get("left"); - if (left || right) { - if (left.isTypeAnnotationGeneric("Number") && right.isTypeAnnotationGeneric("Number")) { - // both numbers so this will be a number - return t.numberTypeAnnotation(); - } else if (left.isTypeAnnotationGeneric("String") || right.isTypeAnnotationGeneric("String")) { - // one is a string so the result will be a string - return t.stringTypeAnnotation(); - } + if (left.isTypeAnnotationGeneric("Number") && right.isTypeAnnotationGeneric("Number")) { + // both numbers so this will be a number + return t.numberTypeAnnotation(); + } else if (left.isTypeAnnotationGeneric("String") || right.isTypeAnnotationGeneric("String")) { + // one is a string so the result will be a string + return t.stringTypeAnnotation(); } // unsure if left and right are strings or numbers so stay on the safe side @@ -235,30 +260,42 @@ export function _inferTypeAnnotation(force?: boolean): ?Object { } } - if (path.isLogicalExpression()) { - // todo: create UnionType of left and right annotations + // + if (this.isLogicalExpression()) { + return t.createUnionTypeAnnotation([ + this.get("left").getTypeAnnotation(), + this.get("right").getTypeAnnotation() + ]); } - if (path.isConditionalExpression()) { - // todo: create UnionType of consequent and alternate annotations + // + if (this.isConditionalExpression()) { + return t.createUnionTypeAnnotation([ + this.get("consequent").getTypeAnnotation(), + this.get("alternate").getTypeAnnotation() + ]); } - if (path.isSequenceExpression()) { - return this.get("expressions").pop().inferTypeAnnotation(force); + // + if (this.isSequenceExpression()) { + return this.get("expressions").pop().getTypeAnnotation(force); } - if (path.isAssignmentExpression()) { - return this.get("right").inferTypeAnnotation(force); + // + if (this.isAssignmentExpression()) { + return this.get("right").getTypeAnnotation(force); } - if (path.isUpdateExpression()) { + // + if (this.isUpdateExpression()) { let operator = node.operator; if (operator === "++" || operator === "--") { return t.numberTypeAnnotation(); } } - if (path.isLiteral()) { + // + if (this.isLiteral()) { var value = node.value; if (typeof value === "string") return t.stringTypeAnnotation(); if (typeof value === "number") return t.numberTypeAnnotation(); @@ -266,12 +303,14 @@ export function _inferTypeAnnotation(force?: boolean): ?Object { if (node.regex) return t.genericTypeAnnotation(t.identifier("RegExp")); } + // var callPath; - if (path.isCallExpression()) callPath = path.get("callee"); - if (path.isTaggedTemplateExpression()) callPath = path.get("tag"); + if (this.isCallExpression()) callPath = this.get("callee"); + if (this.isTaggedTemplateExpression()) callPath = this.get("tag"); if (callPath) { var callee = callPath.resolve(); // todo: read typescript/flow interfaces + if (callee.isFunction()) { if (callee.is("async")) { if (callee.is("generator")) { @@ -280,7 +319,11 @@ export function _inferTypeAnnotation(force?: boolean): ?Object { return t.genericTypeAnnotation(t.identifier("Promise")); } } else { - return callee.node.returnType; + if (callee.node.returnType) { + return callee.node.returnType; + } else { + // todo: get union type of all return arguments + } } } } @@ -290,21 +333,11 @@ export function _inferTypeAnnotation(force?: boolean): ?Object { * Description */ -export function isTypeAnnotationGeneric(genericName: string, opts = {}): boolean { - var typeInfo = this.getTypeAnnotationInfo(); - var type = typeInfo.annotation; - if (!type) return false; - - if (typeInfo.inferred && opts.inference === false) { - return false; - } +export function isTypeAnnotationGeneric(genericName: string): boolean { + var type = this.getTypeAnnotation(); if (t.isGenericTypeAnnotation(type) && t.isIdentifier(type.id, { name: genericName })) { - if (opts.requireTypeParameters && !type.typeParameters) { - return false; - } else { - return true; - } + return true; } if (genericName === "String") { diff --git a/src/babel/traversal/scope/index.js b/src/babel/traversal/scope/index.js index 63a510542a..6ee4e087df 100644 --- a/src/babel/traversal/scope/index.js +++ b/src/babel/traversal/scope/index.js @@ -43,7 +43,7 @@ var collectorVisitor = { ForXStatement() { var left = this.get("left"); if (left.isPattern() || left.isIdentifier()) { - this.scope.registerConstantViolation(left); + this.scope.registerConstantViolation(left, left); } }, @@ -79,15 +79,15 @@ var collectorVisitor = { } // register as constant violation - this.scope.registerConstantViolation(this.get("left"), this.get("right")); + this.scope.registerConstantViolation(this, this.get("left"), this.get("right")); }, UpdateExpression(node, parent, scope) { - scope.registerConstantViolation(this.get("argument"), null); + scope.registerConstantViolation(this, this.get("argument"), null); }, UnaryExpression(node, parent, scope) { - if (node.operator === "delete") scope.registerConstantViolation(this.get("left"), null); + if (node.operator === "delete") scope.registerConstantViolation(this, this.get("left"), null); }, BlockScoped(node, parent, scope) { @@ -456,7 +456,7 @@ export default class Scope { * Description */ - registerConstantViolation(left: NodePath, right: NodePath) { + registerConstantViolation(root: NodePath, left: NodePath, right: NodePath) { var ids = left.getBindingIdentifiers(); for (var name in ids) { var binding = this.getBinding(name); @@ -467,7 +467,7 @@ export default class Scope { if (rightType && binding.isCompatibleWithType(rightType)) continue; } - binding.reassign(left, right); + binding.reassign(root, left, right); } } diff --git a/src/babel/types/alias-keys.json b/src/babel/types/alias-keys.json index 649c56586b..c0b823ac7a 100644 --- a/src/babel/types/alias-keys.json +++ b/src/babel/types/alias-keys.json @@ -79,9 +79,9 @@ "JSXEmptyExpression": ["Expression"], "JSXMemberExpression": ["Expression"], - "AnyTypeAnnotation": ["Flow"], + "AnyTypeAnnotation": ["Flow", "FlowBaseAnnotation"], "ArrayTypeAnnotation": ["Flow"], - "BooleanTypeAnnotation": ["Flow"], + "BooleanTypeAnnotation": ["Flow", "FlowBaseAnnotation"], "ClassImplements": ["Flow"], "DeclareClass": ["Flow", "Statement"], "DeclareFunction": ["Flow", "Statement"], @@ -93,11 +93,11 @@ "InterfaceExtends": ["Flow"], "InterfaceDeclaration": ["Flow", "Statement", "Declaration"], "IntersectionTypeAnnotation": ["Flow"], - "MixedTypeAnnotation": ["Flow"], + "MixedTypeAnnotation": ["Flow", "FlowBaseAnnotation"], "NullableTypeAnnotation": ["Flow"], - "NumberTypeAnnotation": ["Flow"], + "NumberTypeAnnotation": ["Flow", "FlowBaseAnnotation"], "StringLiteralTypeAnnotation": ["Flow"], - "StringTypeAnnotation": ["Flow"], + "StringTypeAnnotation": ["Flow", "FlowBaseAnnotation"], "TupleTypeAnnotation": ["Flow"], "TypeofTypeAnnotation": ["Flow"], "TypeAlias": ["Flow", "Statement"], @@ -111,7 +111,7 @@ "ObjectTypeProperty": ["Flow", "UserWhitespacable"], "QualifiedTypeIdentifier": ["Flow"], "UnionTypeAnnotation": ["Flow"], - "VoidTypeAnnotation": ["Flow"], + "VoidTypeAnnotation": ["Flow", "FlowBaseAnnotation"], "JSXAttribute": ["JSX", "Immutable"], "JSXClosingElement": ["JSX", "Immutable"], diff --git a/src/babel/types/flow.js b/src/babel/types/flow.js new file mode 100644 index 0000000000..13f09c189b --- /dev/null +++ b/src/babel/types/flow.js @@ -0,0 +1,82 @@ +import * as t from "./index"; + +/** + * Takes an array of `types` and flattens them, removing duplicates and + * returns a `UnionTypeAnnotation` node containg them. + */ + +export function createUnionTypeAnnotation(types) { + var flattened = removeTypeDuplicates(types); + + if (flattened.length === 1) { + return flattened[0]; + } else { + return t.unionTypeAnnotation(flattened); + } +} + +export function removeTypeDuplicates(nodes) { + var generics = {}; + var bases = {}; + + var flattened = []; + var types = []; + + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if (!node) continue; + + // this type matches anything + if (t.isAnyTypeAnnotation(node)) { + return [node]; + } + + // + if (t.isFlowBaseAnnotation(node)) { + bases[node.type] = node; + continue; + } + + // + if (t.isUnionTypeAnnotation(node)) { + nodes = nodes.concat(node.types); + continue; + } + + // find a matching generic type and merge and deduplicate the type parameters + if (t.isGenericTypeAnnotation(node)) { + var name = node.id.name; + + if (generics[name]) { + var existing = generics[name]; + if (existing.typeParameters) { + if (node.typeParameters) { + existing.typeParameters.params = removeTypeDuplicates( + existing.typeParameters.params.concat(node.typeParameters.params) + ); + } + } else { + existing = node.typeParameters; + } + } else { + generics[name] = node; + } + + continue; + } + + types.push(node); + } + + // add back in bases + for (var type in bases) { + types.push(bases[type]); + } + + // add back in generics + for (var name in generics) { + types.push(generics[name]); + } + + return types; +} diff --git a/src/babel/types/index.js b/src/babel/types/index.js index 5ef490b0fa..001c9120d5 100644 --- a/src/babel/types/index.js +++ b/src/babel/types/index.js @@ -314,3 +314,4 @@ exports.__esModule = true; assign(t, require("./retrievers")); assign(t, require("./validators")); assign(t, require("./converters")); +assign(t, require("./flow"));