From 02c1da194b850852c0706b70f0a4bcc4fa134b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=C3=A1ng=20J=C3=B9nli=C3=A0ng?= Date: Fri, 25 Mar 2022 13:06:34 -0400 Subject: [PATCH] Support some TSTypes in the inferrer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add ts inferer tests * fix: support some TSTypes in inferrer * Update packages/babel-plugin-transform-for-of/src/index.ts * fix typing errors * fix(inferer): allow {} prototype name in generics Co-authored-by: Nicolò Ribaudo --- .../src/index.ts | 6 +- .../test/fixtures/opt/options.json | 3 +- .../type-annotation-array-flow-deopt/input.js | 4 + .../options.json | 3 + .../output.js | 15 + .../opt/type-annotation-array-flow/input.js | 1 + .../type-annotation-array-flow/options.json | 3 + .../opt/type-annotation-array-flow/output.js | 3 + .../type-annotation-array-ts-deopt/input.ts | 4 + .../options.json | 4 + .../output.js | 5 +- .../opt/type-annotation-array-ts/input.ts | 1 + .../opt/type-annotation-array-ts/options.json | 4 + .../opt/type-annotation-array-ts/output.js | 3 + .../test/fixtures/opt/typeannotation/input.js | 4 - .../spread/type-reference-flow/input.ts | 14 + .../spread/type-reference-flow/options.json | 4 + .../spread/type-reference-flow/output.js | 13 + .../spread/type-reference-ts/input.ts | 14 + .../spread/type-reference-ts}/options.json | 0 .../spread/type-reference-ts/output.js | 13 + .../src/path/inference/index.ts | 33 +- .../src/path/inference/inferer-reference.ts | 38 +- .../src/path/inference/inferers.ts | 75 +-- .../babel-traverse/src/path/inference/util.ts | 33 ++ .../test/fixtures/type-reference/input.ts | 13 - packages/babel-traverse/test/inference.js | 478 ++++++++++++++++-- .../builders/typescript/createTSUnionType.ts | 7 +- .../flow/removeTypeDuplicates.ts | 23 +- .../typescript/removeTypeDuplicates.ts | 47 +- 30 files changed, 690 insertions(+), 178 deletions(-) create mode 100644 packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow-deopt/input.js create mode 100644 packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow-deopt/options.json create mode 100644 packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow-deopt/output.js create mode 100644 packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow/input.js create mode 100644 packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow/options.json create mode 100644 packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow/output.js create mode 100644 packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts-deopt/input.ts create mode 100644 packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts-deopt/options.json rename packages/babel-plugin-transform-for-of/test/fixtures/opt/{typeannotation => type-annotation-array-ts-deopt}/output.js (69%) create mode 100644 packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts/input.ts create mode 100644 packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts/options.json create mode 100644 packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts/output.js delete mode 100644 packages/babel-plugin-transform-for-of/test/fixtures/opt/typeannotation/input.js create mode 100644 packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-flow/input.ts create mode 100644 packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-flow/options.json create mode 100644 packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-flow/output.js create mode 100644 packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-ts/input.ts rename packages/{babel-traverse/test/fixtures/type-reference => babel-plugin-transform-spread/test/fixtures/spread/type-reference-ts}/options.json (100%) create mode 100644 packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-ts/output.js create mode 100644 packages/babel-traverse/src/path/inference/util.ts delete mode 100644 packages/babel-traverse/test/fixtures/type-reference/input.ts diff --git a/packages/babel-plugin-transform-for-of/src/index.ts b/packages/babel-plugin-transform-for-of/src/index.ts index edce0e1baca9..c29d60f39324 100644 --- a/packages/babel-plugin-transform-for-of/src/index.ts +++ b/packages/babel-plugin-transform-for-of/src/index.ts @@ -208,8 +208,10 @@ export default declare((api, options: Options) => { const right = path.get("right"); if ( right.isArrayExpression() || - right.isGenericType("Array") || - t.isArrayTypeAnnotation(right.getTypeAnnotation()) + (process.env.BABEL_8_BREAKING + ? right.isGenericType("Array") + : right.isGenericType("Array") || + t.isArrayTypeAnnotation(right.getTypeAnnotation())) ) { path.replaceWith(_ForOfStatementArray(path)); return; diff --git a/packages/babel-plugin-transform-for-of/test/fixtures/opt/options.json b/packages/babel-plugin-transform-for-of/test/fixtures/opt/options.json index 5ac45b8e39fe..9136101a3922 100644 --- a/packages/babel-plugin-transform-for-of/test/fixtures/opt/options.json +++ b/packages/babel-plugin-transform-for-of/test/fixtures/opt/options.json @@ -1,3 +1,4 @@ { - "plugins": ["transform-for-of", "transform-flow-strip-types"] + "plugins": ["transform-for-of"], + "presets": ["typescript"] } diff --git a/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow-deopt/input.js b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow-deopt/input.js new file mode 100644 index 000000000000..8c475251a3cc --- /dev/null +++ b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow-deopt/input.js @@ -0,0 +1,4 @@ +// This won't be optimize because when `for-of` is handled, the b's type annotation has been removed by the Flow plugin +function a(b: Array) { + for (const y of b) {} +} diff --git a/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow-deopt/options.json b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow-deopt/options.json new file mode 100644 index 000000000000..5ac45b8e39fe --- /dev/null +++ b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow-deopt/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["transform-for-of", "transform-flow-strip-types"] +} diff --git a/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow-deopt/output.js b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow-deopt/output.js new file mode 100644 index 000000000000..4df17880bca5 --- /dev/null +++ b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow-deopt/output.js @@ -0,0 +1,15 @@ +// This won't be optimize because when `for-of` is handled, the b's type annotation has been removed by the Flow plugin +function a(b) { + var _iterator = babelHelpers.createForOfIteratorHelper(b), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + const y = _step.value; + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } +} diff --git a/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow/input.js b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow/input.js new file mode 100644 index 000000000000..def3d332d28b --- /dev/null +++ b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow/input.js @@ -0,0 +1 @@ +for (const y of (b: Array)) {} diff --git a/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow/options.json b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow/options.json new file mode 100644 index 000000000000..5ac45b8e39fe --- /dev/null +++ b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["transform-for-of", "transform-flow-strip-types"] +} diff --git a/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow/output.js b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow/output.js new file mode 100644 index 000000000000..b7e4a3d80c72 --- /dev/null +++ b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-flow/output.js @@ -0,0 +1,3 @@ +for (var _i = 0, _arr = b; _i < _arr.length; _i++) { + const y = _arr[_i]; +} diff --git a/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts-deopt/input.ts b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts-deopt/input.ts new file mode 100644 index 000000000000..ea25bcd75bba --- /dev/null +++ b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts-deopt/input.ts @@ -0,0 +1,4 @@ +// This won't be optimize because when `for-of` is handled, the b's type annotation has been removed by the TS plugin +function a(b: Array) { + for (const y of b) {} +} diff --git a/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts-deopt/options.json b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts-deopt/options.json new file mode 100644 index 000000000000..9136101a3922 --- /dev/null +++ b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts-deopt/options.json @@ -0,0 +1,4 @@ +{ + "plugins": ["transform-for-of"], + "presets": ["typescript"] +} diff --git a/packages/babel-plugin-transform-for-of/test/fixtures/opt/typeannotation/output.js b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts-deopt/output.js similarity index 69% rename from packages/babel-plugin-transform-for-of/test/fixtures/opt/typeannotation/output.js rename to packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts-deopt/output.js index 7b047a5d5517..c6d45adb57b8 100644 --- a/packages/babel-plugin-transform-for-of/test/fixtures/opt/typeannotation/output.js +++ b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts-deopt/output.js @@ -1,7 +1,4 @@ -for (var _i = 0, _arr = b; _i < _arr.length; _i++) { - const y = _arr[_i]; -} - +// This won't be optimize because when `for-of` is handled, the b's type annotation has been removed by the TS plugin function a(b) { var _iterator = babelHelpers.createForOfIteratorHelper(b), _step; diff --git a/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts/input.ts b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts/input.ts new file mode 100644 index 000000000000..af0de5675aea --- /dev/null +++ b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts/input.ts @@ -0,0 +1 @@ +for (const y of (b as Array)) {} diff --git a/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts/options.json b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts/options.json new file mode 100644 index 000000000000..9136101a3922 --- /dev/null +++ b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts/options.json @@ -0,0 +1,4 @@ +{ + "plugins": ["transform-for-of"], + "presets": ["typescript"] +} diff --git a/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts/output.js b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts/output.js new file mode 100644 index 000000000000..b7e4a3d80c72 --- /dev/null +++ b/packages/babel-plugin-transform-for-of/test/fixtures/opt/type-annotation-array-ts/output.js @@ -0,0 +1,3 @@ +for (var _i = 0, _arr = b; _i < _arr.length; _i++) { + const y = _arr[_i]; +} diff --git a/packages/babel-plugin-transform-for-of/test/fixtures/opt/typeannotation/input.js b/packages/babel-plugin-transform-for-of/test/fixtures/opt/typeannotation/input.js deleted file mode 100644 index 0625a93fa34c..000000000000 --- a/packages/babel-plugin-transform-for-of/test/fixtures/opt/typeannotation/input.js +++ /dev/null @@ -1,4 +0,0 @@ -for (const y of (b: Array)) {} -function a(b: Array) { - for (const y of b) {} -} diff --git a/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-flow/input.ts b/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-flow/input.ts new file mode 100644 index 000000000000..affbf9b07e3c --- /dev/null +++ b/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-flow/input.ts @@ -0,0 +1,14 @@ +function foo() { + const x = 1 ? a() : b(); + const y = a() || b(); + + return [...x, ...y]; +} + +function a(): Array { + return []; +} + +function b(): Array { + return []; +} diff --git a/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-flow/options.json b/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-flow/options.json new file mode 100644 index 000000000000..6a0bf348d1b4 --- /dev/null +++ b/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-flow/options.json @@ -0,0 +1,4 @@ +{ + "plugins": ["transform-spread"], + "presets": ["flow"] +} diff --git a/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-flow/output.js b/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-flow/output.js new file mode 100644 index 000000000000..4befc47afae9 --- /dev/null +++ b/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-flow/output.js @@ -0,0 +1,13 @@ +function foo() { + const x = 1 ? a() : b(); + const y = a() || b(); + return [].concat(x, y); +} + +function a() { + return []; +} + +function b() { + return []; +} diff --git a/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-ts/input.ts b/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-ts/input.ts new file mode 100644 index 000000000000..affbf9b07e3c --- /dev/null +++ b/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-ts/input.ts @@ -0,0 +1,14 @@ +function foo() { + const x = 1 ? a() : b(); + const y = a() || b(); + + return [...x, ...y]; +} + +function a(): Array { + return []; +} + +function b(): Array { + return []; +} diff --git a/packages/babel-traverse/test/fixtures/type-reference/options.json b/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-ts/options.json similarity index 100% rename from packages/babel-traverse/test/fixtures/type-reference/options.json rename to packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-ts/options.json diff --git a/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-ts/output.js b/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-ts/output.js new file mode 100644 index 000000000000..4befc47afae9 --- /dev/null +++ b/packages/babel-plugin-transform-spread/test/fixtures/spread/type-reference-ts/output.js @@ -0,0 +1,13 @@ +function foo() { + const x = 1 ? a() : b(); + const y = a() || b(); + return [].concat(x, y); +} + +function a() { + return []; +} + +function b() { + return []; +} diff --git a/packages/babel-traverse/src/path/inference/index.ts b/packages/babel-traverse/src/path/inference/index.ts index ccc073292ccb..04f04fb7dc37 100644 --- a/packages/babel-traverse/src/path/inference/index.ts +++ b/packages/babel-traverse/src/path/inference/index.ts @@ -3,6 +3,7 @@ import * as inferers from "./inferers"; import { anyTypeAnnotation, isAnyTypeAnnotation, + isArrayTypeAnnotation, isBooleanTypeAnnotation, isEmptyTypeAnnotation, isFlowBaseAnnotation, @@ -11,6 +12,10 @@ import { isMixedTypeAnnotation, isNumberTypeAnnotation, isStringTypeAnnotation, + isTSArrayType, + isTSTypeAnnotation, + isTSTypeReference, + isTupleTypeAnnotation, isTypeAnnotation, isUnionTypeAnnotation, isVoidTypeAnnotation, @@ -23,13 +28,15 @@ import type * as t from "@babel/types"; * Infer the type of the current `NodePath`. */ -export function getTypeAnnotation(this: NodePath): t.FlowType { +export function getTypeAnnotation(this: NodePath): t.FlowType | t.TSType { let type = this.getData("typeAnnotation"); if (type != null) { return type; } type = this._getTypeAnnotation() || anyTypeAnnotation(); - if (isTypeAnnotation(type)) type = type.typeAnnotation; + if (isTypeAnnotation(type) || isTSTypeAnnotation(type)) { + type = type.typeAnnotation; + } this.setData("typeAnnotation", type); return type; } @@ -108,7 +115,7 @@ export function isBaseType( function _isBaseType( baseName: string, - type?: t.FlowType, + type?: t.FlowType | t.TSType, soft?: boolean, ): boolean { if (baseName === "string") { @@ -165,8 +172,24 @@ export function baseTypeStrictlyMatches( export function isGenericType(this: NodePath, genericName: string): boolean { const type = this.getTypeAnnotation(); + if (genericName === "Array") { + // T[] + if ( + isTSArrayType(type) || + isArrayTypeAnnotation(type) || + isTupleTypeAnnotation(type) + ) { + return true; + } + } return ( - isGenericTypeAnnotation(type) && - isIdentifier(type.id, { name: genericName }) + (isGenericTypeAnnotation(type) && + isIdentifier(type.id, { + name: genericName, + })) || + (isTSTypeReference(type) && + isIdentifier(type.typeName, { + name: genericName, + })) ); } diff --git a/packages/babel-traverse/src/path/inference/inferer-reference.ts b/packages/babel-traverse/src/path/inference/inferer-reference.ts index baf9ecf17000..1e71b03154ae 100644 --- a/packages/babel-traverse/src/path/inference/inferer-reference.ts +++ b/packages/babel-traverse/src/path/inference/inferer-reference.ts @@ -1,17 +1,15 @@ import type NodePath from "../index"; import { BOOLEAN_NUMBER_BINARY_OPERATORS, - createFlowUnionType, - createTSUnionType, createTypeAnnotationBasedOnTypeof, - createUnionTypeAnnotation, - isTSTypeAnnotation, numberTypeAnnotation, voidTypeAnnotation, } from "@babel/types"; import type * as t from "@babel/types"; import type Binding from "../../scope/binding"; +import { createUnionType } from "./util"; + export default function (this: NodePath, node: t.Identifier) { if (!this.isReferenced()) return; @@ -110,18 +108,7 @@ function getTypeAnnotationBindingConstantViolations( return; } - if (isTSTypeAnnotation(types[0]) && createTSUnionType) { - return createTSUnionType( - // @ts-ignore fixme: createTSUnionType should handle voidTypeAnnotation - types as t.TSTypeAnnotation[], - ); - } - - if (createFlowUnionType) { - return createFlowUnionType(types as t.FlowType[]); - } - - return createUnionTypeAnnotation(types as t.FlowType[]); + return createUnionType(types); } function getConstantViolationsBefore( @@ -249,25 +236,8 @@ function getConditionalAnnotation( } if (types.length) { - if (isTSTypeAnnotation(types[0]) && createTSUnionType) { - return { - typeAnnotation: createTSUnionType( - // @ts-ignore fixme: createTSUnionType should handle voidTypeAnnotation - types as t.TSTypeAnnotation[], - ), - ifStatement, - }; - } - - if (createFlowUnionType) { - return { - typeAnnotation: createFlowUnionType(types), - ifStatement, - }; - } - return { - typeAnnotation: createUnionTypeAnnotation(types), + typeAnnotation: createUnionType(types), ifStatement, }; } diff --git a/packages/babel-traverse/src/path/inference/inferers.ts b/packages/babel-traverse/src/path/inference/inferers.ts index c2d0813029be..165c3b36afa2 100644 --- a/packages/babel-traverse/src/path/inference/inferers.ts +++ b/packages/babel-traverse/src/path/inference/inferers.ts @@ -8,45 +8,26 @@ import { arrayTypeAnnotation, booleanTypeAnnotation, buildMatchMemberExpression, - createFlowUnionType, - createTSUnionType, - createUnionTypeAnnotation, genericTypeAnnotation, identifier, - isTSTypeAnnotation, nullLiteralTypeAnnotation, numberTypeAnnotation, stringTypeAnnotation, tupleTypeAnnotation, unionTypeAnnotation, voidTypeAnnotation, + isIdentifier, } from "@babel/types"; import type * as t from "@babel/types"; export { default as Identifier } from "./inferer-reference"; +import { createUnionType } from "./util"; import type NodePath from ".."; export function VariableDeclarator(this: NodePath) { - const id = this.get("id"); - - if (!id.isIdentifier()) return; - const init = this.get("init"); - - let type = init.getTypeAnnotation(); - - if (type?.type === "AnyTypeAnnotation") { - // Detect "var foo = Array()" calls so we can optimize for arrays vs iterables. - if ( - init.isCallExpression() && - init.get("callee").isIdentifier({ name: "Array" }) && - !init.scope.hasBinding("Array", true /* noGlobals */) - ) { - type = ArrayExpression(); - } - } - - return type; + if (!this.get("id").isIdentifier()) return; + return this.get("init").getTypeAnnotation(); } export function TypeCastExpression(node: t.TypeCastExpression) { @@ -55,6 +36,16 @@ export function TypeCastExpression(node: t.TypeCastExpression) { TypeCastExpression.validParent = true; +export function TSAsExpression(node: t.TSAsExpression) { + return node.typeAnnotation; +} + +TSAsExpression.validParent = true; + +export function TSNonNullExpression(this: NodePath) { + return this.get("expression").getTypeAnnotation(); +} + export function NewExpression( this: NodePath, node: t.NewExpression, @@ -119,16 +110,7 @@ export function LogicalExpression(this: NodePath) { this.get("right").getTypeAnnotation(), ]; - if (isTSTypeAnnotation(argumentTypes[0]) && createTSUnionType) { - // @ts-expect-error Fixme: getTypeAnnotation also returns TS types - return createTSUnionType(argumentTypes); - } - - if (createFlowUnionType) { - return createFlowUnionType(argumentTypes); - } - - return createUnionTypeAnnotation(argumentTypes); + return createUnionType(argumentTypes); } export function ConditionalExpression(this: NodePath) { @@ -137,16 +119,7 @@ export function ConditionalExpression(this: NodePath) { this.get("alternate").getTypeAnnotation(), ]; - if (isTSTypeAnnotation(argumentTypes[0]) && createTSUnionType) { - // @ts-expect-error Fixme: getTypeAnnotation also returns TS types - return createTSUnionType(argumentTypes); - } - - if (createFlowUnionType) { - return createFlowUnionType(argumentTypes); - } - - return createUnionTypeAnnotation(argumentTypes); + return createUnionType(argumentTypes); } export function SequenceExpression(this: NodePath) { @@ -227,7 +200,12 @@ export function CallExpression(this: NodePath) { const { callee } = this.node; if (isObjectKeys(callee)) { return arrayTypeAnnotation(stringTypeAnnotation()); - } else if (isArrayFrom(callee) || isObjectValues(callee)) { + } else if ( + isArrayFrom(callee) || + isObjectValues(callee) || + // Detect "var foo = Array()" calls so we can optimize for arrays vs iterables. + isIdentifier(callee, { name: "Array" }) + ) { return arrayTypeAnnotation(anyTypeAnnotation()); } else if (isObjectEntries(callee)) { return arrayTypeAnnotation( @@ -248,14 +226,17 @@ function resolveCall(callee: NodePath) { callee = callee.resolve(); if (callee.isFunction()) { - if (callee.is("async")) { - if (callee.is("generator")) { + const { node } = callee; + if (node.async) { + if (node.generator) { return genericTypeAnnotation(identifier("AsyncIterator")); } else { return genericTypeAnnotation(identifier("Promise")); } } else { - if (callee.node.returnType) { + if (node.generator) { + return genericTypeAnnotation(identifier("Iterator")); + } else if (callee.node.returnType) { return callee.node.returnType; } else { // todo: get union type of all return arguments diff --git a/packages/babel-traverse/src/path/inference/util.ts b/packages/babel-traverse/src/path/inference/util.ts new file mode 100644 index 000000000000..199a3f6df79b --- /dev/null +++ b/packages/babel-traverse/src/path/inference/util.ts @@ -0,0 +1,33 @@ +import { + createFlowUnionType, + createTSUnionType, + createUnionTypeAnnotation, + isFlowType, + isTSType, +} from "@babel/types"; +import type * as t from "@babel/types"; + +export function createUnionType( + types: Array, +): t.FlowType | t.TSType { + if (process.env.BABEL_8_BREAKING) { + if (isFlowType(types[0])) { + return createFlowUnionType(types as t.FlowType[]); + } + if (isTSType(types[0])) { + return createTSUnionType(types as t.TSType[]); + } + } else { + if (isFlowType(types[0])) { + if (createFlowUnionType) { + return createFlowUnionType(types as t.FlowType[]); + } + + return createUnionTypeAnnotation(types as t.FlowType[]); + } else { + if (createTSUnionType) { + return createTSUnionType(types as t.TSType[]); + } + } + } +} diff --git a/packages/babel-traverse/test/fixtures/type-reference/input.ts b/packages/babel-traverse/test/fixtures/type-reference/input.ts deleted file mode 100644 index 9fc2c011bd9c..000000000000 --- a/packages/babel-traverse/test/fixtures/type-reference/input.ts +++ /dev/null @@ -1,13 +0,0 @@ -function foo() { - const x = 1 ? a() : b(); - - return [...x]; -} - -function a(): number[] { - return []; -} - -function b(): number[] { - return []; -} diff --git a/packages/babel-traverse/test/inference.js b/packages/babel-traverse/test/inference.js index 71025d5ea699..50c72e1ef002 100644 --- a/packages/babel-traverse/test/inference.js +++ b/packages/babel-traverse/test/inference.js @@ -4,8 +4,8 @@ import * as t from "@babel/types"; import _traverse from "../lib/index.js"; const traverse = _traverse.default || _traverse; -function getPath(code) { - const ast = parse(code, { plugins: ["flow"] }); +function getPath(code, options) { + const ast = parse(code, options); let path; traverse(ast, { Program: function (_path) { @@ -16,10 +16,18 @@ function getPath(code) { return path; } -describe("inference", function () { +function flowGetPath(code) { + return getPath(code, { plugins: ["flow"] }); +} + +function tsGetPath(code) { + return getPath(code, { plugins: ["typescript"] }); +} + +describe("inference with Flow", function () { describe("baseTypeStrictlyMatches", function () { it("should work with null", function () { - const path = getPath("var x = null; x === null") + const path = flowGetPath("var x = null; x === null") .get("body")[1] .get("expression"); const left = path.get("left"); @@ -30,7 +38,7 @@ describe("inference", function () { }); it("should work with numbers", function () { - const path = getPath("var x = 1; x === 2") + const path = flowGetPath("var x = 1; x === 2") .get("body")[1] .get("expression"); const left = path.get("left"); @@ -41,7 +49,9 @@ describe("inference", function () { }); it("should bail when type changes", function () { - const path = getPath("var x = 1; if (foo) x = null;else x = 3; x === 2") + const path = flowGetPath( + "var x = 1; if (foo) x = null;else x = 3; x === 2", + ) .get("body")[2] .get("expression"); const left = path.get("left"); @@ -53,7 +63,7 @@ describe("inference", function () { }); it("should differentiate between null and undefined", function () { - const path = getPath("var x; x === null") + const path = flowGetPath("var x; x === null") .get("body")[1] .get("expression"); const left = path.get("left"); @@ -65,85 +75,85 @@ describe("inference", function () { }); describe("getTypeAnnotation", function () { it("should infer from type cast", function () { - const path = getPath("(x: number)").get("body")[0].get("expression"); + const path = flowGetPath("(x: number)").get("body")[0].get("expression"); expect(t.isNumberTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); }); it("should infer string from template literal", function () { - const path = getPath("`hey`").get("body")[0].get("expression"); + const path = flowGetPath("`hey`").get("body")[0].get("expression"); expect(t.isStringTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); }); it("should infer number from +x", function () { - const path = getPath("+x").get("body")[0].get("expression"); + const path = flowGetPath("+x").get("body")[0].get("expression"); expect(t.isNumberTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); }); it("should infer T from new T", function () { - const path = getPath("new T").get("body")[0].get("expression"); + const path = flowGetPath("new T").get("body")[0].get("expression"); const type = path.getTypeAnnotation(); expect( t.isGenericTypeAnnotation(type) && type.id.name === "T", ).toBeTruthy(); }); it("should infer number from ++x", function () { - const path = getPath("++x").get("body")[0].get("expression"); + const path = flowGetPath("++x").get("body")[0].get("expression"); expect(t.isNumberTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); }); it("should infer number from --x", function () { - const path = getPath("--x").get("body")[0].get("expression"); + const path = flowGetPath("--x").get("body")[0].get("expression"); expect(t.isNumberTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); }); it("should infer void from void x", function () { - const path = getPath("void x").get("body")[0].get("expression"); + const path = flowGetPath("void x").get("body")[0].get("expression"); expect(t.isVoidTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); }); it("should infer string from typeof x", function () { - const path = getPath("typeof x").get("body")[0].get("expression"); + const path = flowGetPath("typeof x").get("body")[0].get("expression"); expect(t.isStringTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); }); it("should infer boolean from !x", function () { - const path = getPath("!x").get("body")[0].get("expression"); + const path = flowGetPath("!x").get("body")[0].get("expression"); expect(t.isBooleanTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); }); it("should infer type of sequence expression", function () { - const path = getPath("a,1").get("body")[0].get("expression"); + const path = flowGetPath("a,1").get("body")[0].get("expression"); expect(t.isNumberTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); }); it("should infer type of logical expression", function () { - const path = getPath("'a' && 1").get("body")[0].get("expression"); + const path = flowGetPath("'a' && 1").get("body")[0].get("expression"); const type = path.getTypeAnnotation(); expect(t.isUnionTypeAnnotation(type)).toBeTruthy(); expect(t.isStringTypeAnnotation(type.types[0])).toBeTruthy(); expect(t.isNumberTypeAnnotation(type.types[1])).toBeTruthy(); }); it("should infer type of conditional expression", function () { - const path = getPath("q ? true : 0").get("body")[0].get("expression"); + const path = flowGetPath("q ? true : 0").get("body")[0].get("expression"); const type = path.getTypeAnnotation(); expect(t.isUnionTypeAnnotation(type)).toBeTruthy(); expect(t.isBooleanTypeAnnotation(type.types[0])).toBeTruthy(); expect(t.isNumberTypeAnnotation(type.types[1])).toBeTruthy(); }); it("should infer RegExp from RegExp literal", function () { - const path = getPath("/.+/").get("body")[0].get("expression"); + const path = flowGetPath("/.+/").get("body")[0].get("expression"); const type = path.getTypeAnnotation(); expect( t.isGenericTypeAnnotation(type) && type.id.name === "RegExp", ).toBeTruthy(); }); it("should infer Object from object expression", function () { - const path = getPath("({ a: 5 })").get("body")[0].get("expression"); + const path = flowGetPath("({ a: 5 })").get("body")[0].get("expression"); const type = path.getTypeAnnotation(); expect( t.isGenericTypeAnnotation(type) && type.id.name === "Object", ).toBeTruthy(); }); it("should infer Array from array expression", function () { - const path = getPath("[ 5 ]").get("body")[0].get("expression"); + const path = flowGetPath("[ 5 ]").get("body")[0].get("expression"); const type = path.getTypeAnnotation(); expect( t.isGenericTypeAnnotation(type) && type.id.name === "Array", ).toBeTruthy(); }); it("should infer Function from function", function () { - const path = getPath("(function (): string {})") + const path = flowGetPath("(function (): string {})") .get("body")[0] .get("expression"); const type = path.getTypeAnnotation(); @@ -152,14 +162,14 @@ describe("inference", function () { ).toBeTruthy(); }); it("should infer call return type using function", function () { - const path = getPath("(function (): string {})()") + const path = flowGetPath("(function (): string {})()") .get("body")[0] .get("expression"); const type = path.getTypeAnnotation(); expect(t.isStringTypeAnnotation(type)).toBeTruthy(); }); it("should infer call return type using async function", function () { - const path = getPath("(async function (): string {})()") + const path = flowGetPath("(async function (): string {})()") .get("body")[0] .get("expression"); const type = path.getTypeAnnotation(); @@ -168,7 +178,7 @@ describe("inference", function () { ).toBeTruthy(); }); it("should infer call return type using async generator function", function () { - const path = getPath("(async function * (): string {})()") + const path = flowGetPath("(async function * (): string {})()") .get("body")[0] .get("expression"); const type = path.getTypeAnnotation(); @@ -177,29 +187,31 @@ describe("inference", function () { ).toBeTruthy(); }); it("should infer number from x/y", function () { - const path = getPath("x/y").get("body")[0].get("expression"); + const path = flowGetPath("x/y").get("body")[0].get("expression"); const type = path.getTypeAnnotation(); expect(t.isNumberTypeAnnotation(type)).toBeTruthy(); }); it("should infer boolean from x instanceof y", function () { - const path = getPath("x instanceof y").get("body")[0].get("expression"); + const path = flowGetPath("x instanceof y") + .get("body")[0] + .get("expression"); const type = path.getTypeAnnotation(); expect(t.isBooleanTypeAnnotation(type)).toBeTruthy(); }); it("should infer number from 1 + 2", function () { - const path = getPath("1 + 2").get("body")[0].get("expression"); + const path = flowGetPath("1 + 2").get("body")[0].get("expression"); const type = path.getTypeAnnotation(); expect(t.isNumberTypeAnnotation(type)).toBeTruthy(); }); it("should infer string|number from x + y", function () { - const path = getPath("x + y").get("body")[0].get("expression"); + const path = flowGetPath("x + y").get("body")[0].get("expression"); const type = path.getTypeAnnotation(); expect(t.isUnionTypeAnnotation(type)).toBeTruthy(); expect(t.isStringTypeAnnotation(type.types[0])).toBeTruthy(); expect(t.isNumberTypeAnnotation(type.types[1])).toBeTruthy(); }); it("should infer type of tagged template literal", function () { - const path = getPath("(function (): RegExp {}) `hey`") + const path = flowGetPath("(function (): RegExp {}) `hey`") .get("body")[0] .get("expression"); const type = path.getTypeAnnotation(); @@ -208,19 +220,19 @@ describe("inference", function () { ).toBeTruthy(); }); it("should infer constant identifier", function () { - const path = getPath("const x = 0; x").get("body.1.expression"); + const path = flowGetPath("const x = 0; x").get("body.1.expression"); const type = path.getTypeAnnotation(); expect(t.isNumberTypeAnnotation(type)).toBeTruthy(); }); it("should infer indirect constant identifier", function () { - const path = getPath("const x = 0; const y = x; y").get( + const path = flowGetPath("const x = 0; const y = x; y").get( "body.2.expression", ); const type = path.getTypeAnnotation(); expect(t.isNumberTypeAnnotation(type)).toBeTruthy(); }); it("should infer identifier type from if statement (===)", function () { - const path = getPath( + const path = flowGetPath( `function test(x) { if (x === true) x; }`, @@ -229,14 +241,14 @@ describe("inference", function () { expect(t.isBooleanTypeAnnotation(type)).toBeTruthy(); }); it("should infer identifier type from if statement (typeof)", function () { - let path = getPath( + let path = flowGetPath( `function test(x) { if (typeof x == 'string') x; }`, ).get("body.0.body.body.0.consequent.expression"); let type = path.getTypeAnnotation(); expect(t.isStringTypeAnnotation(type)).toBeTruthy(); - path = getPath( + path = flowGetPath( `function test(x) { if (typeof x === 'number') x; }`, @@ -245,7 +257,7 @@ describe("inference", function () { expect(t.isNumberTypeAnnotation(type)).toBeTruthy(); }); it("should infer identifier type from if statement (&&)", function () { - let path = getPath( + let path = flowGetPath( `function test(x) { if (typeof x == 'string' && x === 3) x; }`, @@ -254,14 +266,14 @@ describe("inference", function () { expect(t.isUnionTypeAnnotation(type)).toBeTruthy(); expect(t.isStringTypeAnnotation(type.types[0])).toBeTruthy(); expect(t.isNumberTypeAnnotation(type.types[1])).toBeTruthy(); - path = getPath( + path = flowGetPath( `function test(x) { if (true && x === 3) x; }`, ).get("body.0.body.body.0.consequent.expression"); type = path.getTypeAnnotation(); expect(t.isNumberTypeAnnotation(type)).toBeTruthy(); - path = getPath( + path = flowGetPath( `function test(x) { if (x === 'test' && true) x; }`, @@ -270,7 +282,7 @@ describe("inference", function () { expect(t.isStringTypeAnnotation(type)).toBeTruthy(); }); it("should infer identifier type from if statement (||)", function () { - const path = getPath( + const path = flowGetPath( `function test(x) { if (typeof x == 'string' || x === 3) x; }`, @@ -279,7 +291,7 @@ describe("inference", function () { expect(t.isAnyTypeAnnotation(type)).toBeTruthy(); }); it("should not infer identifier type from incorrect binding", function () { - const path = getPath( + const path = flowGetPath( `function outer(x) { if (x === 3) { function inner(x) { @@ -292,7 +304,7 @@ describe("inference", function () { expect(t.isAnyTypeAnnotation(type)).toBeTruthy(); }); it("should not cause a stack overflow when two variable depend on eachother", function () { - const path = getPath(` + const path = flowGetPath(` var b, c; while (0) { c = 1; @@ -308,4 +320,386 @@ describe("inference", function () { expect(path.getTypeAnnotation()).toEqual({ type: "AnyTypeAnnotation" }); }); }); + describe(`isGenericType("Array")`, () => { + it.each([ + "var x = Array()", + "var x = Array.from([])", + "var x = Object.keys({})", + "var x = Object.values({})", + "var x = Object.entries({})", + "var x = new Array", + "const x = [];", + "const x = (function (): Array { return []; })()", + "const x = (function (): T[] { return []; })()", + "const x = (v: Array)", + "const x = ({}, [])", + "let x = (y: [number, string])", + "var x = true ? a() : b(); function a(): Array {}; function b(): Array {}", + "var x = a() || b(); function a(): Array {}; function b(): Array {}", + ])(`NodePath(%p).isGenericType("Array") should be true`, input => { + const path = flowGetPath(input).get("body.0.declarations.0"); + expect(path.isGenericType("Array")).toBe(true); + }); + it.each([ + "x = Array()", + "x = Array.from([])", + "x = Object.keys({})", + "x = Object.values({})", + "x = Object.entries({})", + "x = new Array", + "x = [];", + "x = (function (): Array { return []; })()", + "x = (function (): T[] { return []; })()", + "x = (v: Array)", + "x = ({}, [])", + "x = (y: [number, string])", + ])(`NodePath(%p).isGenericType("Array") should be true`, input => { + const path = flowGetPath(input).get("body.0.expression"); + expect(path.isGenericType("Array")).toBe(true); + }); + it.each(["const x = ({}, [])"])( + `With { createParenthesizedExpressions: true}, NodePath(%p).isGenericType("Array") should be true`, + input => { + const path = getPath(input, { + plugins: ["flow"], + createParenthesizedExpressions: true, + }).get("body.0.declarations.0"); + expect(path.isGenericType("Array")).toBe(true); + }, + ); + }); +}); + +describe("inference with TypeScript", function () { + describe("baseTypeStrictlyMatches", function () { + it("should work with null", function () { + const path = tsGetPath("var x = null; x === null") + .get("body")[1] + .get("expression"); + const left = path.get("left"); + const right = path.get("right"); + const strictMatch = left.baseTypeStrictlyMatches(right); + + expect(strictMatch).toBeTruthy(); + }); + + it("should work with numbers", function () { + const path = tsGetPath("var x = 1; x === 2") + .get("body")[1] + .get("expression"); + const left = path.get("left"); + const right = path.get("right"); + const strictMatch = left.baseTypeStrictlyMatches(right); + + expect(strictMatch).toBeTruthy(); + }); + + it("should bail when type changes", function () { + const path = tsGetPath("var x = 1; if (foo) x = null;else x = 3; x === 2") + .get("body")[2] + .get("expression"); + const left = path.get("left"); + const right = path.get("right"); + + const strictMatch = left.baseTypeStrictlyMatches(right); + + expect(strictMatch).toBeFalsy(); + }); + + it("should differentiate between null and undefined", function () { + const path = tsGetPath("var x; x === null") + .get("body")[1] + .get("expression"); + const left = path.get("left"); + const right = path.get("right"); + const strictMatch = left.baseTypeStrictlyMatches(right); + + expect(strictMatch).toBeFalsy(); + }); + }); + describe("getTypeAnnotation", function () { + it("should infer from type cast", function () { + const path = tsGetPath("x as number").get("body")[0].get("expression"); + expect(t.isTSNumberKeyword(path.getTypeAnnotation())).toBeTruthy(); + }); + it("should infer from non-null expression", function () { + const path = tsGetPath("x as number").get("body")[0].get("expression"); + expect(t.isTSNumberKeyword(path.getTypeAnnotation())).toBeTruthy(); + }); + it("should infer string from template literal", function () { + const path = tsGetPath("`hey`").get("body")[0].get("expression"); + expect(t.isStringTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); + }); + it("should infer number from +x", function () { + const path = tsGetPath("+x").get("body")[0].get("expression"); + expect(t.isNumberTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); + }); + it("should infer T from new T", function () { + const path = tsGetPath("new T").get("body")[0].get("expression"); + const type = path.getTypeAnnotation(); + expect( + t.isGenericTypeAnnotation(type) && type.id.name === "T", + ).toBeTruthy(); + }); + it("should infer number from ++x", function () { + const path = tsGetPath("++x").get("body")[0].get("expression"); + expect(t.isNumberTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); + }); + it("should infer number from --x", function () { + const path = tsGetPath("--x").get("body")[0].get("expression"); + expect(t.isNumberTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); + }); + it("should infer void from void x", function () { + const path = tsGetPath("void x").get("body")[0].get("expression"); + expect(t.isVoidTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); + }); + it("should infer string from typeof x", function () { + const path = tsGetPath("typeof x").get("body")[0].get("expression"); + expect(t.isStringTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); + }); + it("should infer boolean from !x", function () { + const path = tsGetPath("!x").get("body")[0].get("expression"); + expect(t.isBooleanTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); + }); + it("should infer type of sequence expression", function () { + const path = tsGetPath("a,1").get("body")[0].get("expression"); + expect(t.isNumberTypeAnnotation(path.getTypeAnnotation())).toBeTruthy(); + }); + it("should infer type of logical expression", function () { + const path = tsGetPath("'a' && 1").get("body")[0].get("expression"); + const type = path.getTypeAnnotation(); + expect(t.isUnionTypeAnnotation(type)).toBeTruthy(); + expect(t.isStringTypeAnnotation(type.types[0])).toBeTruthy(); + expect(t.isNumberTypeAnnotation(type.types[1])).toBeTruthy(); + }); + it("should infer type of conditional expression", function () { + const path = tsGetPath("q ? true : 0").get("body")[0].get("expression"); + const type = path.getTypeAnnotation(); + expect(t.isUnionTypeAnnotation(type)).toBeTruthy(); + expect(t.isBooleanTypeAnnotation(type.types[0])).toBeTruthy(); + expect(t.isNumberTypeAnnotation(type.types[1])).toBeTruthy(); + }); + it("should infer RegExp from RegExp literal", function () { + const path = tsGetPath("/.+/").get("body")[0].get("expression"); + const type = path.getTypeAnnotation(); + expect( + t.isGenericTypeAnnotation(type) && type.id.name === "RegExp", + ).toBeTruthy(); + }); + it("should infer Object from object expression", function () { + const path = tsGetPath("({ a: 5 })").get("body")[0].get("expression"); + const type = path.getTypeAnnotation(); + expect( + t.isGenericTypeAnnotation(type) && type.id.name === "Object", + ).toBeTruthy(); + }); + it("should infer Array from array expression", function () { + const path = tsGetPath("[ 5 ]").get("body")[0].get("expression"); + const type = path.getTypeAnnotation(); + expect( + t.isGenericTypeAnnotation(type) && type.id.name === "Array", + ).toBeTruthy(); + }); + it("should infer Function from function", function () { + const path = tsGetPath("(function (): string {})") + .get("body")[0] + .get("expression"); + const type = path.getTypeAnnotation(); + expect( + t.isGenericTypeAnnotation(type) && type.id.name === "Function", + ).toBeTruthy(); + }); + it("should infer call return type using function", function () { + const path = tsGetPath("(function (): string {})()") + .get("body")[0] + .get("expression"); + const type = path.getTypeAnnotation(); + expect(type.type).toBe("TSStringKeyword"); + }); + it("should infer call return type using async function", function () { + const path = tsGetPath("(async function (): string {})()") + .get("body")[0] + .get("expression"); + const type = path.getTypeAnnotation(); + expect( + t.isGenericTypeAnnotation(type) && type.id.name === "Promise", + ).toBeTruthy(); + }); + it("should infer call return type using async generator function", function () { + const path = tsGetPath("(async function * (): string {})()") + .get("body")[0] + .get("expression"); + const type = path.getTypeAnnotation(); + expect( + t.isGenericTypeAnnotation(type) && type.id.name === "AsyncIterator", + ).toBeTruthy(); + }); + it("should infer number from x/y", function () { + const path = tsGetPath("x/y").get("body")[0].get("expression"); + const type = path.getTypeAnnotation(); + expect(t.isNumberTypeAnnotation(type)).toBeTruthy(); + }); + it("should infer boolean from x instanceof y", function () { + const path = tsGetPath("x instanceof y").get("body")[0].get("expression"); + const type = path.getTypeAnnotation(); + expect(t.isBooleanTypeAnnotation(type)).toBeTruthy(); + }); + it("should infer number from 1 + 2", function () { + const path = tsGetPath("1 + 2").get("body")[0].get("expression"); + const type = path.getTypeAnnotation(); + expect(t.isNumberTypeAnnotation(type)).toBeTruthy(); + }); + it("should infer string|number from x + y", function () { + const path = tsGetPath("x + y").get("body")[0].get("expression"); + const type = path.getTypeAnnotation(); + expect(t.isUnionTypeAnnotation(type)).toBeTruthy(); + expect(t.isStringTypeAnnotation(type.types[0])).toBeTruthy(); + expect(t.isNumberTypeAnnotation(type.types[1])).toBeTruthy(); + }); + it("should infer type of tagged template literal", function () { + const path = tsGetPath("(function (): RegExp {}) `hey`") + .get("body")[0] + .get("expression"); + const type = path.getTypeAnnotation(); + expect(type.type).toBe("TSTypeReference"); + expect(type.typeName.name).toBe("RegExp"); + }); + it("should infer constant identifier", function () { + const path = tsGetPath("const x = 0; x").get("body.1.expression"); + const type = path.getTypeAnnotation(); + expect(t.isNumberTypeAnnotation(type)).toBeTruthy(); + }); + it("should infer indirect constant identifier", function () { + const path = tsGetPath("const x = 0; const y = x; y").get( + "body.2.expression", + ); + const type = path.getTypeAnnotation(); + expect(t.isNumberTypeAnnotation(type)).toBeTruthy(); + }); + it("should infer identifier type from if statement (===)", function () { + const path = tsGetPath( + `function test(x) { + if (x === true) x; + }`, + ).get("body.0.body.body.0.consequent.expression"); + const type = path.getTypeAnnotation(); + expect(t.isBooleanTypeAnnotation(type)).toBeTruthy(); + }); + it("should infer identifier type from if statement (typeof)", function () { + let path = tsGetPath( + `function test(x) { + if (typeof x == 'string') x; + }`, + ).get("body.0.body.body.0.consequent.expression"); + let type = path.getTypeAnnotation(); + expect(t.isStringTypeAnnotation(type)).toBeTruthy(); + path = tsGetPath( + `function test(x) { + if (typeof x === 'number') x; + }`, + ).get("body.0.body.body.0.consequent.expression"); + type = path.getTypeAnnotation(); + expect(t.isNumberTypeAnnotation(type)).toBeTruthy(); + }); + it("should infer identifier type from if statement (&&)", function () { + let path = tsGetPath( + `function test(x) { + if (typeof x == 'string' && x === 3) x; + }`, + ).get("body.0.body.body.0.consequent.expression"); + let type = path.getTypeAnnotation(); + expect(t.isUnionTypeAnnotation(type)).toBeTruthy(); + expect(t.isStringTypeAnnotation(type.types[0])).toBeTruthy(); + expect(t.isNumberTypeAnnotation(type.types[1])).toBeTruthy(); + path = tsGetPath( + `function test(x) { + if (true && x === 3) x; + }`, + ).get("body.0.body.body.0.consequent.expression"); + type = path.getTypeAnnotation(); + expect(t.isNumberTypeAnnotation(type)).toBeTruthy(); + path = tsGetPath( + `function test(x) { + if (x === 'test' && true) x; + }`, + ).get("body.0.body.body.0.consequent.expression"); + type = path.getTypeAnnotation(); + expect(t.isStringTypeAnnotation(type)).toBeTruthy(); + }); + it("should infer identifier type from if statement (||)", function () { + const path = tsGetPath( + `function test(x) { + if (typeof x == 'string' || x === 3) x; + }`, + ).get("body.0.body.body.0.consequent.expression"); + const type = path.getTypeAnnotation(); + expect(t.isAnyTypeAnnotation(type)).toBeTruthy(); + }); + it("should not infer identifier type from incorrect binding", function () { + const path = tsGetPath( + `function outer(x) { + if (x === 3) { + function inner(x) { + x; + } + } + }`, + ).get("body.0.body.body.0.consequent.body.0.body.body.0.expression"); + const type = path.getTypeAnnotation(); + expect(t.isAnyTypeAnnotation(type)).toBeTruthy(); + }); + it("should not cause a stack overflow when two variable depend on eachother", function () { + const path = tsGetPath(` + var b, c; + while (0) { + c = 1; + b = c; + } + c = b; + `).get("body.2.expression"); + + expect(path.toString()).toBe("c = b"); + + // Note: this could technically be "number | void", but the cycle detection + // logic just bails out to "any" to avoid infinite loops. + expect(path.getTypeAnnotation()).toEqual({ type: "AnyTypeAnnotation" }); + }); + }); + describe(`isGenericType("Array")`, () => { + it.each([ + "var x = Array()", + "var x = Array.from([])", + "var x = Object.keys({})", + "var x = Object.values({})", + "var x = Object.entries({})", + "var x = new Array", + "const x = [];", + "const x = (function (): Array { return []; })()", + "const x = (function (): T[] { return []; })()", + "const x = (v as Array)", + "const x = ({}, [])", + "var x = true ? a() : b(); function a(): Array {}; function b(): Array {}", + "var x = a() || b(); function a(): Array {}; function b(): Array {}", + "let x = []!", + ])(`NodePath(%p).isGenericType("Array") should be true`, input => { + const path = tsGetPath(input).get("body.0.declarations.0"); + expect(path.isGenericType("Array")).toBe(true); + }); + it.each([ + "x = Array()", + "x = Array.from([])", + "x = Object.keys({})", + "x = Object.values({})", + "x = Object.entries({})", + "x = new Array", + "x = [];", + "x = (function (): Array { return []; })()", + "x = (function (): T[] { return []; })()", + "x = (v as Array)", + "x = ({}, [])", + ])(`NodePath(%p).isGenericType("Array") should be true`, input => { + const path = tsGetPath(input).get("body.0.expression"); + expect(path.isGenericType("Array")).toBe(true); + }); + }); }); diff --git a/packages/babel-types/src/builders/typescript/createTSUnionType.ts b/packages/babel-types/src/builders/typescript/createTSUnionType.ts index a24b3b0b62c1..1ff47c37cddf 100644 --- a/packages/babel-types/src/builders/typescript/createTSUnionType.ts +++ b/packages/babel-types/src/builders/typescript/createTSUnionType.ts @@ -1,5 +1,6 @@ import { tsUnionType } from "../generated"; import removeTypeDuplicates from "../../modifications/typescript/removeTypeDuplicates"; +import { isTSTypeAnnotation } from "../../validators/generated/index"; import type * as t from "../.."; /** @@ -7,9 +8,11 @@ import type * as t from "../.."; * returns a `UnionTypeAnnotation` node containing them. */ export default function createTSUnionType( - typeAnnotations: Array, + typeAnnotations: Array, ): t.TSType { - const types = typeAnnotations.map(type => type.typeAnnotation); + const types = typeAnnotations.map(type => { + return isTSTypeAnnotation(type) ? type.typeAnnotation : type; + }); const flattened = removeTypeDuplicates(types); if (flattened.length === 1) { diff --git a/packages/babel-types/src/modifications/flow/removeTypeDuplicates.ts b/packages/babel-types/src/modifications/flow/removeTypeDuplicates.ts index f9885552a00d..55b09d5e553b 100644 --- a/packages/babel-types/src/modifications/flow/removeTypeDuplicates.ts +++ b/packages/babel-types/src/modifications/flow/removeTypeDuplicates.ts @@ -20,11 +20,8 @@ export default function removeTypeDuplicates( // todo(babel-8): change type to Array<...> nodes: ReadonlyArray, ): t.FlowType[] { - const generics: Record = {}; - const bases = {} as Record< - t.FlowBaseAnnotation["type"], - t.FlowBaseAnnotation - >; + const generics = new Map(); + const bases = new Map(); // store union type groups to circular references const typeGroups = new Set(); @@ -46,7 +43,7 @@ export default function removeTypeDuplicates( } if (isFlowBaseAnnotation(node)) { - bases[node.type] = node; + bases.set(node.type, node); continue; } @@ -63,8 +60,8 @@ export default function removeTypeDuplicates( if (isGenericTypeAnnotation(node)) { const name = getQualifiedName(node.id); - if (generics[name]) { - let existing: t.Flow = generics[name]; + if (generics.has(name)) { + let existing: t.Flow = generics.get(name); if (existing.typeParameters) { if (node.typeParameters) { existing.typeParameters.params = removeTypeDuplicates( @@ -75,7 +72,7 @@ export default function removeTypeDuplicates( existing = node.typeParameters; } } else { - generics[name] = node; + generics.set(name, node); } continue; @@ -85,13 +82,13 @@ export default function removeTypeDuplicates( } // add back in bases - for (const type of Object.keys(bases) as (keyof typeof bases)[]) { - types.push(bases[type]); + for (const [, baseType] of bases) { + types.push(baseType); } // add back in generics - for (const name of Object.keys(generics)) { - types.push(generics[name]); + for (const [, genericName] of generics) { + types.push(genericName); } return types; diff --git a/packages/babel-types/src/modifications/typescript/removeTypeDuplicates.ts b/packages/babel-types/src/modifications/typescript/removeTypeDuplicates.ts index 28f811a4e86c..ffb9277ed56e 100644 --- a/packages/babel-types/src/modifications/typescript/removeTypeDuplicates.ts +++ b/packages/babel-types/src/modifications/typescript/removeTypeDuplicates.ts @@ -1,18 +1,26 @@ import { + isIdentifier, isTSAnyKeyword, + isTSTypeReference, isTSUnionType, isTSBaseType, } from "../../validators/generated"; import type * as t from "../.."; +function getQualifiedName(node: t.TSTypeReference["typeName"]): string { + return isIdentifier(node) + ? node.name + : `${node.right.name}.${getQualifiedName(node.left)}`; +} + /** * Dedupe type annotations. */ export default function removeTypeDuplicates( nodes: Array, ): Array { - const generics = {}; - const bases = {} as Record; + const generics = new Map(); + const bases = new Map(); // store union type groups to circular references const typeGroups = new Set(); @@ -35,7 +43,7 @@ export default function removeTypeDuplicates( // Analogue of FlowBaseAnnotation if (isTSBaseType(node)) { - bases[node.type] = node; + bases.set(node.type, node); continue; } @@ -47,22 +55,39 @@ export default function removeTypeDuplicates( continue; } - // TODO: add generic types + // todo: support merging tuples: number[] + if (isTSTypeReference(node) && node.typeParameters) { + const name = getQualifiedName(node.typeName); + + if (generics.has(name)) { + let existing: t.TypeScript = generics.get(name); + if (existing.typeParameters) { + if (node.typeParameters) { + existing.typeParameters.params = removeTypeDuplicates( + existing.typeParameters.params.concat(node.typeParameters.params), + ); + } + } else { + existing = node.typeParameters; + } + } else { + generics.set(name, node); + } + + continue; + } types.push(node); } // add back in bases - for (const type of Object.keys(bases) as (keyof typeof bases)[]) { - types.push(bases[type]); + for (const [, baseType] of bases) { + types.push(baseType); } // add back in generics - for (const name of Object.keys(generics)) { - types.push( - // @ts-ignore generics are not implemented - generics[name], - ); + for (const [, genericName] of generics) { + types.push(genericName); } return types;