Skip to content

Commit

Permalink
Skip TSAsExpression when transforming spread in CallExpression (#11404)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
oliverdunk and nicolo-ribaudo committed Jul 30, 2020
1 parent 32e7bb4 commit db56261
Show file tree
Hide file tree
Showing 36 changed files with 224 additions and 33 deletions.
@@ -0,0 +1,3 @@
src
test
*.log
@@ -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
```
@@ -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"
}
}
@@ -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;
}
Expand Up @@ -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"
Expand Down
60 changes: 38 additions & 22 deletions packages/babel-plugin-proposal-optional-chaining/src/index.js
@@ -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";

Expand All @@ -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) ||
Expand All @@ -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();
Expand All @@ -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) {
Expand All @@ -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"),
);
}
}

Expand All @@ -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;
Expand All @@ -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;
}
}

Expand All @@ -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)`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -180,7 +194,9 @@ export default declare((api, options) => {
),
);

replacementPath = replacementPath.get("alternate");
replacementPath = skipTransparentExprWrappers(
replacementPath.get("alternate"),
);
}
},
},
Expand Down
@@ -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;
@@ -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;
@@ -0,0 +1,3 @@
{
"plugins": ["proposal-optional-chaining"]
}
@@ -0,0 +1 @@
(a.b as any)?.()
@@ -0,0 +1,10 @@
{
"plugins": [
[
"syntax-typescript"
],
[
"proposal-optional-chaining"
]
]
}
@@ -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);
@@ -0,0 +1 @@
(((foo as A).bar) as B)?.(foo.bar, false)
@@ -0,0 +1,13 @@
{
"plugins": [
[
"syntax-typescript"
],
[
"proposal-optional-chaining",
{
"loose": true
}
]
]
}
@@ -0,0 +1,3 @@
var _bar, _ref;

(_bar = ((_ref = (foo as A)).bar as B)) == null ? void 0 : _bar.call(_ref, foo.bar, false);
@@ -0,0 +1 @@
(a?.b as ExampleType)?.c as ExampleType2
@@ -0,0 +1,10 @@
{
"plugins": [
[
"syntax-typescript"
],
[
"proposal-optional-chaining"
]
]
}
@@ -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);
@@ -0,0 +1 @@
((o?.Foo.m) as ExampleType)()
@@ -0,0 +1,10 @@
{
"plugins": [
[
"syntax-typescript"
],
[
"proposal-optional-chaining"
]
]
}
@@ -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)();
3 changes: 2 additions & 1 deletion packages/babel-plugin-transform-spread/package.json
Expand Up @@ -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"
Expand Down
12 changes: 7 additions & 5 deletions packages/babel-plugin-transform-spread/src/index.js
@@ -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) => {
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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();
}
Expand Down
@@ -0,0 +1 @@
(a.b: any)(...args)
@@ -0,0 +1,3 @@
{
"plugins": ["external-helpers", "transform-spread", "syntax-flow"]
}
@@ -0,0 +1,3 @@
var _a;

((_a = a).b: any).apply(_a, babelHelpers.toConsumableArray(args));
@@ -0,0 +1,3 @@
{
"plugins": ["external-helpers", "transform-spread", "transform-parameters"]
}
@@ -0,0 +1 @@
(a.b)(...args)

0 comments on commit db56261

Please sign in to comment.