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
36 changed files with 224 additions and 33 deletions

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"),
);
}
},
},