When there is a variable declaration inside the function body, which shares its name to a referenced identifier in default parameter expression, the function body should be wrapped into iife, otherwise the binding in default parameter scope will be shadowed by function body.
157 lines
4.2 KiB
JavaScript
157 lines
4.2 KiB
JavaScript
import callDelegate from "@babel/helper-call-delegate";
|
|
import { template, types as t } from "@babel/core";
|
|
|
|
const buildDefaultParam = template(`
|
|
let VARIABLE_NAME =
|
|
arguments.length > ARGUMENT_KEY && arguments[ARGUMENT_KEY] !== undefined ?
|
|
arguments[ARGUMENT_KEY]
|
|
:
|
|
DEFAULT_VALUE;
|
|
`);
|
|
|
|
const buildLooseDefaultParam = template(`
|
|
if (ASSIGNMENT_IDENTIFIER === UNDEFINED) {
|
|
ASSIGNMENT_IDENTIFIER = DEFAULT_VALUE;
|
|
}
|
|
`);
|
|
|
|
const buildLooseDestructuredDefaultParam = template(`
|
|
let ASSIGNMENT_IDENTIFIER = PARAMETER_NAME === UNDEFINED ? DEFAULT_VALUE : PARAMETER_NAME ;
|
|
`);
|
|
|
|
const buildSafeArgumentsAccess = template(`
|
|
let $0 = arguments.length > $1 ? arguments[$1] : undefined;
|
|
`);
|
|
|
|
function isSafeBinding(scope, node) {
|
|
if (!scope.hasOwnBinding(node.name)) return true;
|
|
const { kind } = scope.getOwnBinding(node.name);
|
|
return kind === "param" || kind === "local";
|
|
}
|
|
|
|
const iifeVisitor = {
|
|
ReferencedIdentifier(path, state) {
|
|
const { scope, node } = path;
|
|
if (
|
|
node.name === "eval" ||
|
|
!isSafeBinding(scope, node) ||
|
|
!isSafeBinding(state.scope, node)
|
|
) {
|
|
state.iife = true;
|
|
path.stop();
|
|
}
|
|
},
|
|
|
|
Scope(path) {
|
|
// different bindings
|
|
path.skip();
|
|
},
|
|
};
|
|
|
|
export default function convertFunctionParams(path, loose) {
|
|
const { node, scope } = path;
|
|
|
|
const state = {
|
|
iife: false,
|
|
scope: scope,
|
|
};
|
|
|
|
const body = [];
|
|
const params = path.get("params");
|
|
|
|
let firstOptionalIndex = null;
|
|
|
|
for (let i = 0; i < params.length; i++) {
|
|
const param = params[i];
|
|
|
|
const paramIsAssignmentPattern = param.isAssignmentPattern();
|
|
if (paramIsAssignmentPattern && (loose || node.kind === "set")) {
|
|
const left = param.get("left");
|
|
const right = param.get("right");
|
|
|
|
const undefinedNode = scope.buildUndefinedNode();
|
|
|
|
if (left.isIdentifier()) {
|
|
body.push(
|
|
buildLooseDefaultParam({
|
|
ASSIGNMENT_IDENTIFIER: t.cloneNode(left.node),
|
|
DEFAULT_VALUE: right.node,
|
|
UNDEFINED: undefinedNode,
|
|
}),
|
|
);
|
|
param.replaceWith(left.node);
|
|
} else if (left.isObjectPattern() || left.isArrayPattern()) {
|
|
const paramName = scope.generateUidIdentifier();
|
|
body.push(
|
|
buildLooseDestructuredDefaultParam({
|
|
ASSIGNMENT_IDENTIFIER: left.node,
|
|
DEFAULT_VALUE: right.node,
|
|
PARAMETER_NAME: t.cloneNode(paramName),
|
|
UNDEFINED: undefinedNode,
|
|
}),
|
|
);
|
|
param.replaceWith(paramName);
|
|
}
|
|
} else if (paramIsAssignmentPattern) {
|
|
if (firstOptionalIndex === null) firstOptionalIndex = i;
|
|
|
|
const left = param.get("left");
|
|
const right = param.get("right");
|
|
|
|
if (!state.iife) {
|
|
if (right.isIdentifier() && !isSafeBinding(scope, right.node)) {
|
|
// the right hand side references a parameter
|
|
state.iife = true;
|
|
} else {
|
|
right.traverse(iifeVisitor, state);
|
|
}
|
|
}
|
|
|
|
const defNode = buildDefaultParam({
|
|
VARIABLE_NAME: left.node,
|
|
DEFAULT_VALUE: right.node,
|
|
ARGUMENT_KEY: t.numericLiteral(i),
|
|
});
|
|
body.push(defNode);
|
|
} else if (firstOptionalIndex !== null) {
|
|
const defNode = buildSafeArgumentsAccess([
|
|
param.node,
|
|
t.numericLiteral(i),
|
|
]);
|
|
body.push(defNode);
|
|
} else if (param.isObjectPattern() || param.isArrayPattern()) {
|
|
const uid = path.scope.generateUidIdentifier("ref");
|
|
|
|
const defNode = t.variableDeclaration("let", [
|
|
t.variableDeclarator(param.node, uid),
|
|
]);
|
|
body.push(defNode);
|
|
|
|
param.replaceWith(t.cloneNode(uid));
|
|
}
|
|
|
|
if (!state.iife && !param.isIdentifier()) {
|
|
param.traverse(iifeVisitor, state);
|
|
}
|
|
}
|
|
|
|
if (body.length === 0) return false;
|
|
|
|
// we need to cut off all trailing parameters
|
|
if (firstOptionalIndex !== null) {
|
|
node.params = node.params.slice(0, firstOptionalIndex);
|
|
}
|
|
|
|
// ensure it's a block, useful for arrow functions
|
|
path.ensureBlock();
|
|
|
|
if (state.iife) {
|
|
body.push(callDelegate(path, scope));
|
|
path.set("body", t.blockStatement(body));
|
|
} else {
|
|
path.get("body").unshiftContainer("body", body);
|
|
}
|
|
|
|
return true;
|
|
}
|