Skip to content

Commit 6788026

Browse files
committed
[New] no-rename-default: Forbid importing a default export by a different name
1 parent e1bd0ba commit 6788026

33 files changed

+1394
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
88

99
### Added
1010
- [`dynamic-import-chunkname`]: add `allowEmpty` option to allow empty leading comments ([#2942], thanks [@JiangWeixian])
11+
- [`no-rename-default`]: Forbid importing a default export by a different name ([#3006], thanks [@whitneyit])
1112

1213
### Changed
1314
- [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit ([#2944], thanks [@mulztob])

README.md

Lines changed: 1 addition & 0 deletions
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

config/warnings.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module.exports = {
77
rules: {
88
'import/no-named-as-default': 1,
99
'import/no-named-as-default-member': 1,
10+
'import/no-rename-default': 1,
1011
'import/no-duplicates': 1,
1112
},
1213
};

docs/rules/no-rename-default.md

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

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const rules = {
2020
'no-named-as-default': require('./rules/no-named-as-default'),
2121
'no-named-as-default-member': require('./rules/no-named-as-default-member'),
2222
'no-anonymous-default-export': require('./rules/no-anonymous-default-export'),
23+
'no-rename-default': require('./rules/no-rename-default'),
2324
'no-unused-modules': require('./rules/no-unused-modules'),
2425

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

src/rules/no-rename-default.js

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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('@typescript-eslint/utils').TSESLint.RuleModule} */
15+
const rule = {
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 findDefaultDestructure(properties) {
49+
const found = properties.find((property) => {
50+
if (property.key.name === 'default') {
51+
return property;
52+
}
53+
});
54+
return found;
55+
}
56+
57+
function getDefaultExportName(targetNode) {
58+
switch (targetNode.type) {
59+
case 'AssignmentExpression': {
60+
if (!preventRenamingBindings) {
61+
// Allow assignments to be renamed when the `preventRenamingBindings`
62+
// option is set to `false`.
63+
//
64+
// export default Foo = 1;
65+
return;
66+
}
67+
return targetNode.left.name;
68+
}
69+
case 'CallExpression': {
70+
const [argumentNode] = targetNode.arguments;
71+
return getDefaultExportName(argumentNode);
72+
}
73+
case 'ClassDeclaration': {
74+
if (targetNode.id && typeof targetNode.id.name === 'string') {
75+
return targetNode.id.name;
76+
}
77+
// Here we have an anonymous class. We can skip here.
78+
return;
79+
}
80+
case 'FunctionDeclaration': {
81+
return targetNode.id.name;
82+
}
83+
case 'Identifier': {
84+
if (!preventRenamingBindings) {
85+
// Allow identifier to be renamed when the `preventRenamingBindings`
86+
// option is set to `false`.
87+
//
88+
// const foo = 'foo';
89+
// export default foo;
90+
return;
91+
}
92+
return targetNode.name;
93+
}
94+
default:
95+
// This type of node is not handled.
96+
// Returning `undefined` here signifies this and causes the check to
97+
// exit early.
98+
}
99+
}
100+
101+
function getDefaultExportNode(exportMap) {
102+
const defaultExportNode = exportMap.exports.get('default');
103+
if (defaultExportNode == null) {
104+
return;
105+
}
106+
return defaultExportNode;
107+
}
108+
109+
function getExportMap(source, context) {
110+
const exportMap = ExportMapBuilder.get(source.value, context);
111+
if (exportMap == null) {
112+
return;
113+
}
114+
if (exportMap.errors.length > 0) {
115+
exportMap.reportErrors(context, source.value);
116+
return;
117+
}
118+
return exportMap;
119+
}
120+
121+
function handleImport(node) {
122+
const exportMap = getExportMap(node.parent.source, context);
123+
if (exportMap == null) {
124+
return;
125+
}
126+
127+
const defaultExportNode = getDefaultExportNode(exportMap);
128+
if (defaultExportNode == null) {
129+
return;
130+
}
131+
132+
const defaultExportName = getDefaultExportName(defaultExportNode.declaration);
133+
if (defaultExportName === undefined) {
134+
return;
135+
}
136+
137+
const importTarget = node.parent.source.value;
138+
const importBasename = path.basename(exportMap.path);
139+
140+
if (node.type === 'ImportDefaultSpecifier') {
141+
const importName = node.local.name;
142+
143+
if (importName === defaultExportName) {
144+
return;
145+
}
146+
147+
context.report({
148+
node,
149+
message: `Caution: \`${importBasename}\` has a default export \`${defaultExportName}\`. This imports \`${defaultExportName}\` as \`${importName}\`. Check if you meant to write \`import ${defaultExportName} from '${importTarget}'\` instead.`,
150+
});
151+
152+
return;
153+
}
154+
155+
if (node.type !== 'ImportSpecifier') {
156+
return;
157+
}
158+
159+
if (node.imported.name !== 'default') {
160+
return;
161+
}
162+
163+
const actualImportedName = node.local.name;
164+
165+
if (actualImportedName === defaultExportName) {
166+
return;
167+
}
168+
169+
context.report({
170+
node,
171+
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.`,
172+
});
173+
}
174+
175+
function handleRequire(node) {
176+
if (
177+
!commonjs
178+
|| node.type !== 'VariableDeclarator'
179+
|| !node.id || !(node.id.type === 'Identifier' || node.id.type === 'ObjectPattern')
180+
|| !node.init || node.init.type !== 'CallExpression'
181+
) {
182+
return;
183+
}
184+
185+
let defaultDestructure;
186+
if (node.id.type === 'ObjectPattern') {
187+
defaultDestructure = findDefaultDestructure(node.id.properties);
188+
if (defaultDestructure === undefined) {
189+
return;
190+
}
191+
}
192+
193+
const call = node.init;
194+
const [source] = call.arguments;
195+
196+
if (
197+
call.callee.type !== 'Identifier' || call.callee.name !== 'require' || call.arguments.length !== 1
198+
|| source.type !== 'Literal'
199+
) {
200+
return;
201+
}
202+
203+
const exportMap = getExportMap(source, context);
204+
if (exportMap == null) {
205+
return;
206+
}
207+
208+
const defaultExportNode = getDefaultExportNode(exportMap);
209+
if (defaultExportNode == null) {
210+
return;
211+
}
212+
213+
const defaultExportName = getDefaultExportName(defaultExportNode.declaration);
214+
const requireTarget = source.value;
215+
const requireBasename = path.basename(exportMap.path);
216+
const requireName = node.id.type === 'Identifier' ? node.id.name : defaultDestructure.value.name;
217+
218+
if (defaultExportName === undefined) {
219+
return;
220+
}
221+
222+
if (requireName === defaultExportName) {
223+
return;
224+
}
225+
226+
if (node.id.type === 'Identifier') {
227+
context.report({
228+
node,
229+
message: `Caution: \`${requireBasename}\` has a default export \`${defaultExportName}\`. This requires \`${defaultExportName}\` as \`${requireName}\`. Check if you meant to write \`const ${defaultExportName} = require('${requireTarget}')\` instead.`,
230+
});
231+
return;
232+
}
233+
234+
context.report({
235+
node,
236+
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.`,
237+
});
238+
}
239+
240+
return {
241+
ImportDefaultSpecifier(node) {
242+
handleImport(node);
243+
},
244+
ImportSpecifier(node) {
245+
handleImport(node);
246+
},
247+
VariableDeclarator(node) {
248+
handleRequire(node);
249+
},
250+
};
251+
},
252+
};
253+
254+
module.exports = rule;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default async () => {};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default () => {};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default class {};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default {};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 123;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default arrowAsync = async () => {};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default arrow = () => {};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default User = class MyUser {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default User = class {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default fn = function myFn() {};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default fn = function () {};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default generator = function* myGenerator() {};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default generator = function* () {};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default class User {};
Lines changed: 6 additions & 0 deletions
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;
Lines changed: 6 additions & 0 deletions
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;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default function getUsersSync() {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default async function getUsers() {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default function* reader() {}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const foo = function bar() {};
2+
3+
export default foo;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
function bar() {}
2+
3+
export default bar;
Lines changed: 4 additions & 0 deletions
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);
Lines changed: 4 additions & 0 deletions
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);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import getUsers from '../default-fn-get-users';
2+
import withAuth from './hoc-with-auth';
3+
import withLogger from './hoc-with-logger';
4+
5+
export default withLogger(withAuth(getUsers));
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default function withAuth(fn) {
2+
return function innerAuth(...args) {
3+
const auth = {};
4+
return fn.call(null, auth, ...args);
5+
}
6+
}

0 commit comments

Comments
 (0)