Skip to content

Commit 5cd8b5b

Browse files
nicolo-ribaudologanfsmyth
authored andcommitted
Add eslint plugin to disallow t.clone and t.cloneDeep (babel#7191)
* Add eslint plugin to disallow `t.clone` and `t.cloneDeep` * Make it better and add flow * Other cases * Superpowers * Fix
1 parent a328b6a commit 5cd8b5b

File tree

2 files changed

+267
-1
lines changed

2 files changed

+267
-1
lines changed

.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"codemods/*/src/**/*.js"
1919
],
2020
"rules": {
21-
"no-undefined-identifier": "error"
21+
"no-undefined-identifier": "error",
22+
"no-deprecated-clone": "error"
2223
}
2324
},
2425
{
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
// @flow
2+
3+
"use strict";
4+
5+
function getVariableDefinition(name /*: string */, scope /*: Scope */) {
6+
let currentScope = scope;
7+
do {
8+
const variable = currentScope.set.get(name);
9+
if (variable && variable.defs[0]) {
10+
return { scope: currentScope, definition: variable.defs[0] };
11+
}
12+
} while ((currentScope = currentScope.upper));
13+
}
14+
15+
/*::
16+
type ReferenceOriginImport = { kind: "import", source: string, name: string };
17+
type ReferenceOriginParam = {
18+
kind: "export param",
19+
exportName: string,
20+
index: number,
21+
};
22+
23+
type ReferenceOrigin =
24+
| ReferenceOriginImport
25+
| ReferenceOriginParam
26+
| { kind: "import *", source: string }
27+
| {
28+
kind: "property",
29+
base: ReferenceOriginImport | ReferenceOriginParam,
30+
path: string,
31+
};
32+
*/
33+
34+
// Given a node and a context, returns a description of where its value comes
35+
// from.
36+
// It resolves imports, parameters of exported functions and property accesses.
37+
// See the ReferenceOrigin type for more informations.
38+
function getReferenceOrigin(
39+
node /*: Node */,
40+
scope /*: Scope */
41+
) /*: ?ReferenceOrigin */ {
42+
if (node.type === "Identifier") {
43+
const variable = getVariableDefinition(node.name, scope);
44+
if (!variable) return null;
45+
46+
const definition = variable.definition;
47+
const defNode = definition.node;
48+
49+
if (definition.type === "ImportBinding") {
50+
if (defNode.type === "ImportSpecifier") {
51+
return {
52+
kind: "import",
53+
source: definition.parent.source.value,
54+
name: defNode.imported.name,
55+
};
56+
}
57+
if (defNode.type === "ImportNamespaceSpecifier") {
58+
return {
59+
kind: "import *",
60+
source: definition.parent.source.value,
61+
};
62+
}
63+
}
64+
65+
if (definition.type === "Variable" && defNode.init) {
66+
const origin = getReferenceOrigin(defNode.init, variable.scope);
67+
return origin && patternToProperty(definition.name, origin);
68+
}
69+
70+
if (definition.type === "Parameter") {
71+
const parent = defNode.parent;
72+
let exportName /*: string */;
73+
if (parent.type === "ExportDefaultDeclaration") {
74+
exportName = "default";
75+
} else if (parent.type === "ExportNamedDeclaration") {
76+
exportName = defNode.id.name;
77+
} else if (
78+
parent.type === "AssignmentExpression" &&
79+
parent.left.type === "MemberExpression" &&
80+
parent.left.object.type === "Identifier" &&
81+
parent.left.object.name === "module" &&
82+
parent.left.property.type === "Identifier" &&
83+
parent.left.property.name === "exports"
84+
) {
85+
exportName = "module.exports";
86+
} else {
87+
return null;
88+
}
89+
return patternToProperty(definition.name, {
90+
kind: "export param",
91+
exportName,
92+
index: definition.index,
93+
});
94+
}
95+
}
96+
97+
if (node.type === "MemberExpression" && !node.computed) {
98+
const origin = getReferenceOrigin(node.object, scope);
99+
return origin && addProperty(origin, node.property.name);
100+
}
101+
102+
return null;
103+
}
104+
105+
function patternToProperty(
106+
id /*: Node */,
107+
base /*: ReferenceOrigin */
108+
) /*: ?ReferenceOrigin */ {
109+
const path = getPatternPath(id);
110+
return path && path.reduce(addProperty, base);
111+
}
112+
113+
// Adds a property to a given origin. If it was a namespace import it becomes
114+
// a named import, so that `import * as x from "foo"; x.bar` and
115+
// `import { bar } from "foo"` have the same origin.
116+
function addProperty(
117+
origin /*: ReferenceOrigin */,
118+
name /*: string */
119+
) /* ReferenceOrigin */ {
120+
if (origin.kind === "import *") {
121+
return {
122+
kind: "import",
123+
source: origin.source,
124+
name,
125+
};
126+
}
127+
if (origin.kind === "property") {
128+
return {
129+
kind: "property",
130+
base: origin.base,
131+
path: origin.path + "." + name,
132+
};
133+
}
134+
return {
135+
kind: "property",
136+
base: origin,
137+
path: name,
138+
};
139+
}
140+
141+
// if "node" is c of { a: { b: c } }, the result is ["a","b"]
142+
function getPatternPath(node /*: Node */) /*: ?string[] */ {
143+
let current = node;
144+
const path = [];
145+
146+
// Unshift keys to path while going up
147+
do {
148+
const property = current.parent;
149+
if (
150+
property.type === "ArrayPattern" ||
151+
property.type === "AssignmentPattern" ||
152+
property.computed
153+
) {
154+
// These nodes are not supported.
155+
return null;
156+
}
157+
if (property.type === "Property") {
158+
path.unshift(property.key.name);
159+
} else {
160+
// The destructuring pattern is finished
161+
break;
162+
}
163+
} while ((current = current.parent.parent));
164+
165+
return path;
166+
}
167+
168+
function reportError(context /*: Context */, node /*: Node */) {
169+
const isMemberExpression = node.type === "MemberExpression";
170+
const id = isMemberExpression ? node.property : node;
171+
context.report({
172+
node: id,
173+
message: `t.${id.name}() is deprecated. Use t.cloneNode() instead.`,
174+
fix(fixer) {
175+
if (isMemberExpression) {
176+
return fixer.replaceText(id, "cloneNode");
177+
}
178+
},
179+
});
180+
}
181+
182+
module.exports = {
183+
meta: {
184+
schema: [],
185+
fixable: "code",
186+
},
187+
create(context /*: Context */) {
188+
return {
189+
CallExpression(node /*: Node */) {
190+
const origin = getReferenceOrigin(node.callee, context.getScope());
191+
192+
if (!origin) return;
193+
194+
if (
195+
origin.kind === "import" &&
196+
(origin.name === "clone" || origin.name === "cloneDeep") &&
197+
origin.source === "@babel/types"
198+
) {
199+
// imported from @babel/types
200+
return reportError(context, node.callee);
201+
}
202+
203+
if (
204+
origin.kind === "property" &&
205+
(origin.path === "clone" || origin.path === "cloneDeep") &&
206+
origin.base.kind === "import" &&
207+
origin.base.name === "types" &&
208+
origin.base.source === "@babel/core"
209+
) {
210+
// imported from @babel/core
211+
return reportError(context, node.callee);
212+
}
213+
214+
if (
215+
origin.kind === "property" &&
216+
(origin.path === "types.clone" ||
217+
origin.path === "types.cloneDeep") &&
218+
origin.base.kind === "export param" &&
219+
(origin.base.exportName === "default" ||
220+
origin.base.exportName === "module.exports") &&
221+
origin.base.index === 0
222+
) {
223+
// export default function ({ types: t }) {}
224+
// module.exports = function ({ types: t }) {}
225+
return reportError(context, node.callee);
226+
}
227+
},
228+
};
229+
},
230+
};
231+
232+
/*:: // ESLint types
233+
234+
type Node = { type: string, [string]: any };
235+
236+
type Definition = {
237+
type: "ImportedBinding",
238+
name: Node,
239+
node: Node,
240+
parent: Node,
241+
};
242+
243+
type Variable = {
244+
defs: Definition[],
245+
};
246+
247+
type Scope = {
248+
set: Map<string, Variable>,
249+
upper: ?Scope,
250+
};
251+
252+
type Context = {
253+
report(options: {
254+
node: Node,
255+
message: string,
256+
fix?: (fixer: Fixer) => ?Fixer,
257+
}): void,
258+
259+
getScope(): Scope,
260+
};
261+
262+
type Fixer = {
263+
replaceText(node: Node, replacement: string): Fixer,
264+
};
265+
*/

0 commit comments

Comments
 (0)