Return inserted/replaced paths (#5710)

* Return inserted/replaced paths

This gives `Path`’s replacement and insertion methods a consistent
return value: the inserted/replaced paths.

Before, they could return `undefined`, a `node`, or a the current path
inside an array. It was kinda pointless.  But now they always return an
array of paths, which is useful for solving
https://github.com/babel/babel/pull/4935#discussion_r96151368.

* Return inserted nodes and not BlockStatement

Addded test for bug #4363

* Cleanups

- `#replaceWith` will now return the current path if it's the same node
- `#insertAfter` and `#insertBefore` use public Path APIs now
- Makes container insertion faster (single splice call)
- Use public APIs in container insertion
- Replacing a statement with an expression returns the expression's path
- Replacing an expression with multiple statements returns the inserted
  closure's body's paths.
This commit is contained in:
Justin Ridgewell 2017-09-11 16:07:04 -04:00 committed by GitHub
parent c47258d68c
commit 4daf11528c
7 changed files with 248 additions and 92 deletions

View File

@ -0,0 +1,18 @@
function WithoutCurlyBraces() {
if (true)
for (let k in kv) {
function foo() { return this }
function bar() { return foo.call(this) }
console.log(this, k) // => undefined
}
}
function WithCurlyBraces() {
if (true) {
for (let k in kv) {
function foo() { return this }
function bar() { return foo.call(this) }
console.log(this, k) // => 777
}
}
}

View File

@ -0,0 +1,43 @@
function WithoutCurlyBraces() {
var _this = this;
if (true) {
var _loop = function (k) {
function foo() {
return this;
}
function bar() {
return foo.call(this);
}
console.log(_this, k); // => undefined
};
for (var k in kv) {
_loop(k);
}
}
}
function WithCurlyBraces() {
var _this2 = this;
if (true) {
var _loop2 = function (k) {
function foo() {
return this;
}
function bar() {
return foo.call(this);
}
console.log(_this2, k); // => 777
};
for (var k in kv) {
_loop2(k);
}
}
}

View File

@ -44,7 +44,10 @@ export default function({ types: t }) {
if (node[VISITED]) return;
const inferred = nameFunction(path);
if (inferred && inferred !== node) return path.replaceWith(inferred);
if (inferred && inferred !== node) {
path.replaceWith(inferred);
return;
}
node[VISITED] = true;

View File

@ -109,7 +109,8 @@ export default function({ messages, template, types: t }) {
right.isGenericType("Array") ||
t.isArrayTypeAnnotation(right.getTypeAnnotation())
) {
return replaceWithArray(path);
replaceWithArray(path);
return;
}
let callback = spec;

View File

@ -24,20 +24,24 @@ export function insertBefore(nodes) {
(this.parentPath.isForStatement() && this.key === "init")
) {
if (this.node) nodes.push(this.node);
this.replaceExpressionWithStatements(nodes);
return this.replaceExpressionWithStatements(nodes);
} else if (Array.isArray(this.container)) {
return this._containerInsertBefore(nodes);
} else if (this.isStatementOrBlock()) {
if (this.node) nodes.push(this.node);
this.replaceWith(t.blockStatement(nodes));
const shouldInsertCurrentNode =
this.node &&
(!this.isExpressionStatement() || this.node.expression != null);
this.replaceWith(
t.blockStatement(shouldInsertCurrentNode ? [this.node] : []),
);
return this.unshiftContainer("body", nodes);
} else {
throw new Error(
"We don't know what to do with this node type. " +
"We were previously a Statement but we can't fit in here?",
);
}
return [this];
}
export function _containerInsert(from, nodes) {
@ -45,34 +49,14 @@ export function _containerInsert(from, nodes) {
const paths = [];
this.container.splice(from, 0, ...nodes);
for (let i = 0; i < nodes.length; i++) {
const to = from + i;
const node = nodes[i];
this.container.splice(to, 0, node);
const path = this.getSibling(`${to}`);
paths.push(path);
if (this.context) {
const path = this.context.create(
this.parent,
this.container,
to,
this.listKey,
);
// While this path may have a context, there is currently no guarantee that the context
// will be the active context, because `popContext` may leave a final context in place.
// We should remove this `if` and always push once #4145 has been resolved.
if (this.context.queue) path.pushContext(this.context);
paths.push(path);
} else {
paths.push(
NodePath.get({
parentPath: this.parentPath,
parent: this.parent,
container: this.container,
listKey: this.listKey,
key: to,
}),
);
if (this.context && this.context.queue) {
path.pushContext(this.context);
}
}
@ -124,26 +108,24 @@ export function insertAfter(nodes) {
);
nodes.push(t.expressionStatement(temp));
}
this.replaceExpressionWithStatements(nodes);
return this.replaceExpressionWithStatements(nodes);
} else if (Array.isArray(this.container)) {
return this._containerInsertAfter(nodes);
} else if (this.isStatementOrBlock()) {
// Unshift current node if it's not an empty expression
if (
const shouldInsertCurrentNode =
this.node &&
(!this.isExpressionStatement() || this.node.expression != null)
) {
nodes.unshift(this.node);
}
this.replaceWith(t.blockStatement(nodes));
(!this.isExpressionStatement() || this.node.expression != null);
this.replaceWith(
t.blockStatement(shouldInsertCurrentNode ? [this.node] : []),
);
return this.pushContainer("body", nodes);
} else {
throw new Error(
"We don't know what to do with this node type. " +
"We were previously a Statement but we can't fit in here?",
);
}
return [this];
}
/**

View File

@ -50,13 +50,14 @@ export function replaceWithMultiple(nodes: Array<Object>) {
t.inheritLeadingComments(nodes[0], this.node);
t.inheritTrailingComments(nodes[nodes.length - 1], this.node);
this.node = this.container[this.key] = null;
this.insertAfter(nodes);
const paths = this.insertAfter(nodes);
if (this.node) {
this.requeue();
} else {
this.remove();
}
return paths;
}
/**
@ -115,7 +116,7 @@ export function replaceWith(replacement) {
}
if (this.node === replacement) {
return;
return [this];
}
if (this.isProgram() && !t.isProgram(replacement)) {
@ -136,6 +137,8 @@ export function replaceWith(replacement) {
);
}
let nodePath = "";
if (this.isNodeType("Statement") && t.isExpression(replacement)) {
if (
!this.canHaveVariableDeclarationOrExpression() &&
@ -144,6 +147,7 @@ export function replaceWith(replacement) {
) {
// replacing a statement with an expression so wrap it in an expression statement
replacement = t.expressionStatement(replacement);
nodePath = "expression";
}
}
@ -172,6 +176,8 @@ export function replaceWith(replacement) {
// requeue for visiting
this.requeue();
return [nodePath ? this.get(nodePath) : this];
}
/**
@ -206,45 +212,45 @@ export function replaceExpressionWithStatements(nodes: Array<Object>) {
const toSequenceExpression = t.toSequenceExpression(nodes, this.scope);
if (toSequenceExpression) {
this.replaceWith(toSequenceExpression);
} else {
const container = t.arrowFunctionExpression([], t.blockStatement(nodes));
this.replaceWith(t.callExpression(container, []));
this.traverse(hoistVariablesVisitor);
// add implicit returns to all ending expression statements
const completionRecords: Array<NodePath> = this.get(
"callee",
).getCompletionRecords();
for (const path of completionRecords) {
if (!path.isExpressionStatement()) continue;
const loop = path.findParent(path => path.isLoop());
if (loop) {
let uid = loop.getData("expressionReplacementReturnUid");
if (!uid) {
const callee = this.get("callee");
uid = callee.scope.generateDeclaredUidIdentifier("ret");
callee.get("body").pushContainer("body", t.returnStatement(uid));
loop.setData("expressionReplacementReturnUid", uid);
} else {
uid = t.identifier(uid.name);
}
path
.get("expression")
.replaceWith(t.assignmentExpression("=", uid, path.node.expression));
} else {
path.replaceWith(t.returnStatement(path.node.expression));
}
}
this.get("callee").arrowFunctionToExpression();
return this.node;
return this.replaceWith(toSequenceExpression)[0].get("expressions");
}
const container = t.arrowFunctionExpression([], t.blockStatement(nodes));
this.replaceWith(t.callExpression(container, []));
this.traverse(hoistVariablesVisitor);
// add implicit returns to all ending expression statements
const completionRecords: Array<NodePath> = this.get(
"callee",
).getCompletionRecords();
for (const path of completionRecords) {
if (!path.isExpressionStatement()) continue;
const loop = path.findParent(path => path.isLoop());
if (loop) {
let uid = loop.getData("expressionReplacementReturnUid");
if (!uid) {
const callee = this.get("callee");
uid = callee.scope.generateDeclaredUidIdentifier("ret");
callee.get("body").pushContainer("body", t.returnStatement(uid));
loop.setData("expressionReplacementReturnUid", uid);
} else {
uid = t.identifier(uid.name);
}
path
.get("expression")
.replaceWith(t.assignmentExpression("=", uid, path.node.expression));
} else {
path.replaceWith(t.returnStatement(path.node.expression));
}
}
const callee = this.get("callee");
callee.arrowFunctionToExpression();
return callee.get("body.body");
}
export function replaceInline(nodes: Object | Array<Object>) {
@ -253,8 +259,9 @@ export function replaceInline(nodes: Object | Array<Object>) {
if (Array.isArray(nodes)) {
if (Array.isArray(this.container)) {
nodes = this._verifyNodeList(nodes);
this._containerInsertAfter(nodes);
return this.remove();
const paths = this._containerInsertAfter(nodes);
this.remove();
return paths;
} else {
return this.replaceWithMultiple(nodes);
}

View File

@ -9,7 +9,7 @@ function getPath(code) {
let path;
traverse(ast, {
Program: function(_path) {
path = _path;
path = _path.get("body.0");
_path.stop();
},
});
@ -18,22 +18,21 @@ function getPath(code) {
}
function generateCode(path) {
return generate(path.node).code;
return generate(path.parentPath.node).code;
}
describe("modification", function() {
describe("pushContainer", function() {
it("pushes identifier into params", function() {
const rootPath = getPath("function test(a) {}");
const path = rootPath.get("body.0");
path.pushContainer("params", t.identifier("b"));
rootPath.pushContainer("params", t.identifier("b"));
assert.equal(generateCode(rootPath), "function test(a, b) {}");
});
it("pushes identifier into block", function() {
const rootPath = getPath("function test(a) {}");
const path = rootPath.get("body.0.body");
const path = rootPath.get("body");
path.pushContainer("body", t.expressionStatement(t.identifier("b")));
assert.equal(generateCode(rootPath), "function test(a) {\n b;\n}");
@ -42,18 +41,121 @@ describe("modification", function() {
describe("unshiftContainer", function() {
it("unshifts identifier into params", function() {
const rootPath = getPath("function test(a) {}");
const path = rootPath.get("body.0");
path.unshiftContainer("params", t.identifier("b"));
rootPath.unshiftContainer("params", t.identifier("b"));
assert.equal(generateCode(rootPath), "function test(b, a) {}");
});
it("unshifts identifier into block", function() {
const rootPath = getPath("function test(a) {}");
const path = rootPath.get("body.0.body");
const path = rootPath.get("body");
path.unshiftContainer("body", t.expressionStatement(t.identifier("b")));
assert.equal(generateCode(rootPath), "function test(a) {\n b;\n}");
});
});
describe("insertBefore", function() {
it("returns inserted path with BlockStatement", function() {
const rootPath = getPath("if (x) { y; }");
const path = rootPath.get("consequent.body.0");
const result = path.insertBefore(t.identifier("b"));
assert.equal(Array.isArray(result), true);
assert.equal(result.length, 1);
assert.deepEqual(result[0].node, t.identifier("b"));
assert.equal(generateCode(rootPath), "if (x) {\n b\n y;\n}");
});
it("returns inserted path without BlockStatement", function() {
const rootPath = getPath("if (x) y;");
const path = rootPath.get("consequent");
const result = path.insertBefore(t.identifier("b"));
assert.equal(Array.isArray(result), true);
assert.equal(result.length, 1);
assert.deepEqual(result[0].node, t.identifier("b"));
assert.equal(generateCode(rootPath), "if (x) {\n b\n y;\n}");
});
it("returns inserted path without BlockStatement without ExpressionStatement", function() {
const rootPath = getPath("if (x) for (var i = 0; i < 0; i++) {}");
const path = rootPath.get("consequent");
const result = path.insertBefore(t.identifier("b"));
assert.equal(Array.isArray(result), true);
assert.equal(result.length, 1);
assert.deepEqual(result[result.length - 1].node, t.identifier("b"));
assert.equal(
generateCode(rootPath),
"if (x) {\n b\n\n for (var i = 0; i < 0; i++) {}\n}",
);
});
it("returns inserted path with BlockStatement without ExpressionStatement", function() {
const rootPath = getPath("if (x) { for (var i = 0; i < 0; i++) {} }");
const path = rootPath.get("consequent.body.0");
const result = path.insertBefore(t.identifier("b"));
assert.equal(Array.isArray(result), true);
assert.equal(result.length, 1);
assert.deepEqual(result[result.length - 1].node, t.identifier("b"));
assert.equal(
generateCode(rootPath),
"if (x) {\n b\n\n for (var i = 0; i < 0; i++) {}\n}",
);
});
});
describe("insertAfter", function() {
it("returns inserted path with BlockStatement with ExpressionStatement", function() {
const rootPath = getPath("if (x) { y; }");
const path = rootPath.get("consequent.body.0");
const result = path.insertAfter(t.identifier("b"));
assert.equal(Array.isArray(result), true);
assert.equal(result.length, 1);
assert.deepEqual(result[result.length - 1].node, t.identifier("b"));
assert.equal(generateCode(rootPath), "if (x) {\n y;\n b\n}");
});
it("returns inserted path without BlockStatement with ExpressionStatement", function() {
const rootPath = getPath("if (x) y;");
const path = rootPath.get("consequent");
const result = path.insertAfter(t.identifier("b"));
assert.equal(Array.isArray(result), true);
assert.equal(result.length, 1);
assert.deepEqual(result[result.length - 1].node, t.identifier("b"));
assert.equal(generateCode(rootPath), "if (x) {\n y;\n b\n}");
});
it("returns inserted path without BlockStatement without ExpressionStatement", function() {
const rootPath = getPath("if (x) for (var i = 0; i < 0; i++) {}");
const path = rootPath.get("consequent");
const result = path.insertAfter(t.identifier("b"));
assert.equal(Array.isArray(result), true);
assert.equal(result.length, 1);
assert.deepEqual(result[result.length - 1].node, t.identifier("b"));
assert.equal(
generateCode(rootPath),
"if (x) {\n for (var i = 0; i < 0; i++) {}\n\n b\n}",
);
});
it("returns inserted path with BlockStatement without ExpressionStatement", function() {
const rootPath = getPath("if (x) { for (var i = 0; i < 0; i++) {} }");
const path = rootPath.get("consequent.body.0");
const result = path.insertAfter(t.identifier("b"));
assert.equal(Array.isArray(result), true);
assert.equal(result.length, 1);
assert.deepEqual(result[result.length - 1].node, t.identifier("b"));
assert.equal(
generateCode(rootPath),
"if (x) {\n for (var i = 0; i < 0; i++) {}\n\n b\n}",
);
});
});
});