Skip to content

Commit d46657f

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

File tree

11 files changed

+294
-0
lines changed

11 files changed

+294
-0
lines changed

CHANGELOG.md

+1
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

+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

config/warnings.js

+1
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

+31
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

+1
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

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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+
type: 'boolean',
30+
},
31+
},
32+
additionalProperties: false,
33+
},
34+
],
35+
},
36+
37+
create(context) {
38+
function getDefaultExportName(defaultExportNode) {
39+
return defaultExportNode.declaration.name;
40+
}
41+
42+
function getDefaultExportNode(exportMap) {
43+
const defaultExportNode = exportMap.exports.get('default');
44+
if (defaultExportNode == null) {
45+
return;
46+
}
47+
return defaultExportNode;
48+
}
49+
50+
function getExportMap(source, context) {
51+
const exportMap = ExportMapBuilder.get(source.value, context);
52+
if (exportMap == null) {
53+
return;
54+
}
55+
if (exportMap.errors.length > 0) {
56+
exportMap.reportErrors(context, source.value);
57+
return;
58+
}
59+
return exportMap;
60+
}
61+
62+
return {
63+
ImportDeclaration(node) {
64+
const exportMap = getExportMap(node.source, context);
65+
if (exportMap == null) {
66+
return;
67+
}
68+
69+
const defaultExportNode = getDefaultExportNode(exportMap);
70+
if (defaultExportNode == null) {
71+
return;
72+
}
73+
74+
const defaultExportName = getDefaultExportName(defaultExportNode);
75+
const importTarget = node.source.value;
76+
const importBasename = path.basename(exportMap.path);
77+
78+
node.specifiers.forEach((importClause) => {
79+
const importName = importClause.local.name;
80+
81+
// No named default export
82+
if (defaultExportName === undefined) {
83+
return;
84+
}
85+
86+
// The name of the import matches the name of the default export.
87+
if (importName === defaultExportName) {
88+
return;
89+
}
90+
91+
context.report({
92+
node: importClause,
93+
message: `Caution: \`${importBasename}\` has a default export \`${defaultExportName}\`. This imports \`${defaultExportName}\` as \`${importName}\`. Check if you meant to write \`import ${defaultExportName} from '${importTarget}'\` instead.`,
94+
});
95+
});
96+
},
97+
VariableDeclarator(node) {
98+
const options = context.options[0] || {};
99+
100+
if (
101+
!options.commonjs
102+
|| node.type !== 'VariableDeclarator'
103+
// return if it's not an object destructure or it's an empty object destructure
104+
|| !node.id || node.id.type !== 'Identifier'
105+
// return if there is no call expression on the right side
106+
|| !node.init || node.init.type !== 'CallExpression'
107+
) {
108+
return;
109+
}
110+
111+
const call = node.init;
112+
const [source] = call.arguments;
113+
114+
if (
115+
// return if it's not a commonjs require statement
116+
call.callee.type !== 'Identifier' || call.callee.name !== 'require' || call.arguments.length !== 1
117+
// return if it's not a string source
118+
|| source.type !== 'Literal'
119+
) {
120+
return;
121+
}
122+
123+
const exportMap = getExportMap(source, context);
124+
if (exportMap == null) {
125+
return;
126+
}
127+
128+
const defaultExportNode = getDefaultExportNode(exportMap);
129+
if (defaultExportNode == null) {
130+
return;
131+
}
132+
133+
const defaultExportName = getDefaultExportName(defaultExportNode);
134+
const requireTarget = source.value;
135+
const requireBasename = path.basename(exportMap.path);
136+
const requireName = node.id.name;
137+
138+
// No named default export
139+
if (defaultExportName === undefined) {
140+
return;
141+
}
142+
143+
// The name of the require matches the name of the default export.
144+
if (requireName === defaultExportName) {
145+
return;
146+
}
147+
148+
context.report({
149+
node,
150+
message: `Caution: \`${requireBasename}\` has a default export \`${defaultExportName}\`. This requires \`${defaultExportName}\` as \`${requireName}\`. Check if you meant to write \`const ${defaultExportName} = require('${requireTarget}')\` instead.`,
151+
});
152+
},
153+
};
154+
},
155+
};
156+
157+
module.exports = rule;

