flesh out removal hooks more

This commit is contained in:
Sebastian McKenzie 2015-05-25 15:28:11 +01:00
parent f4a3b2f01a
commit 334cbb06c6

View File

@ -1,20 +1,128 @@
// This file contains all the cases where we have to perform additional logic when performing
// specific operations in order to retain correct JavaScript semantics.
// this file contains all the cases where we have to perform additional logic when performing
// specific operations in order to retain correct javascript semantics
// guidelines for rules:
//
// - removing a node must **never** result in invalid javascript
// - always clean up the ancestry
// - worry about execution sideeffects of removing entire nodes and replace with siblings/children
//
import * as t from "../../../types";
// todo:
//
// - catch clause param
// - property key
// - tagged template literal tag
//
// pre hooks should be used for either rejecting removal or delegating removal to a replacement
export var pre = [
function (self, parent) {
if (self.key === "body" && self.isBlockStatement() && parent.isFunction()) {
// lol nah, you'll break stuff
function (self) {
if (self.isClassDeclaration() || self.isFunctionDeclaration()) {
// removing a class/function declaration so we need to remove it's binding
self.scope.parent.removeBinding(self.id.name);
}
},
function (self) {
if (self.isVariableDeclaration()) {
// we're removing a variable declaration so remove each declarator separately to call
// the specific hooks
var declarators = self.get("declarations");
for (var declarator of (declarators: Array)) {
declarator.remove();
}
return true;
} else if (self.isVariableDeclarator()) {
// we're removing a variable declarator so remove all it's bindings
var ids = this.getBindingIdentifiers();
for (var name in ids) {
self.scope.removeBinding(name);
}
}
},
function (self) {
if (self.key === "body" && (self.isBlockStatement() || self.isClassBody())) {
// function () NODE
// class NODE
// attempting to remove a block statement that's someones body so let's just clear all the inner
// statements instead
self.node.body = [];
return true;
}
},
function (self, parent) {
// attempting to remove body of an arrow function so we just replace it with undefined
if (self.key === "body" && parent.isArrowFunctionExpression()) {
if (self.key === "id" && parent.isClassDeclaration() && !self.scope.isPure(self.node.superClass)) {
// class NODE extends superClass() {}
// removing the id of a class declaration with an impure super class so we just replace it with the
// super class
self.replaceWithMultiple([
t.expressionStatement(self.node.superClass),
t.identifier("undefined")
]);
return true;
}
},
function (self, parent) {
if (self.key === "callee" && parent.isCallExpression()) {
// NODE();
// attempting to remove the callee of a call expression, in order to retain semantics we'll turn all
// the arguments into a sequence expression and return `undefined`
var expressions = parent.node.arguments;
// filter expressions that don't effect the execution of the code
expressions = expressions.filter((node) => !self.scope.isPure(node));
// push undefined as otherwise we'll evaluate to the last argument
expressions.push(t.identifier("undefined"));
if (expressions.length === 1) {
parent.replaceWith(expressions[0]);
} else {
parent.replaceWith(t.sequenceExpression(expressions));
}
return true;
}
},
function (self, parent) {
if (self.key === "test" && parent.isConditionalExpression()) {
// NODE ? consequent : alternate
// removing the test of a conditional so just use the alternate since consequent will never be hit
parent.replaceWith(parent.node.alternate);
return true;
}
},
function (self, parent) {
if (self.key === "test" && parent.IfStatement() && parent.node.alternate) {
// if (NODE) consequent else alternate
// removing the test of an if statement and there's an alternate so just replace the entire if with it
// todo: check if this is a BlockStatement and whether it has any block scoped variables
parent.replaceWith(parent.node.alternate);
return true;
}
},
function (self, parent) {
var replace = false;
// () => NODE;
// removing the body of an arrow function
replace = replace || (self.key === "body" && parent.isArrowFunctionExpression());
// throw NODE;
// removing a throw statement argument
replace = replace || (self.key === "argument" && parent.isThrowStatement());
if (replace) {
self.replaceWith(t.identifier("undefined"));
return true;
}
@ -26,18 +134,43 @@ export var post = [
function (self, parent) {
var removeParent = false;
// while (NODE);
// removing the test of a while/switch, we can either just remove it entirely *or* turn the `test` into `true`
// unlikely that the latter will ever be what's wanted so we just remove the loop to avoid infinite recursion
removeParent = removeParent || (self.key === "test" && (parent.isWhile() || parent.isSwitchCase()));
// function NODE() {}
// class NODE {}
// attempting to remove the id of a declaration, no choice really but to purge it
// if it's a class that has a super class that's not prue then it's handled in a pre hook
removeParent = removeParent || (self.key === "id" && (parent.isClassDeclaration() || parent.isFunctionDeclaration()));
// var NODE = init;
// kill variable declarator if we're removing it's initializer binding
// todo: possibly explode the variable declarations to retain it's initializer
removeParent = removeParent || (self.key === "init" && parent.isVariableDeclarator());
// export NODE;
// just remove a declaration for an export as this is no longer valid
removeParent = removeParent || (self.key === "declaration" && parent.isExportDeclaration());
// stray labels with no body
// label: NODE
// stray labeled statement with no body
removeParent = removeParent || (self.key === "body" && parent.isLabeledStatement());
// var NODE;
// remove an entire declaration if there are no declarators left
removeParent = removeParent || (parent.isVariableDeclaration() && parent.node.declarations.length === 0);
removeParent = removeParent || (self.containerKey === "declarations" && parent.isVariableDeclaration() && parent.node.declarations.length === 0);
// NODE;
// remove the entire expression statement if there's no expression
removeParent = removeParent || (self.key === "expression" && parent.isExpressionStatement());
// if (NODE);
// remove the entire if since the consequent is never going to be hit, if there's an alternate then it's already been
// handled with the `pre` hook
removeParent = removeParent || (self.key === "test" && parent.isIfStatement());
if (removeParent) {
parent.remove();
return true;
@ -45,17 +178,20 @@ export var post = [
},
function (self, parent) {
// we've just removed the second element of a sequence expression so let's turn that sequence
// expression into a regular expression
if (parent.isSequenceExpression() && parent.node.expressions.length === 1) {
// (node, NODE);
// we've just removed the second element of a sequence expression so let's turn that sequence
// expression into a regular expression
parent.replaceWith(parent.node.expressions[0]);
return true;
}
},
function (self, parent) {
// we're in a binary expression, better remove it and replace it with the last expression
if (parent.isBinary()) {
// left + NODE;
// NODE + right;
// we're in a binary expression, better remove it and replace it with the last expression
if (self.key === "left") {
parent.replaceWith(parent.node.right);
} else { // key === "right"