From f4d7cc55c1139bfcb7c17f33241d41ff73829ea2 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Tue, 9 Jun 2015 14:02:57 +0100 Subject: [PATCH] split inference logic into separate folder --- .../transformers/es6/parameters.rest.js | 2 +- .../transformers/es6/template-literals.js | 2 +- src/babel/traversal/path/README.md | 2 +- src/babel/traversal/path/index.js | 2 +- src/babel/traversal/path/inference/index.js | 89 ++++ .../path/inference/inferer-reference.js | 180 +++++++ .../traversal/path/inference/inferers.js | 157 ++++++ src/babel/traversal/path/introspection.js | 87 +++- src/babel/traversal/path/resolution.js | 483 ------------------ src/babel/types/flow.js | 4 +- src/babel/types/index.js | 5 +- 11 files changed, 521 insertions(+), 492 deletions(-) create mode 100644 src/babel/traversal/path/inference/index.js create mode 100644 src/babel/traversal/path/inference/inferer-reference.js create mode 100644 src/babel/traversal/path/inference/inferers.js delete mode 100644 src/babel/traversal/path/resolution.js diff --git a/src/babel/transformation/transformers/es6/parameters.rest.js b/src/babel/transformation/transformers/es6/parameters.rest.js index 7cd88691b3..5154b2aa32 100644 --- a/src/babel/transformation/transformers/es6/parameters.rest.js +++ b/src/babel/transformation/transformers/es6/parameters.rest.js @@ -33,7 +33,7 @@ var memberExpressionOptimisationVisitor = { // if we know that this member expression is referencing a number then we can safely // optimise it var prop = this.parentPath.get("property"); - if (prop.isGenericType("Number")) { + if (prop.isBaseType("number")) { state.candidates.push(this); return; } diff --git a/src/babel/transformation/transformers/es6/template-literals.js b/src/babel/transformation/transformers/es6/template-literals.js index 4fd265fe2b..9c65385f43 100644 --- a/src/babel/transformation/transformers/es6/template-literals.js +++ b/src/babel/transformation/transformers/es6/template-literals.js @@ -16,7 +16,7 @@ function crawl(path) { if (path.is("_templateLiteralProduced")) { crawl(path.get("left")); crawl(path.get("right")); - } else if (!path.isGenericType("String") && !path.isGenericType("Number")) { + } else if (!path.isBaseType("string") && !path.isBaseType("number")) { path.replaceWith(t.callExpression(t.identifier("String"), [path.node])); } } diff --git a/src/babel/traversal/path/README.md b/src/babel/traversal/path/README.md index 5cd632ba30..d0d0a77af6 100644 --- a/src/babel/traversal/path/README.md +++ b/src/babel/traversal/path/README.md @@ -2,8 +2,8 @@ - `context` - Methods responsible for maintaing TraversalContexts. - `conversion` - Methods that convert the path node into another node or some other type of data. - `evaluation` - Methods responsible for statically evaluating code. + - `inference` - Methods responsible for type inferrence etc. - `modification` - Methods that modify the path/node in some ways. - - `resolution` - Methods responsible for type inferrence etc. - `replacement` - Methods responsible for replacing a node with another. - `removal` - Methods responsible for removing a node. - `family` - Methods responsible for dealing with/retrieving children or siblings. diff --git a/src/babel/traversal/path/index.js b/src/babel/traversal/path/index.js index bdbbaf89de..6df4b3c929 100644 --- a/src/babel/traversal/path/index.js +++ b/src/babel/traversal/path/index.js @@ -97,7 +97,7 @@ export default class NodePath { } assign(NodePath.prototype, require("./ancestry")); -assign(NodePath.prototype, require("./resolution")); +assign(NodePath.prototype, require("./inference")); assign(NodePath.prototype, require("./replacement")); assign(NodePath.prototype, require("./evaluation")); assign(NodePath.prototype, require("./conversion")); diff --git a/src/babel/traversal/path/inference/index.js b/src/babel/traversal/path/inference/index.js new file mode 100644 index 0000000000..03e155d1a9 --- /dev/null +++ b/src/babel/traversal/path/inference/index.js @@ -0,0 +1,89 @@ +import * as inferers from "./inferers"; +import * as t from "../../../types"; + +/** + * Infer the type of the current `NodePath`. + */ + +export function getTypeAnnotation() { + if (this.typeAnnotation) return this.typeAnnotation; + + var type = this._getTypeAnnotation() || t.anyTypeAnnotation(); + if (t.isTypeAnnotation(type)) type = type.typeAnnotation; + return this.typeAnnotation = type; +} + +/** + * todo: split up this method + */ + +export function _getTypeAnnotation(): ?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; + } + + var inferer = inferers[node.type]; + if (inferer) { + return inferer.call(this, node); + } + + inferer = inferer[this.parentPath.type]; + if (inferer && inferer.validParent) { + return this.parentPath.getTypeAnnotation(); + } +} + +/** + * Description + */ + +export function isBaseType(baseName: string): boolean { + var type = this.getTypeAnnotation(); + + if (baseName === "string") { + return t.isStringTypeAnnotation(type); + } else if (baseName === "number") { + return t.isNumberTypeAnnotation(type); + } else if (baseName === "boolean") { + return t.isBooleanTypeAnnotation(type); + } else if (baseName === "any") { + return t.isAnyTypeAnnotation(type); + } else if (baseName === "mixed") { + return t.isMixedTypeAnnotation(type); + } else { + throw new Error(`Unknown base type ${baseName}`); + } +} + +/** + * Description + */ + +export function isGenericType(genericName: string): boolean { + var type = this.getTypeAnnotation(); + return t.isGenericTypeAnnotation(type) && t.isIdentifier(type.id, { name: genericName }); +} diff --git a/src/babel/traversal/path/inference/inferer-reference.js b/src/babel/traversal/path/inference/inferer-reference.js new file mode 100644 index 0000000000..faf5e906a9 --- /dev/null +++ b/src/babel/traversal/path/inference/inferer-reference.js @@ -0,0 +1,180 @@ +import * as t from "../../../types"; + +export default function (node) { + if (!this.isReferenced()) return; + + // 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 getTypeAnnotationBindingConstantViolations(this, node.name); + } + } + + // built-in values + if (node.name === "undefined") { + return t.voidTypeAnnotation(); + } else if (node.name === "NaN" || node.name === "Infinity") { + return t.numberTypeAnnotation(); + } else if (node.name === "arguments") { + // todo + } +} + +function getTypeAnnotationBindingConstantViolations(path, name) { + var binding = path.scope.getBinding(name); + + var types = []; + path.typeAnnotation = t.unionTypeAnnotation(types); + + var functionConstantViolations = []; + var constantViolations = getConstantViolationsBefore(binding, path, functionConstantViolations); + + var testType = getConditionalAnnotation(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); + + // clear current types and add in observed test type + types.push(testType.typeAnnotation); + } + + if (constantViolations.length) { + // pick one constant from each scope which will represent the last possible + // control flow path that it could've taken/been + var rawConstantViolations = constantViolations.reverse(); + var visitedScopes = []; + constantViolations = []; + for (let violation of (rawConstantViolations: Array)) { + if (visitedScopes.indexOf(violation.scope) >= 0) continue; + visitedScopes.push(violation.scope); + constantViolations.push(violation); + } + + // add back on function constant violations since we can't track calls + constantViolations = constantViolations.concat(functionConstantViolations); + + // push on inferred types of violated paths + for (let violation of (constantViolations: Array)) { + types.push(violation.getTypeAnnotation()); + } + } + + if (types.length) { + return t.createUnionTypeAnnotation(types); + } +} + +function getConstantViolationsBefore(binding, path, functions) { + var violations = binding.constantViolations.slice(); + violations.unshift(binding.path); + return violations.filter((violation) => { + violation = violation.resolve(); + var status = violation._guessExecutionStatusRelativeTo(path); + if (functions && status === "function") functions.push(violation); + return status === "before"; + }); +} + +function inferAnnotationFromBinaryExpression(name, path) { + var operator = path.node.operator; + + var right = path.get("right").resolve(); + var left = path.get("left").resolve(); + + var target; + if (left.isIdentifier({ name })) { + target = right; + } else if (right.isIdentifier({ name })) { + target = left; + } + if (target) { + if (operator === "===") { + return target.getTypeAnnotation(); + } else if (t.BOOLEAN_NUMBER_BINARY_OPERATORS.indexOf(operator) >= 0) { + return t.numberTypeAnnotation(); + } else { + return; + } + } else { + if (operator !== "===") return; + } + + // + var typeofPath; + var typePath; + if (left.isUnaryExpression({ operator: "typeof" })) { + typeofPath = left; + typePath = right; + } else if (right.isUnaryExpression({ operator: "typeof" })) { + typeofPath = right; + typePath = left; + } + if (!typePath && !typeofPath) return; + + // ensure that the type path is a Literal + typePath = typePath.resolve(); + if (!typePath.isLiteral()) return; + + // and that it's a string so we can infer it + var typeValue = typePath.node.value; + if (typeof typeValue !== "string") return; + + // and that the argument of the typeof path references us! + if (!typeofPath.get("argument").isIdentifier({ name })) return; + + // turn type value into a type annotation + return t.createTypeAnnotationBasedOnTypeof(typePath.node.value); +} + +function getParentConditionalPath(path) { + var parentPath; + while (parentPath = path.parentPath) { + if (parentPath.isIfStatement() || parentPath.isConditionalExpression()) { + if (path.key === "test") { + return; + } else { + return parentPath; + } + } else { + path = parentPath; + } + } +} + +function getConditionalAnnotation(path, name) { + var ifStatement = getParentConditionalPath(path); + if (!ifStatement) return; + + var test = ifStatement.get("test"); + var paths = [test]; + var types = []; + + do { + let path = paths.shift().resolve(); + + if (path.isLogicalExpression()) { + paths.push(path.get("left")); + paths.push(path.get("right")); + } + + if (path.isBinaryExpression()) { + var type = inferAnnotationFromBinaryExpression(name, path); + if (type) types.push(type); + } + } while(paths.length); + + if (types.length) { + return { + typeAnnotation: t.createUnionTypeAnnotation(types), + ifStatement + }; + } else { + return getConditionalAnnotation(ifStatement, name); + } +} diff --git a/src/babel/traversal/path/inference/inferers.js b/src/babel/traversal/path/inference/inferers.js new file mode 100644 index 0000000000..2e6747f086 --- /dev/null +++ b/src/babel/traversal/path/inference/inferers.js @@ -0,0 +1,157 @@ +import * as t from "../../../types"; + +export { default as Identifier } from "./inferer-reference"; + +export function VariableDeclarator() { + var id = this.get("id"); + + if (id.isIdentifier()) { + return this.get("init").getTypeAnnotation(); + } else { + return; + } +} + +export function TypeCastExpression(node) { + return node.typeAnnotation; +} + +TypeCastExpression.validParent = true; + +export function NewExpression(node) { + if (this.get("callee").isIdentifier()) { + // only resolve identifier callee + return t.genericTypeAnnotation(node.callee); + } +} + +export function TemplateLiteral() { + return t.stringTypeAnnotation(); +} + +export function UnaryExpression(node) { + let operator = node.operator; + + if (operator === "void") { + return t.voidTypeAnnotation(); + } else if (t.NUMBER_UNARY_OPERATORS.indexOf(operator) >= 0) { + return t.numberTypeAnnotation(); + } else if (t.STRING_UNARY_OPERATORS.indexOf(operator) >= 0) { + return t.stringTypeAnnotation(); + } else if (t.BOOLEAN_UNARY_OPERATORS.indexOf(operator) >= 0) { + return t.booleanTypeAnnotation(); + } +} + +export function BinaryExpression(node) { + let operator = node.operator; + + if (t.NUMBER_BINARY_OPERATORS.indexOf(operator) >= 0) { + return t.numberTypeAnnotation(); + } else if (t.BOOLEAN_BINARY_OPERATORS.indexOf(operator) >= 0) { + return t.booleanTypeAnnotation(); + } else if (operator === "+") { + var right = this.get("right"); + var left = this.get("left"); + + if (left.isBaseType("number") && right.isBaseType("number")) { + // both numbers so this will be a number + return t.numberTypeAnnotation(); + } else if (left.isBaseType("string") || right.isBaseType("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 + return t.unionTypeAnnotation([ + t.stringTypeAnnotation(), + t.numberTypeAnnotation() + ]); + } +} + +export function LogicalExpression() { + return t.createUnionTypeAnnotation([ + this.get("left").getTypeAnnotation(), + this.get("right").getTypeAnnotation() + ]); +} + +export function ConditionalExpression() { + return t.createUnionTypeAnnotation([ + this.get("consequent").getTypeAnnotation(), + this.get("alternate").getTypeAnnotation() + ]); +} + +export function SequenceExpression(node) { + return this.get("expressions").pop().getTypeAnnotation(); +} + +export function AssignmentExpression(node) { + return this.get("right").getTypeAnnotation(); +} + +export function UpdateExpression(node) { + let operator = node.operator; + if (operator === "++" || operator === "--") { + return t.numberTypeAnnotation(); + } +} + +export function Literal(node) { + var value = node.value; + if (typeof value === "string") return t.stringTypeAnnotation(); + if (typeof value === "number") return t.numberTypeAnnotation(); + if (typeof value === "boolean") return t.booleanTypeAnnotation(); + if (value === null) return t.voidTypeAnnotation(); + if (node.regex) return t.genericTypeAnnotation(t.identifier("RegExp")); +} + +export function ObjectExpression() { + return t.genericTypeAnnotation(t.identifier("Object")); +} + +export function ArrayExpression () { + return t.genericTypeAnnotation(t.identifier("Array")); +} + +export function RestElement() { + return ArrayExpression(); +} + +RestElement.validParent = true; + +export function Func() { + return t.genericTypeAnnotation(t.identifier("Function")); +} + +export { Func as Function, Func as Class }; + +export function CallExpression() { + return resolveCall(this.get("callee")); +} + +export function TaggedTemplateExpression() { + return resolveCall(this.get("tag")); +} + +function resolveCall(callee) { + callee = callee.resolve(); + + if (callee.isFunction()) { + if (callee.is("async")) { + if (callee.is("generator")) { + return t.genericTypeAnnotation(t.identifier("AsyncIterator")); + } else { + return t.genericTypeAnnotation(t.identifier("Promise")); + } + } else { + if (callee.node.returnType) { + return callee.node.returnType; + } else { + // todo: get union type of all return arguments + } + } + } +} diff --git a/src/babel/traversal/path/introspection.js b/src/babel/traversal/path/introspection.js index 719ffaea49..102e9badea 100644 --- a/src/babel/traversal/path/introspection.js +++ b/src/babel/traversal/path/introspection.js @@ -1,3 +1,4 @@ +import type NodePath from "./index"; import includes from "lodash/collection/includes"; import * as t from "../../types"; @@ -232,21 +233,105 @@ export function willIMaybeExecutesBefore(target) { return this._guessExecutionStatusRelativeTo(target) !== "after"; } +/** + * Given a `target` check the execution status of it relative to the current path. + * + * "Execution status" simply refers to where or not we **think** this will execuete + * before or after the input `target` element. + */ + export function _guessExecutionStatusRelativeTo(target) { var self = this.getStatementParent(); target = target.getStatementParent(); + if (target === self) return "before"; + // check if the two paths are in different functions, we can't track execution of these var targetFuncParent = target.scope.getFunctionParent(); var selfFuncParent = self.scope.getFunctionParent(); if (targetFuncParent !== selfFuncParent) { return "function"; } + // crawl up the targets parents until we hit the container we're in and check keys do { + // todo: if (target === self) return "after"; + if (target.container === self.container) { - return target.key > self.key ? "before" : "after"; + return target.key >= self.key ? "before" : "after"; } } while(self = self.parentPath); return "before"; } + +/** + * Resolve a "pointer" `NodePath` to it's absolute path. + */ + +export function resolve(dangerous, resolved) { + return this._resolve(dangerous, resolved) || this; +} + +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; + + // we store all the paths we've "resolved" in this array to prevent infinite recursion + resolved = resolved || []; + resolved.push(this); + + if (this.isVariableDeclarator()) { + if (this.get("id").isIdentifier()) { + return this.get("init").resolve(dangerous, resolved); + } else { + // otherwise it's a request for a pattern and that's a bit more tricky + } + } else if (this.isReferencedIdentifier()) { + var binding = this.scope.getBinding(this.node.name); + if (!binding) return; + + // reassigned so we can't really resolve it + if (!binding.constant) return; + + // todo - lookup module in dependency graph + if (binding.kind === "module") return; + + 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 + + var targetKey = this.toComputedKey(); + if (!t.isLiteral(targetKey)) return; + + var targetName = targetKey.value; + + var target = this.get("object").resolve(dangerous, resolved); + + if (target.isObjectExpression()) { + var props = target.get("properties"); + for (var prop of (props: Array)) { + if (!prop.isProperty()) continue; + + var key = prop.get("key"); + + // { foo: obj } + var match = prop.isnt("computed") && key.isIdentifier({ name: targetName }); + + // { "foo": "obj" } or { ["foo"]: "obj" } + match = match || key.isLiteral({ value: targetName }); + + 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(dangerous, resolved); + } + } +} diff --git a/src/babel/traversal/path/resolution.js b/src/babel/traversal/path/resolution.js deleted file mode 100644 index 00b1efce23..0000000000 --- a/src/babel/traversal/path/resolution.js +++ /dev/null @@ -1,483 +0,0 @@ -import type NodePath from "./index"; -import * as t from "../../types"; - -/** - * Resolve a "pointer" `NodePath` to it's absolute path. - */ - -export function resolve(dangerous, resolved) { - return this._resolve(dangerous, resolved) || this; -} - -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; - - // we store all the paths we've "resolved" in this array to prevent infinite recursion - resolved = resolved || []; - resolved.push(this); - - if (this.isVariableDeclarator()) { - if (this.get("id").isIdentifier()) { - return this.get("init").resolve(dangerous, resolved); - } else { - // otherwise it's a request for a pattern and that's a bit more tricky - } - } else if (this.isReferencedIdentifier()) { - var binding = this.scope.getBinding(this.node.name); - if (!binding) return; - - // reassigned so we can't really resolve it - if (!binding.constant) return; - - // todo - lookup module in dependency graph - if (binding.kind === "module") return; - - 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 - - var targetKey = this.toComputedKey(); - if (!t.isLiteral(targetKey)) return; - - var targetName = targetKey.value; - - var target = this.get("object").resolve(dangerous, resolved); - - if (target.isObjectExpression()) { - var props = target.get("properties"); - for (var prop of (props: Array)) { - if (!prop.isProperty()) continue; - - var key = prop.get("key"); - - // { foo: obj } - var match = prop.isnt("computed") && key.isIdentifier({ name: targetName }); - - // { "foo": "obj" } or { ["foo"]: "obj" } - match = match || key.isLiteral({ value: targetName }); - - 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(dangerous, resolved); - } - } -} - - -/** - * Infer the type of the current `NodePath`. - */ - -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 _getTypeAnnotationBindingConstantViolations(path, name) { - var binding = this.scope.getBinding(name); - - var types = []; - this.typeAnnotation = t.unionTypeAnnotation(types); - - var functionConstantViolations = []; - var constantViolations = getConstantViolationsBefore(binding, path, functionConstantViolations); - - 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); - - // clear current types and add in observed test type - types.push(testType.typeAnnotation); - } - - if (constantViolations.length) { - // pick one constant from each scope which will represent the last possible - // control flow path that it could've taken/been - var rawConstantViolations = constantViolations.reverse(); - var visitedScopes = []; - constantViolations = []; - for (let violation of (rawConstantViolations: Array)) { - if (visitedScopes.indexOf(violation.scope) >= 0) continue; - visitedScopes.push(violation.scope); - constantViolations.push(violation); - } - - // add back on function constant violations since we can't track calls - constantViolations = constantViolations.concat(functionConstantViolations); - - // push on inferred types of violated paths - for (let violation of (constantViolations: Array)) { - types.push(violation.getTypeAnnotation()); - } - } - - if (types.length) { - return t.createUnionTypeAnnotation(types); - } -} - -function getConstantViolationsBefore(binding, path, functions) { - var violations = binding.constantViolations.slice(); - violations.unshift(binding.path); - return violations.filter((violation) => { - violation = violation.resolve(); - var status = violation._guessExecutionStatusRelativeTo(path); - if (functions && status === "function") functions.push(violation); - return status === "before"; - }); -} - -function checkBinary(name, path) { - 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(); - } - - // - var typeofPath; - var typePath; - if (left.isUnaryExpression({ operator: "typeof" })) { - typeofPath = left; - typePath = right; - } else if (right.isUnaryExpression({ operator: "typeof" })) { - typeofPath = right; - typePath = left; - } - if (!typePath && !typeofPath) return; - - // ensure that the type path is a Literal - typePath = typePath.resolve(); - if (!typePath.isLiteral()) return; - - // and that it's a string so we can infer it - var typeValue = typePath.node.value; - if (typeof typeValue !== "string") return; - - // and that the argument of the typeof path references us! - if (!typeofPath.get("argument").isIdentifier({ name })) return; - - // turn type value into a type annotation - return t.createTypeAnnotationBasedOnTypeof(typePath.node.value); -} - -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 { - let path = paths.shift().resolve(); - - if (path.isLogicalExpression()) { - paths.push(path.get("left")); - paths.push(path.get("right")); - } - - if (path.isBinaryExpression({ operator: "===" })) { - // todo: add in cases where operators imply a number - var type = checkBinary(name, path); - if (type) types.push(type); - } - } while(paths.length); - - if (types.length) { - return { - typeAnnotation: t.createUnionTypeAnnotation(types), - ifStatement - }; - } else { - return getTypeAnnotationBasedOnConditional(ifStatement, name); - } -} - -/** - * todo: split up this method - */ - -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 (this.isVariableDeclarator()) { - var id = this.get("id"); - - if (id.isIdentifier()) { - return this.get("init").getTypeAnnotation(); - } else { - return; - } - } - - // - if (this.parentPath.isTypeCastExpression()) { - return this.parentPath.getTypeAnnotation(); - } - - if (this.isTypeCastExpression()) { - return node.typeAnnotation; - } - - // - if (this.isRestElement() || this.parentPath.isRestElement() || this.isArrayExpression()) { - return t.genericTypeAnnotation(t.identifier("Array")); - } - - // - if (!force && this.parentPath.isReturnStatement()) { - return this.parentPath.getTypeAnnotation(); - } - - if (this.isReturnStatement()) { - var funcPath = this.findParent((path) => path.isFunction()); - if (!funcPath) return; - - var returnType = funcPath.node.returnType; - if (returnType) { - return returnType; - } else { - return this.get("argument").getTypeAnnotation(true); - } - } - - // - if (this.isNewExpression() && this.get("callee").isIdentifier()) { - // only resolve identifier callee - return t.genericTypeAnnotation(node.callee); - } - - // - 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(this, node.name); - } - } - - // built-in values - if (node.name === "undefined") { - return t.voidTypeAnnotation(); - } else if (node.name === "NaN" || node.name === "Infinity") { - return t.numberTypeAnnotation(); - } else if (node.name === "arguments") { - // todo - } - } - - // - if (this.isObjectExpression()) { - return t.genericTypeAnnotation(t.identifier("Object")); - } - - // - if (this.isFunction() || this.isClass()) { - return t.genericTypeAnnotation(t.identifier("Function")); - } - - // - if (this.isTemplateLiteral()) { - return t.stringTypeAnnotation(); - } - - // - if (this.isUnaryExpression()) { - let operator = node.operator; - - if (operator === "void") { - return t.voidTypeAnnotation(); - } else if (t.NUMBER_UNARY_OPERATORS.indexOf(operator) >= 0) { - return t.numberTypeAnnotation(); - } else if (t.STRING_UNARY_OPERATORS.indexOf(operator) >= 0) { - return t.stringTypeAnnotation(); - } else if (t.BOOLEAN_UNARY_OPERATORS.indexOf(operator) >= 0) { - return t.booleanTypeAnnotation(); - } - } - - // - if (this.isBinaryExpression()) { - let operator = node.operator; - - if (t.NUMBER_BINARY_OPERATORS.indexOf(operator) >= 0) { - return t.numberTypeAnnotation(); - } else if (t.BOOLEAN_BINARY_OPERATORS.indexOf(operator) >= 0) { - return t.booleanTypeAnnotation(); - } else if (operator === "+") { - var right = this.get("right"); - var left = this.get("left"); - - if (left.isGenericType("Number") && right.isGenericType("Number")) { - // both numbers so this will be a number - return t.numberTypeAnnotation(); - } else if (left.isGenericType("String") || right.isGenericType("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 - return t.unionTypeAnnotation([ - t.stringTypeAnnotation(), - t.numberTypeAnnotation() - ]); - } - } - - // - if (this.isLogicalExpression()) { - return t.createUnionTypeAnnotation([ - this.get("left").getTypeAnnotation(), - this.get("right").getTypeAnnotation() - ]); - } - - // - if (this.isConditionalExpression()) { - return t.createUnionTypeAnnotation([ - this.get("consequent").getTypeAnnotation(), - this.get("alternate").getTypeAnnotation() - ]); - } - - // - if (this.isSequenceExpression()) { - return this.get("expressions").pop().getTypeAnnotation(force); - } - - // - if (this.isAssignmentExpression()) { - return this.get("right").getTypeAnnotation(force); - } - - // - if (this.isUpdateExpression()) { - let operator = node.operator; - if (operator === "++" || operator === "--") { - return t.numberTypeAnnotation(); - } - } - - // - if (this.isLiteral()) { - var value = node.value; - if (typeof value === "string") return t.stringTypeAnnotation(); - if (typeof value === "number") return t.numberTypeAnnotation(); - if (typeof value === "boolean") return t.booleanTypeAnnotation(); - if (value === null) return t.voidTypeAnnotation(); - if (node.regex) return t.genericTypeAnnotation(t.identifier("RegExp")); - } - - // - var callPath; - 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")) { - return t.genericTypeAnnotation(t.identifier("AsyncIterator")); - } else { - return t.genericTypeAnnotation(t.identifier("Promise")); - } - } else { - if (callee.node.returnType) { - return callee.node.returnType; - } else { - // todo: get union type of all return arguments - } - } - } - } -} - -/** - * Description - */ - -export function isGenericType(genericName: string): boolean { - var type = this.getTypeAnnotation(); - - if (t.isGenericTypeAnnotation(type) && t.isIdentifier(type.id, { name: genericName })) { - return true; - } - - if (genericName === "String") { - return t.isStringTypeAnnotation(type); - } else if (genericName === "Number") { - return t.isNumberTypeAnnotation(type); - } else if (genericName === "Boolean") { - return t.isBooleanTypeAnnotation(type); - } - - return false; -} diff --git a/src/babel/types/flow.js b/src/babel/types/flow.js index 1d37a971d7..3793ef39de 100644 --- a/src/babel/types/flow.js +++ b/src/babel/types/flow.js @@ -109,8 +109,8 @@ export function createTypeAnnotationBasedOnTypeof(type) { } else if (type === "boolean") { return t.booleanTypeAnnotation(); } else if (type === "function") { - // todo + return t.genericTypeAnnotation(t.identifier("Function")); } else if (type === "object") { - // todo + return t.genericTypeAnnotation(t.identifier("Object")); } } diff --git a/src/babel/types/index.js b/src/babel/types/index.js index 001c9120d5..69eae978af 100644 --- a/src/babel/types/index.js +++ b/src/babel/types/index.js @@ -29,8 +29,9 @@ export const FLATTENABLE_KEYS = ["body", "expressions"]; export const FOR_INIT_KEYS = ["left", "init"]; export const COMMENT_KEYS = ["leadingComments", "trailingComments"]; -export const BOOLEAN_BINARY_OPERATORS = ["==", "===", "!=", "!==", ">", "<", ">=", "<=", "in", "instanceof"]; -export const NUMBER_BINARY_OPERATORS = ["-", "/", "*", "**", "&", "|", ">>", ">>>", "<<", "^"]; +export const BOOLEAN_NUMBER_BINARY_OPERATORS = [">", "<", ">=", "<="]; +export const BOOLEAN_BINARY_OPERATORS = ["==", "===", "!=", "!==", "in", "instanceof"].concat(BOOLEAN_NUMBER_BINARY_OPERATORS); +export const NUMBER_BINARY_OPERATORS = ["-", "/", "*", "**", "&", "|", ">>", ">>>", "<<", "^"]; export const BOOLEAN_UNARY_OPERATORS = ["delete", "!"]; export const NUMBER_UNARY_OPERATORS = ["+", "-", "++", "--", "~"];