tests/files/no-rename-default/anon.js

+1
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,3 @@
1+
const bar = 'bar';
2+
3+
export default bar;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const foo = 'foo';
2+
3+
export default foo;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 123;

tests/src/rules/no-rename-default.js

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { RuleTester } from 'eslint';
2+
import { test } from '../utils';
3+
4+
const ruleTester = new RuleTester();
5+
const rule = require('rules/no-rename-default');
6+
7+
ruleTester.run('no-rename-default', rule, {
8+
valid: [
9+
test({
10+
code: `
11+
import _ from './no-rename-default/anon.js'
12+
`,
13+
}),
14+
test({
15+
code: `
16+
import bar from './no-rename-default/named-bar'
17+
import foo from './no-rename-default/named-foo'
18+
`,
19+
}),
20+
test({
21+
code: `
22+
import _ from './no-rename-default/primitive'
23+
`,
24+
}),
25+
test({
26+
code: `
27+
const _ = require('./no-rename-default/anon.js')
28+
`,
29+
options: [{ commonjs: true }],
30+
}),
31+
test({
32+
code: `
33+
const bar = require('./no-rename-default/named-bar')
34+
const foo = require('./no-rename-default/named-foo')
35+
`,
36+
options: [{ commonjs: true }],
37+
}),
38+
test({
39+
code: `
40+
const _ = require('./no-rename-default/primitive')
41+
`,
42+
options: [{ commonjs: true }],
43+
}),
44+
],
45+
46+
invalid: [
47+
test({
48+
code: `
49+
import bar from './no-rename-default/named-foo'
50+
`,
51+
errors: [{
52+
message: 'Caution: `named-foo.js` has a default export `foo`. This imports `foo` as `bar`. Check if you meant to write `import foo from \'./no-rename-default/named-foo\'` instead.',
53+
type: 'ImportDefaultSpecifier',
54+
}],
55+
}),
56+
test({
57+
code: `
58+
import foo from './no-rename-default/named-bar'
59+
import bar from './no-rename-default/named-foo'
60+
`,
61+
errors: [{
62+
message: 'Caution: `named-bar.js` has a default export `bar`. This imports `bar` as `foo`. Check if you meant to write `import bar from \'./no-rename-default/named-bar\'` instead.',
63+
type: 'ImportDefaultSpecifier',
64+
}, {
65+
message: 'Caution: `named-foo.js` has a default export `foo`. This imports `foo` as `bar`. Check if you meant to write `import foo from \'./no-rename-default/named-foo\'` instead.',
66+
type: 'ImportDefaultSpecifier',
67+
}],
68+
}),
69+
test({
70+
code: `
71+
const bar = require('./no-rename-default/named-foo')
72+
`,
73+
options: [{ commonjs: true }],
74+
errors: [{
75+
message: 'Caution: `named-foo.js` has a default export `foo`. This requires `foo` as `bar`. Check if you meant to write `const foo = require(\'./no-rename-default/named-foo\')` instead.',
76+
type: 'VariableDeclarator',
77+
}],
78+
}),
79+
test({
80+
code: `
81+
const foo = require('./no-rename-default/named-bar')
82+
const bar = require('./no-rename-default/named-foo')
83+
`,
84+
options: [{ commonjs: true }],
85+
errors: [{
86+
message: 'Caution: `named-bar.js` has a default export `bar`. This requires `bar` as `foo`. Check if you meant to write `const bar = require(\'./no-rename-default/named-bar\')` instead.',
87+
type: 'VariableDeclarator',
88+
}, {
89+
message: 'Caution: `named-foo.js` has a default export `foo`. This requires `foo` as `bar`. Check if you meant to write `const foo = require(\'./no-rename-default/named-foo\')` instead.',
90+
type: 'VariableDeclarator',
91+
}],
92+
}),
93+
],
94+
});

0 commit comments

Comments
 (0)