Skip to content

Commit 4dd8492

Browse files
whitneyitljharb
authored andcommitted
[New] no-rename-default: Forbid importing a default export by a different name
1 parent d0231c0 commit 4dd8492

33 files changed

+1440
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1111
- [`dynamic-import-chunkname`]: Allow empty chunk name when webpackMode: 'eager' is set; add suggestions to remove name in eager mode ([#3004], thanks [@amsardesai])
1212
- [`no-unused-modules`]: Add `ignoreUnusedTypeExports` option ([#3011], thanks [@silverwind])
1313
- add support for Flat Config ([#3018], thanks [@michaelfaith])
14+
- [`no-rename-default`]: Forbid importing a default export by a different name ([#3006], thanks [@whitneyit])
1415

1516
### Fixed
1617
- [`no-extraneous-dependencies`]: allow wrong path ([#3012], thanks [@chabb])

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
3737
| [no-mutable-exports](docs/rules/no-mutable-exports.md) | Forbid the use of mutable exports with `var` or `let`. | | | | | | |
3838
| [no-named-as-default](docs/rules/no-named-as-default.md) | Forbid use of exported name as identifier of default export. | | ☑️ 🚸 | | | | |
3939
| [no-named-as-default-member](docs/rules/no-named-as-default-member.md) | Forbid use of exported name as property of default export. | | ☑️ 🚸 | | | | |
40+
| [no-rename-default](docs/rules/no-rename-default.md) | Forbid importing a default export by a different name. | | | | | | |
4041
| [no-unused-modules](docs/rules/no-unused-modules.md) | Forbid modules without exports, or exports without matching import in another module. | | | | | | |
4142

4243
### Module systems

docs/rules/no-rename-default.md

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# import/no-rename-default
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Prohibit importing a default export by another name.
6+
7+
## Rule Details
8+
9+
Given:
10+
11+
```js
12+
// api/get-users.js
13+
export default async function getUsers() {}
14+
```
15+
16+
...this would be valid:
17+
18+
```js
19+
import getUsers from './api/get-users.js';
20+
```
21+
22+
...and the following would be reported:
23+
24+
```js
25+
// Caution: `get-users.js` has a default export `getUsers`.
26+
// This imports `getUsers` as `findUsers`.
27+
// Check if you meant to write `import getUsers from './api/get-users'` instead.
28+
import findUsers from './get-users';
29+
```

src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const rules = {
2222
'no-named-as-default': require('./rules/no-named-as-default'),
2323
'no-named-as-default-member': require('./rules/no-named-as-default-member'),
2424
'no-anonymous-default-export': require('./rules/no-anonymous-default-export'),
25+
'no-rename-default': require('./rules/no-rename-default'),
2526
'no-unused-modules': require('./rules/no-unused-modules'),
2627

2728
'no-commonjs': require('./rules/no-commonjs'),

src/rules/no-rename-default.js

+278
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
/**
2+
* @fileOverview Rule to warn about importing a default export by different name
3+
* @author James Whitney
4+
*/
5+
6+
import docsUrl from '../docsUrl';
7+
import ExportMapBuilder from '../exportMap/builder';
8+
import path from 'path';
9+
10+
//------------------------------------------------------------------------------
11+
// Rule Definition
12+
//------------------------------------------------------------------------------
13+
14+
/** @type {import('eslint').Rule.RuleModule} */
15+
module.exports = {
16+
meta: {
17+
type: 'suggestion',
18+
docs: {
19+
category: 'Helpful warnings',
20+
description: 'Forbid importing a default export by a different name.',
21+
recommended: false,
22+
url: docsUrl('no-named-as-default'),
23+
},
24+
schema: [
25+
{
26+
type: 'object',
27+
properties: {
28+
commonjs: {
29+
default: false,
30+
type: 'boolean',
31+
},
32+
preventRenamingBindings: {
33+
default: true,
34+
type: 'boolean',
35+
},
36+
},
37+
additionalProperties: false,
38+
},
39+
],
40+
},
41+
42+
create(context) {
43+
const {
44+
commonjs = false,
45+
preventRenamingBindings = true,
46+
} = context.options[0] || {};
47+
48+
function getDefaultExportName(targetNode) {
49+
if (targetNode == null) {
50+
return;
51+
}
52+
switch (targetNode.type) {
53+
case 'AssignmentExpression': {
54+
if (!preventRenamingBindings) {
55+
// Allow assignments to be renamed when the `preventRenamingBindings`
56+
// option is set to `false`.
57+
//
58+
// export default Foo = 1;
59+
return;
60+
}
61+
return targetNode.left.name;
62+
}
63+
case 'CallExpression': {
64+
const [argumentNode] = targetNode.arguments;
65+
return getDefaultExportName(argumentNode);
66+
}
67+
case 'ClassDeclaration': {
68+
if (targetNode.id && typeof targetNode.id.name === 'string') {
69+
return targetNode.id.name;
70+
}
71+
// Here we have an anonymous class. We can skip here.
72+
return;
73+
}
74+
case 'ExportSpecifier': {
75+
return targetNode.local.name;
76+
}
77+
case 'FunctionDeclaration': {
78+
return targetNode.id.name;
79+
}
80+
case 'Identifier': {
81+
if (!preventRenamingBindings) {
82+
// Allow identifier to be renamed when the `preventRenamingBindings`
83+
// option is set to `false`.
84+
//
85+
// const foo = 'foo';
86+
// export default foo;
87+
return;
88+
}
89+
return targetNode.name;
90+
}
91+
default:
92+
// This type of node is not handled.
93+
// Returning `undefined` here signifies this and causes the check to
94+
// exit early.
95+
}
96+
}
97+
98+
function getDefaultExportNode(exportMap) {
99+
const defaultExportNode = exportMap.exports.get('default');
100+
if (defaultExportNode == null) {
101+
return;
102+
}
103+
104+
if (defaultExportNode.type === 'ExportDefaultDeclaration') {
105+
return defaultExportNode.declaration;
106+
}
107+
108+
if (defaultExportNode.type === 'ExportNamedDeclaration') {
109+
return defaultExportNode.specifiers.find((specifier) => specifier.exported.name === 'default');
110+
}
111+
}
112+
113+
function getExportMap(source, context) {
114+
const exportMap = ExportMapBuilder.get(source.value, context);
115+
if (exportMap == null) {
116+
return;
117+
}
118+
if (exportMap.errors.length > 0) {
119+
exportMap.reportErrors(context, source.value);
120+
return;
121+
}
122+
return exportMap;
123+
}
124+
125+
function handleImport(node) {
126+
const exportMap = getExportMap(node.parent.source, context);
127+
if (exportMap == null) {
128+
return;
129+
}
130+
131+
const defaultExportNode = getDefaultExportNode(exportMap);
132+
if (defaultExportNode == null) {
133+
return;
134+
}
135+
136+
const defaultExportName = getDefaultExportName(defaultExportNode);
137+
if (defaultExportName === undefined) {
138+
return;
139+
}
140+
141+
const importTarget = node.parent.source.value;
142+
const importBasename = path.basename(exportMap.path);
143+
144+
if (node.type === 'ImportDefaultSpecifier') {
145+
const importName = node.local.name;
146+
147+
if (importName === defaultExportName) {
148+
return;
149+
}
150+
151+
context.report({
152+
node,
153+
message: `Caution: \`{{importBasename}}\` has a default export \`{{defaultExportName}}\`. This imports \`{{defaultExportName}}\` as \`{{importName}}\`. Check if you meant to write \`import {{defaultExportName} from '{{importTarget}}'\` instead.`,
154+
data: {
155+
defaultExportName,
156+
importBasename,
157+
importName,
158+
importTarget,
159+
},
160+
});
161+
162+
return;
163+
}
164+
165+
if (node.type !== 'ImportSpecifier' || node.imported.name !== 'default') {
166+
return;
167+
}
168+
169+
const actualImportedName = node.local.name;
170+
171+
if (actualImportedName === defaultExportName) {
172+
return;
173+
}
174+
175+
context.report({
176+
node,
177+
message: `Caution: \`{{importBasename}}\` has a default export \`{{defaultExportName}}\`. This imports \`{{defaultExportName}}\` as \`{{actualImportedName}}\`. Check if you meant to write \`import { default as {{defaultExportName}} } from '{{importTarget}}'\` instead.`,
178+
data: {
179+
actualImportedName,
180+
defaultExportName,
181+
importBasename,
182+
importTarget,
183+
},
184+
});
185+
}
186+
187+
function handleRequire(node) {
188+
if (
189+
!commonjs
190+
|| node.type !== 'VariableDeclarator'
191+
|| !node.id
192+
|| !(node.id.type === 'Identifier'
193+
|| node.id.type === 'ObjectPattern')
194+
|| !node.init
195+
|| node.init.type !== 'CallExpression'
196+
) {
197+
return;
198+
}
199+
200+
let defaultDestructure;
201+
if (node.id.type === 'ObjectPattern') {
202+
defaultDestructure = node.id.properties.find((property) => property.key.name === 'default');
203+
if (defaultDestructure === undefined) {
204+
return;
205+
}
206+
}
207+
208+
const call = node.init;
209+
const [source] = call.arguments;
210+
211+
if (
212+
call.callee.type !== 'Identifier'
213+
|| call.callee.name !== 'require'
214+
|| call.arguments.length !== 1
215+
|| source.type !== 'Literal'
216+
) {
217+
return;
218+
}
219+
220+
const exportMap = getExportMap(source, context);
221+
if (exportMap == null) {
222+
return;
223+
}
224+
225+
const defaultExportNode = getDefaultExportNode(exportMap);
226+
if (defaultExportNode == null) {
227+
return;
228+
}
229+
230+
const defaultExportName = getDefaultExportName(defaultExportNode);
231+
const requireTarget = source.value;
232+
const requireBasename = path.basename(exportMap.path);
233+
const requireName = node.id.type === 'Identifier' ? node.id.name : defaultDestructure.value.name;
234+
235+
if (defaultExportName === undefined) {
236+
return;
237+
}
238+
239+
if (requireName === defaultExportName) {
240+
return;
241+
}
242+
243+
const data = {
244+
defaultExportName,
245+
requireBasename,
246+
requireName,
247+
requireTarget,
248+
};
249+
250+
if (node.id.type === 'Identifier') {
251+
context.report({
252+
node,
253+
message: `Caution: \`{{requireBasename}}\` has a default export \`{{defaultExportName}}\`. This requires \`{{defaultExportName}}\` as \`{{requireName}}\`. Check if you meant to write \`const {{defaultExportName}} = require('{{requireTarget}}')\` instead.`,
254+
data,
255+
});
256+
return;
257+
}
258+
259+
context.report({
260+
node,
261+
message: `Caution: \`{{requireBasename}}\` has a default export \`{{defaultExportName}\`. This requires \`{{defaultExportName}\` as \`{{requireName}}\`. Check if you meant to write \`const { default: {{defaultExportName}} } = require('{{requireTarget}}')\` instead.`,
262+
data,
263+
});
264+
}
265+
266+
return {
267+
ImportDefaultSpecifier(node) {
268+
handleImport(node);
269+
},
270+
ImportSpecifier(node) {
271+
handleImport(node);
272+
},
273+
VariableDeclarator(node) {
274+
handleRequire(node);
275+
},
276+
};
277+
},
278+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default async () => {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default () => {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default class {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 123;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default arrowAsync = async () => {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default arrow = () => {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default User = class MyUser {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default User = class {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default fn = function myFn() {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default fn = function () {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default generator = function* myGenerator() {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default generator = function* () {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default class User {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const barNamed1 = 'bar-named-1';
2+
export const barNamed2 = 'bar-named-2';
3+
4+
const bar = 'bar';
5+
6+
export default bar;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const fooNamed1 = 'foo-named-1';
2+
export const fooNamed2 = 'foo-named-2';
3+
4+
const foo = 'foo';
5+
6+
export default foo;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default function getUsersSync() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default async function getUsers() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default function* reader() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const foo = function bar() {};
2+
3+
export default foo;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
function bar() {}
2+
3+
export default bar;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import foo from '../default-const-foo';
2+
import withLogger from './hoc-with-logger';
3+
4+
export default withLogger(foo);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import getUsers from '../default-fn-get-users';
2+
import withLogger from './hoc-with-logger';
3+
4+
export default withLogger(getUsers);

0 commit comments

Comments
 (0)