Skip TSAsExpression when transforming spread in CallExpression (#11404)

* Skip TSAsExpression when transforming spread in CallExpression

* Create @babel/helper-get-call-context package

* Support OptionalCallExpressions

* Use helper in optional chaining plugin, and move tests

* Update package.json files

* Use dot notation to access property

* Remove private method tests until future MR

* Update packages/babel-plugin-transform-spread/package.json

* Rename @babel/helper-get-call-context to @babel/helper-skip-transparent-expr-wrappers

* Handle typed OptionalMemberExpressions

* Make @babel/helper-skip-transparent-expr-wrappers a dependency

* Support TSNonNullExpressions

* Use named import instead of default

* Add test for call context when parenthesized call expression has type

* Improve handling of member expressions inside transparent expression wrappers

* Add comment explaining what a transparent expression wrapper is

* Add newlines to test fixtures

* Pass correct parameter type to skipTransparentExprWrappers

* Rename to babel-helper-skip-transparent-expression-wrappers

* Remove getCallContext helper

* Fixed exports key

* Preserve types in babel-plugin-transform-spread tests

* Use external-helpers to avoid inlining helper functions in tests

Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
This commit is contained in:
Oliver Dunk 2020-07-30 19:17:37 +01:00 committed by GitHub
parent 32e7bb4027
commit db56261414
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 224 additions and 33 deletions

View File

@ -0,0 +1,3 @@
src
test
*.log

View File

@ -0,0 +1,17 @@
# @babel/helper-skip-transparent-expression-wrappers
> Helper which skips types and parentheses
## Install
Using npm:
```sh
npm install --save-dev @babel/helper-skip-transparent-expression-wrappers
```
or using yarn:
```sh
yarn add @babel/helper-skip-transparent-expression-wrappers --dev
```

View File

@ -0,0 +1,25 @@
{
"name": "@babel/helper-skip-transparent-expression-wrappers",
"version": "7.9.6",
"description": "Helper which skips types and parentheses",
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-helper-skip-transparent-expression-wrappers"
},
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"exports": {
".": "./lib/index.js",
"./package.json": "./package.json"
},
"dependencies": {
"@babel/types": "^7.9.6"
},
"devDependencies": {
"@babel/traverse": "^7.9.6"
}
}

View File

@ -0,0 +1,27 @@
// @flow
import * as t from "@babel/types";
import type { NodePath } from "@babel/traverse";
// A transparent expression wrapper is an AST node that most plugins will wish
// to skip, as its presence does not affect the behaviour of the code. This
// includes expressions used for types, and extra parenthesis. For example, in
// (a as any)(), this helper can be used to skip the TSAsExpression when
// determining the callee.
export function isTransparentExprWrapper(node: Node) {
return (
t.isTSAsExpression(node) ||
t.isTSTypeAssertion(node) ||
t.isTSNonNullExpression(node) ||
t.isTypeCastExpression(node) ||
t.isParenthesizedExpression(node)
);
}
export function skipTransparentExprWrappers(path: NodePath): NodePath {
while (isTransparentExprWrapper(path.node)) {
path = path.get("expression");
}
return path;
}

View File

@ -17,7 +17,8 @@
],
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4",
"@babel/plugin-syntax-optional-chaining": "^7.8.0"
"@babel/plugin-syntax-optional-chaining": "^7.8.0",
"@babel/helper-skip-transparent-expression-wrappers": "^7.9.6"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"

View File

