Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cloneDeepWithoutLoc #10680

Merged
merged 12 commits into from Mar 16, 2020
3 changes: 2 additions & 1 deletion packages/babel-types/scripts/generators/flow.js
Expand Up @@ -135,7 +135,8 @@ lines.push(
// clone/
`declare function clone<T>(n: T): T;`,
`declare function cloneDeep<T>(n: T): T;`,
`declare function cloneNode<T>(n: T, deep?: boolean): T;`,
`declare function cloneDeepWithoutLoc<T>(n: T): T;`,
`declare function cloneNode<T>(n: T, deep?: boolean, withoutLoc?: boolean): T;`,
`declare function cloneWithoutLoc<T>(n: T): T;`,

// comments/
Expand Down
3 changes: 2 additions & 1 deletion packages/babel-types/scripts/generators/typescript.js
Expand Up @@ -154,7 +154,8 @@ lines.push(
// clone/
`export function clone<T extends Node>(n: T): T;`,
`export function cloneDeep<T extends Node>(n: T): T;`,
`export function cloneNode<T extends Node>(n: T, deep?: boolean): T;`,
`export function cloneDeepWithoutLoc<T extends Node>(n: T): T;`,
`export function cloneNode<T extends Node>(n: T, deep?: boolean, withoutLoc?: boolean): T;`,
`export function cloneWithoutLoc<T extends Node>(n: T): T;`,

// comments/
Expand Down
10 changes: 10 additions & 0 deletions packages/babel-types/src/clone/cloneDeepWithoutLoc.js
@@ -0,0 +1,10 @@
// @flow
import cloneNode from "./cloneNode";
/**
* Create a deep clone of a `node` and all of it's child nodes
* including only properties belonging to the node.
* excluding `_private` and location properties.
*/
export default function cloneDeepWithoutLoc<T: Object>(node: T): T {
return cloneNode(node, /* deep */ true, /* withoutLoc */ true);
}
72 changes: 50 additions & 22 deletions packages/babel-types/src/clone/cloneNode.js
Expand Up @@ -2,33 +2,32 @@ import { NODE_FIELDS } from "../definitions";

const has = Function.call.bind(Object.prototype.hasOwnProperty);

function cloneIfNode(obj, deep) {
if (
obj &&
typeof obj.type === "string" &&
// CommentLine and CommentBlock are used in File#comments, but they are
// not defined in babel-types
obj.type !== "CommentLine" &&
obj.type !== "CommentBlock"
) {
return cloneNode(obj, deep);
// This function will never be called for comments, only for real nodes.
function cloneIfNode(obj, deep, withoutLoc) {
if (obj && typeof obj.type === "string") {
return cloneNode(obj, deep, withoutLoc);
}

return obj;
}

function cloneIfNodeOrArray(obj, deep) {
function cloneIfNodeOrArray(obj, deep, withoutLoc) {
if (Array.isArray(obj)) {
return obj.map(node => cloneIfNode(node, deep));
return obj.map(node => cloneIfNode(node, deep, withoutLoc));
}
return cloneIfNode(obj, deep);
return cloneIfNode(obj, deep, withoutLoc);
}

/**
* Create a clone of a `node` including only properties belonging to the node.
* If the second parameter is `false`, cloneNode performs a shallow clone.
jridgewell marked this conversation as resolved.
Show resolved Hide resolved
* If the third parameter is true, the cloned nodes exclude location properties.
*/
export default function cloneNode<T: Object>(node: T, deep: boolean = true): T {
export default function cloneNode<T: Object>(
node: T,
deep: boolean = true,
withoutLoc: boolean = false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep clones also need to receive the withoutLoc param, which is why the tests are failing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I will fix it!

Copy link
Contributor Author

@Taym95 Taym95 Nov 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @jridgewell, I added withoutLoc to deep clones which means cloned.declarations[0].id.loc needs to be null but I am still receiving {} in the test, any idea why?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like is working.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, sorry 😅!

): T {
if (!node) return node;

const { type } = node;
Expand All @@ -44,32 +43,53 @@ export default function cloneNode<T: Object>(node: T, deep: boolean = true): T {

if (has(node, "typeAnnotation")) {
newNode.typeAnnotation = deep
? cloneIfNodeOrArray(node.typeAnnotation, true)
? cloneIfNodeOrArray(node.typeAnnotation, true, withoutLoc)
: node.typeAnnotation;
}
} else if (!has(NODE_FIELDS, type)) {
throw new Error(`Unknown node type: "${type}"`);
} else {
for (const field of Object.keys(NODE_FIELDS[type])) {
if (has(node, field)) {
newNode[field] = deep
? cloneIfNodeOrArray(node[field], true)
: node[field];
if (deep) {
newNode[field] =
type === "File" && field === "comments"
? maybeCloneComments(node.comments, deep, withoutLoc)
: cloneIfNodeOrArray(node[field], true, withoutLoc);
} else {
newNode[field] = node[field];
}
}
}
}

if (has(node, "loc")) {
newNode.loc = node.loc;
if (withoutLoc) {
newNode.loc = null;
} else {
newNode.loc = node.loc;
}
}
if (has(node, "leadingComments")) {
newNode.leadingComments = node.leadingComments;
newNode.leadingComments = maybeCloneComments(
node.leadingComments,
deep,
withoutLoc,
);
}
if (has(node, "innerComments")) {
newNode.innerComments = node.innerComments;
newNode.innerComments = maybeCloneComments(
node.innerComments,
deep,
withoutLoc,
);
}
if (has(node, "trailingComments")) {
newNode.trailingComments = node.trailingComments;
newNode.trailingComments = maybeCloneComments(
node.trailingComments,
deep,
withoutLoc,
);
}
if (has(node, "extra")) {
newNode.extra = {
Expand All @@ -79,3 +99,11 @@ export default function cloneNode<T: Object>(node: T, deep: boolean = true): T {

return newNode;
}

function cloneCommentsWithoutLoc<T: Object>(comments: T[]): T {
return comments.map(({ type, value }) => ({ type, value, loc: null }));
}

function maybeCloneComments(comments, deep, withoutLoc) {
return deep && withoutLoc ? cloneCommentsWithoutLoc(comments) : comments;
}
8 changes: 2 additions & 6 deletions packages/babel-types/src/clone/cloneWithoutLoc.js
@@ -1,12 +1,8 @@
// @flow
import clone from "./clone";

import cloneNode from "./cloneNode";
/**
* Create a shallow clone of a `node` excluding `_private` and location properties.
*/
export default function cloneWithoutLoc<T: Object>(node: T): T {
const newNode = clone(node);
newNode.loc = null;
jridgewell marked this conversation as resolved.
Show resolved Hide resolved

return newNode;
return cloneNode(node, /* deep */ false, /* withoutLoc */ true);
}
1 change: 1 addition & 0 deletions packages/babel-types/src/index.js
Expand Up @@ -16,6 +16,7 @@ export * from "./builders/generated";
export { default as cloneNode } from "./clone/cloneNode";
export { default as clone } from "./clone/clone";
export { default as cloneDeep } from "./clone/cloneDeep";
export { default as cloneDeepWithoutLoc } from "./clone/cloneDeepWithoutLoc";
export { default as cloneWithoutLoc } from "./clone/cloneWithoutLoc";

// comments
Expand Down
68 changes: 68 additions & 0 deletions packages/babel-types/test/cloning.js
Expand Up @@ -72,4 +72,72 @@ describe("cloneNode", function() {
node.declarations[0].id.typeAnnotation,
);
});

it("should support shallow cloning without loc", function() {
const node = t.variableDeclaration("let", [
t.variableDeclarator({
...t.identifier("value"),
typeAnnotation: t.anyTypeAnnotation(),
}),
]);
node.loc = {};
const cloned = t.cloneNode(node, /* deep */ false, /* withoutLoc */ true);
expect(cloned.loc).toBeNull();
});

it("should support deep cloning without loc", function() {
jridgewell marked this conversation as resolved.
Show resolved Hide resolved
const node = t.variableDeclaration("let", [
t.variableDeclarator({
...t.identifier("value"),
typeAnnotation: t.anyTypeAnnotation(),
}),
]);
jridgewell marked this conversation as resolved.
Show resolved Hide resolved
node.loc = {};
jridgewell marked this conversation as resolved.
Show resolved Hide resolved
node.declarations[0].id.loc = {};
const cloned = t.cloneNode(node, /* deep */ true, /* withoutLoc */ true);
expect(cloned.loc).toBeNull();
jridgewell marked this conversation as resolved.
Show resolved Hide resolved
expect(cloned.declarations[0].id.loc).toBeNull();
});

it("should support deep cloning for comments", function() {
const node = t.variableDeclaration("let", [
t.variableDeclarator({
...t.identifier("value"),
leadingComments: [{ loc: {} }],
innerComments: [{ loc: {} }],
trailingComments: [{ loc: {} }],
}),
]);
node.loc = {};
node.declarations[0].id.loc = {};

const cloned = t.cloneNode(node, /* deep */ true, /* withoutLoc */ false);
expect(cloned.declarations[0].id.leadingComments[0].loc).toBe(
node.declarations[0].id.leadingComments[0].loc,
);
expect(cloned.declarations[0].id.innerComments[0].loc).toBe(
node.declarations[0].id.innerComments[0].loc,
);
expect(cloned.declarations[0].id.trailingComments[0].loc).toBe(
node.declarations[0].id.trailingComments[0].loc,
);
});

it("should support deep cloning for comments without loc", function() {
const node = t.variableDeclaration("let", [
t.variableDeclarator({
...t.identifier("value"),
leadingComments: [{ loc: {} }],
innerComments: [{ loc: {} }],
trailingComments: [{ loc: {} }],
}),
]);
node.loc = {};
node.declarations[0].id.loc = {};

const cloned = t.cloneNode(node, /* deep */ true, /* withoutLoc */ true);
expect(cloned.declarations[0].id.leadingComments[0].loc).toBe(null);
expect(cloned.declarations[0].id.innerComments[0].loc).toBe(null);
expect(cloned.declarations[0].id.trailingComments[0].loc).toBe(null);
});
});