@ -1,4 +1,8 @@
import { declare } from "@babel/helper-plugin-utils";
import {
isTransparentExprWrapper,
skipTransparentExprWrappers,
} from "@babel/helper-skip-transparent-expression-wrappers";
import syntaxOptionalChaining from "@babel/plugin-syntax-optional-chaining";
import { types as t } from "@babel/core";
@ -8,6 +12,7 @@ export default declare((api, options) => {
const { loose = false } = options;
function isSimpleMemberExpression(expression) {
expression = skipTransparentExprWrappers(expression);
return (
t.isIdentifier(expression) ||
t.isSuper(expression) ||
@ -24,16 +29,16 @@ export default declare((api, options) => {
visitor: {
"OptionalCallExpression|OptionalMemberExpression"(path) {
const { scope } = path;
// maybeParenthesized points to the outermost parenthesizedExpression
// maybeWrapped points to the outermost transparent expression wrapper
// or the path itself
let maybeParenthesized = path;
let maybeWrapped = path;
const parentPath = path.findParent(p => {
if (!p.isParenthesizedExpression()) return true;
maybeParenthesized = p;
if (!isTransparentExprWrapper(p)) return true;
maybeWrapped = p;
});
let isDeleteOperation = false;
const parentIsCall =
parentPath.isCallExpression({ callee: maybeParenthesized.node }) &&
parentPath.isCallExpression({ callee: maybeWrapped.node }) &&
// note that the first condition must implies that `path.optional` is `true`,
// otherwise the parentPath should be an OptionalCallExpressioin
path.isOptionalMemberExpression();
@ -43,9 +48,7 @@ export default declare((api, options) => {
let optionalPath = path;
while (
optionalPath.isOptionalMemberExpression() ||
optionalPath.isOptionalCallExpression() ||
optionalPath.isParenthesizedExpression() ||
optionalPath.isTSNonNullExpression()
optionalPath.isOptionalCallExpression()
) {
const { node } = optionalPath;
if (node.optional) {
@ -54,13 +57,14 @@ export default declare((api, options) => {
if (optionalPath.isOptionalMemberExpression()) {
optionalPath.node.type = "MemberExpression";
optionalPath = optionalPath.get("object");
optionalPath = skipTransparentExprWrappers(
optionalPath.get("object"),
);
} else if (optionalPath.isOptionalCallExpression()) {
optionalPath.node.type = "CallExpression";
optionalPath = optionalPath.get("callee");
} else {
// unwrap TSNonNullExpression/ParenthesizedExpression if needed
optionalPath = optionalPath.get("expression");
optionalPath = skipTransparentExprWrappers(
optionalPath.get("callee"),
);
}
}
@ -74,7 +78,13 @@ export default declare((api, options) => {
const isCall = t.isCallExpression(node);
const replaceKey = isCall ? "callee" : "object";
const chain = node[replaceKey];
const chainWithTypes = node[replaceKey];
let chain = chainWithTypes;
while (isTransparentExprWrapper(chain)) {
chain = chain.expression;
}
let ref;
let check;
@ -86,20 +96,22 @@ export default declare((api, options) => {
// If we are using a loose transform (avoiding a Function#call) and we are at the call,
// we can avoid a needless memoize. We only do this if the callee is a simple member
// expression, to avoid multiple calls to nested call expressions.
check = ref = chain;
check = ref = chainWithTypes;
} else {
ref = scope.maybeGenerateMemoised(chain);
if (ref) {
check = t.assignmentExpression(
"=",
t.cloneNode(ref),
// Here `chain` MUST NOT be cloned because it could be updated
// when generating the memoised context of a call espression
chain,
// Here `chainWithTypes` MUST NOT be cloned because it could be
// updated when generating the memoised context of a call
// expression
chainWithTypes,
);
node[replaceKey] = ref;
} else {
check = ref = chain;
check = ref = chainWithTypes;
}
}
@ -109,7 +121,7 @@ export default declare((api, options) => {
if (loose && isSimpleMemberExpression(chain)) {
// To avoid a Function#call, we can instead re-grab the property from the context object.
// `a.?b.?()` translates roughly to `_a.b != null && _a.b()`
node.callee = chain;
node.callee = chainWithTypes;
} else {
// Otherwise, we need to memoize the context object, and change the call into a Function#call.
// `a.?b.?()` translates roughly to `(_b = _a.b) != null && _b.call(_a)`
@ -137,7 +149,9 @@ export default declare((api, options) => {
// i.e. `?.b` in `(a?.b.c)()`
if (i === 0 && parentIsCall) {
// `(a?.b)()` to `(a == null ? undefined : a.b.bind(a))()`
const { object } = replacement;
const object = skipTransparentExprWrappers(
replacementPath.get("object"),
).node;
let baseRef;
if (!loose || !isSimpleMemberExpression(object)) {
// memoize the context object in non-loose mode
@ -180,7 +194,9 @@ export default declare((api, options) => {
),
);
replacementPath = replacementPath.get("alternate");
replacementPath = skipTransparentExprWrappers(
replacementPath.get("alternate"),
);
}
},
},

View File

@ -1,10 +1,10 @@
var _a, _a2, _a3, _b, _a4, _ref, _a5, _c, _a6, _a7;
var _a, _a2, _a3, _b, _a4, _a4$b, _a5, _c, _a6, _a7;
(_a = a) === null || _a === void 0 ? void 0 : _a.b!.c;
(_a2 = a) === null || _a2 === void 0 ? void 0 : _a2.b!.c.d;
(_a3 = a) === null || _a3 === void 0 ? void 0 : _a3.b.c!.d;
(_b = a!.b) === null || _b === void 0 ? void 0 : _b.c;
(_a4 = a) === null || _a4 === void 0 ? void 0 : (_ref = _a4.b!) === null || _ref === void 0 ? void 0 : _ref.c;
(_a4 = a) === null || _a4 === void 0 ? void 0 : (_a4$b = _a4.b!) === null || _a4$b === void 0 ? void 0 : _a4$b.c;
(_a5 = a) === null || _a5 === void 0 ? void 0 : (_c = _a5.b!.c) === null || _c === void 0 ? void 0 : _c.c;
((_a6 = a) === null || _a6 === void 0 ? void 0 : _a6.b)!.c;
((_a7 = a) === null || _a7 === void 0 ? void 0 : _a7.b)!.c;

View File

@ -1,10 +1,10 @@
var _a, _a2, _a3, _b, _a4, _ref, _a5, _c, _a6, _a7;
var _a, _a2, _a3, _b, _a4, _a4$b, _a5, _c, _a6, _a7;
(_a = a) === null || _a === void 0 ? void 0 : _a.b.c;
(_a2 = a) === null || _a2 === void 0 ? void 0 : _a2.b.c.d;
(_a3 = a) === null || _a3 === void 0 ? void 0 : _a3.b.c.d;
(_b = a.b) === null || _b === void 0 ? void 0 : _b.c;
(_a4 = a) === null || _a4 === void 0 ? void 0 : (_ref = _a4.b) === null || _ref === void 0 ? void 0 : _ref.c;
(_a4 = a) === null || _a4 === void 0 ? void 0 : (_a4$b = _a4.b) === null || _a4$b === void 0 ? void 0 : _a4$b.c;
(_a5 = a) === null || _a5 === void 0 ? void 0 : (_c = _a5.b.c) === null || _c === void 0 ? void 0 : _c.c;
((_a6 = a) === null || _a6 === void 0 ? void 0 : _a6.b).c;
((_a7 = a) === null || _a7 === void 0 ? void 0 : _a7.b).c;

View File

@ -0,0 +1,3 @@
{
"plugins": ["proposal-optional-chaining"]
}

View File

@ -0,0 +1,10 @@
{
"plugins": [
[
"syntax-typescript"
],
[
"proposal-optional-chaining"
]
]
}

View File

@ -0,0 +1,3 @@
var _a$b, _a;
(_a$b = ((_a = a).b as any)) === null || _a$b === void 0 ? void 0 : _a$b.call(_a);

View File

@ -0,0 +1 @@
(((foo as A).bar) as B)?.(foo.bar, false)

View File

@ -0,0 +1,13 @@
{
"plugins": [
[
"syntax-typescript"
],
[
"proposal-optional-chaining",
{
"loose": true
}
]
]
}

View File

@ -0,0 +1,3 @@
var _bar, _ref;
(_bar = ((_ref = (foo as A)).bar as B)) == null ? void 0 : _bar.call(_ref, foo.bar, false);

View File

@ -0,0 +1 @@
(a?.b as ExampleType)?.c as ExampleType2

View File

@ -0,0 +1,10 @@
{
"plugins": [
[
"syntax-typescript"
],
[
"proposal-optional-chaining"
]
]
}

View File

@ -0,0 +1,3 @@
var _a, _a$b;
(((_a = a) === null || _a === void 0 ? void 0 : (_a$b = (_a.b as ExampleType)) === null || _a$b === void 0 ? void 0 : _a$b.c) as ExampleType2);

View File

@ -0,0 +1,10 @@
{
"plugins": [
[
"syntax-typescript"
],
[
"proposal-optional-chaining"
]
]
}

View File

@ -0,0 +1,3 @@
var _o, _o$Foo;
(((_o = o) === null || _o === void 0 ? void 0 : (_o$Foo = _o.Foo).m.bind(_o$Foo)) as ExampleType)();

View File

@ -16,7 +16,8 @@
"babel-plugin"
],
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4"
"@babel/helper-plugin-utils": "^7.10.4",
"@babel/helper-skip-transparent-expression-wrappers": "7.9.6"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"

View File

@ -1,4 +1,5 @@
import { declare } from "@babel/helper-plugin-utils";
import { skipTransparentExprWrappers } from "@babel/helper-skip-transparent-expression-wrappers";
import { types as t } from "@babel/core";
export default declare((api, options) => {
@ -94,7 +95,8 @@ export default declare((api, options) => {
const args = node.arguments;
if (!hasSpread(args)) return;
const calleePath = path.get("callee");
const calleePath = skipTransparentExprWrappers(path.get("callee"));
if (calleePath.isSuper()) return;
let contextLiteral = scope.buildUndefinedNode();
@ -120,7 +122,7 @@ export default declare((api, options) => {
node.arguments.push(first);
}
const callee = node.callee;
const callee = calleePath.node;
if (calleePath.isMemberExpression()) {
const temp = scope.maybeGenerateMemoised(callee.object);
@ -130,11 +132,11 @@ export default declare((api, options) => {
} else {
contextLiteral = t.cloneNode(callee.object);
}
t.appendToMemberExpression(callee, t.identifier("apply"));
} else {
node.callee = t.memberExpression(node.callee, t.identifier("apply"));
}
// We use the original callee here, to preserve any types/parentheses
node.callee = t.memberExpression(node.callee, t.identifier("apply"));
if (t.isSuper(contextLiteral)) {
contextLiteral = t.thisExpression();
}

View File

@ -0,0 +1 @@
(a.b: any)(...args)

View File

@ -0,0 +1,3 @@
{
"plugins": ["external-helpers", "transform-spread", "syntax-flow"]
}

View File

@ -0,0 +1,3 @@
var _a;
((_a = a).b: any).apply(_a, babelHelpers.toConsumableArray(args));

View File

@ -0,0 +1,3 @@
{
"plugins": ["external-helpers", "transform-spread", "transform-parameters"]
}

View File

@ -0,0 +1,5 @@
{
"parserOpts": {
"createParenthesizedExpressions": true
}
}

View File

@ -0,0 +1,3 @@
var _a;
((_a = a).b).apply(_a, babelHelpers.toConsumableArray(args));

View File

@ -0,0 +1 @@
(<any> a.b)(...args)

View File

@ -0,0 +1,3 @@
{
"plugins": ["external-helpers", "transform-spread", "syntax-typescript"]
}

View File

@ -0,0 +1,3 @@
var _a;
(<any> (_a = a).b).apply(_a, babelHelpers.toConsumableArray(args));

View File

@ -0,0 +1 @@
(dog.bark as any)(...args)

View File

@ -0,0 +1,7 @@
{
"presets": [
[
"typescript"
]
]
}

View File

@ -0,0 +1,3 @@
var _dog;
(_dog = dog).bark.apply(_dog, babelHelpers.toConsumableArray(args));