Skip to content

Commit f579ea7

Browse files
mic4aelljharb
authored andcommitted
[New] add [rename-default-import] rule: Enforce default import naming
1 parent 4ff9b92 commit f579ea7

File tree

6 files changed

+556
-0
lines changed

6 files changed

+556
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
1212
- [`order`]: Add support for TypeScript's "import equals"-expressions ([#1785], thanks [@manuth])
1313
- [`import/default`]: support default export in TSExportAssignment ([#1689], thanks [@Maxim-Mazurok])
1414
- [`no-restricted-paths`]: add custom message support ([#1802], thanks [@malykhinvi])
15+
- add [`rename-default-import`] rule: Enforce default import naming ([#1143], thanks [@mic4ael])
1516

1617
### Fixed
1718
- [`group-exports`]: Flow type export awareness ([#1702], thanks [@ernestostifano])
@@ -687,6 +688,7 @@ for info on changes for earlier releases.
687688
[`order`]: ./docs/rules/order.md
688689
[`prefer-default-export`]: ./docs/rules/prefer-default-export.md
689690
[`unambiguous`]: ./docs/rules/unambiguous.md
691+
[`rename-default-import`]: ./docs/rules/rename-default-import.md
690692

691693
[`memo-parser`]: ./memo-parser/README.md
692694

@@ -800,6 +802,7 @@ for info on changes for earlier releases.
800802
[#1163]: https://github.com/benmosher/eslint-plugin-import/pull/1163
801803
[#1157]: https://github.com/benmosher/eslint-plugin-import/pull/1157
802804
[#1151]: https://github.com/benmosher/eslint-plugin-import/pull/1151
805+
[#1143]: https://github.com/benmosher/eslint-plugin-import/pull/1143
803806
[#1142]: https://github.com/benmosher/eslint-plugin-import/pull/1142
804807
[#1139]: https://github.com/benmosher/eslint-plugin-import/pull/1139
805808
[#1137]: https://github.com/benmosher/eslint-plugin-import/pull/1137
@@ -1197,3 +1200,4 @@ for info on changes for earlier releases.
11971200
[@adjerbetian]: https://github.com/adjerbetian
11981201
[@Maxim-Mazurok]: https://github.com/Maxim-Mazurok
11991202
[@malykhinvi]: https://github.com/malykhinvi
1203+
[@mic4ael]: https://github.com/mic4ael

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
9292
* Forbid anonymous values as default exports ([`no-anonymous-default-export`])
9393
* Prefer named exports to be grouped together in a single export declaration ([`group-exports`])
9494
* Enforce a leading comment with the webpackChunkName for dynamic imports ([`dynamic-import-chunkname`])
95+
* Enforce a specific binding name for the default package import ([`rename-default-import`])
9596

9697
[`first`]: ./docs/rules/first.md
9798
[`exports-last`]: ./docs/rules/exports-last.md
@@ -109,6 +110,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
109110
[`no-default-export`]: ./docs/rules/no-default-export.md
110111
[`no-named-export`]: ./docs/rules/no-named-export.md
111112
[`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md
113+
[`rename-default-import`]: ./docs/rules/rename-default-import.md
112114

113115
## `eslint-plugin-import` for enterprise
114116

docs/rules/rename-default-import.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# import/rename-default-import
2+
3+
This rule will enforce a specific binding name for a default package import.
4+
Works for ES6 imports and CJS require.
5+
6+
7+
## Rule Details
8+
9+
Given:
10+
11+
There is a package `prop-types` with a default export
12+
13+
and
14+
15+
```json
16+
// .eslintrc
17+
{
18+
"rules": {
19+
"import/rename-default-import": [
20+
"warn", {
21+
"prop-types": "PropTypes", // key: name of the module, value: desired binding for default import
22+
}
23+
]
24+
}
25+
}
26+
```
27+
28+
The following is considered valid:
29+
30+
```js
31+
import {default as PropTypes} from 'prop-types'
32+
33+
import PropTypes from 'prop-types'
34+
```
35+
36+
```js
37+
const PropTypes = require('prop-types');
38+
```
39+
40+
...and the following cases are reported:
41+
42+
```js
43+
import propTypes from 'prop-types';
44+
import {default as propTypes} from 'prop-types';
45+
```
46+
47+
```js
48+
const propTypes = require('prop-types');
49+
```
50+
51+
## When not to use it
52+
53+
As long as you don't want to enforce specific naming for default imports.
54+
55+
## Options
56+
57+
This rule accepts an object which is a mapping
58+
between package name and the binding name that should be used for default imports.
59+
For example, a configuration like the one below
60+
61+
`{'prop-types': 'PropTypes'}`
62+
63+
specifies that default import for the package `prop-types` should be aliased to `PropTypes`.

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const rules = {
3939
'no-unassigned-import': require('./rules/no-unassigned-import'),
4040
'no-useless-path-segments': require('./rules/no-useless-path-segments'),
4141
'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'),
42+
'rename-default-import': require('./rules/rename-default-import'),
4243

4344
// export
4445
'exports-last': require('./rules/exports-last'),

src/rules/rename-default-import.js

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* @fileoverview Rule to enforce aliases for default imports
3+
* @author Michał Kołodziejski
4+
*/
5+
6+
import docsUrl from '../docsUrl'
7+
import has from 'has'
8+
import includes from 'array-includes'
9+
10+
function isDefaultImport(specifier) {
11+
if (specifier.type === 'ImportDefaultSpecifier') {
12+
return true
13+
}
14+
if (specifier.type === 'ImportSpecifier' && specifier.imported.name === 'default') {
15+
return true
16+
}
17+
return false
18+
}
19+
20+
function isCommonJSImport(declaration) {
21+
const variableInit = declaration.init
22+
if (variableInit.type === 'CallExpression') {
23+
return variableInit.callee.name === 'require'
24+
}
25+
return false
26+
}
27+
28+
function handleImport(
29+
context,
30+
node,
31+
specifierOrDeclaration,
32+
packageName,
33+
importAlias,
34+
exportedIdentifiers
35+
) {
36+
const mappings = context.options[0] || {}
37+
38+
if (!has(mappings, packageName) || mappings[packageName] === importAlias) {
39+
return
40+
}
41+
42+
let declaredVariables
43+
if (specifierOrDeclaration.type === 'VariableDeclarator') {
44+
declaredVariables = context.getDeclaredVariables(specifierOrDeclaration.parent)[0]
45+
} else {
46+
declaredVariables = context.getDeclaredVariables(specifierOrDeclaration)[0]
47+
}
48+
49+
const references = declaredVariables ? declaredVariables.references : []
50+
const skipFixing = includes(exportedIdentifiers, importAlias)
51+
52+
context.report({
53+
node: node,
54+
message: `Default import from \`${packageName}\` should be bound to \`${mappings[packageName]}\`, not \`${importAlias}\``,
55+
fix: skipFixing ? null : fixImportOrRequire(specifierOrDeclaration, mappings[packageName]),
56+
})
57+
58+
for (const variableReference of references) {
59+
if (specifierOrDeclaration.type === 'VariableDeclarator' && variableReference.init) {
60+
continue
61+
}
62+
63+
context.report({
64+
node: variableReference.identifier,
65+
message: `Using incorrect binding name \`${variableReference.identifier.name}\` instead of \`${mappings[packageName]}\` for default import from package \`${packageName}\``,
66+
fix: fixer => {
67+
if (skipFixing) {
68+
return
69+
}
70+
71+
return fixer.replaceText(variableReference.identifier, mappings[packageName])
72+
},
73+
})
74+
}
75+
}
76+
77+
function fixImportOrRequire(node, text) {
78+
return function(fixer) {
79+
let newAlias = text
80+
let nodeOrToken
81+
if (node.type === 'VariableDeclarator') {
82+
nodeOrToken = node.id
83+
newAlias = text
84+
} else {
85+
nodeOrToken = node
86+
if (node.imported && node.imported.name === 'default') {
87+
newAlias = `default as ${text}`
88+
} else {
89+
newAlias = text
90+
}
91+
}
92+
93+
return fixer.replaceText(nodeOrToken, newAlias)
94+
}
95+
}
96+
97+
module.exports = {
98+
meta: {
99+
type: 'suggestion',
100+
docs: {
101+
url: docsUrl('rename-default-import'),
102+
recommended: false,
103+
},
104+
fixable: 'code',
105+
schema: [
106+
{
107+
type: 'object',
108+
minProperties: 1,
109+
additionalProperties: {
110+
type: 'string',
111+
},
112+
},
113+
],
114+
},
115+
create: function(context) {
116+
const exportedIdentifiers = []
117+
return {
118+
'Program': function(programNode) {
119+
const {body} = programNode
120+
121+
body.forEach((node) => {
122+
if (node.type === 'ExportNamedDeclaration') {
123+
node.specifiers.forEach((specifier) => {
124+
const {exported: {name}} = specifier
125+
if (!includes(exportedIdentifiers, name)) {
126+
exportedIdentifiers.push(name)
127+
}
128+
})
129+
}
130+
})
131+
},
132+
'ImportDeclaration:exit': function(node) {
133+
const {source, specifiers} = node
134+
const {options} = context
135+
136+
if (options.length === 0) {
137+
return
138+
}
139+
140+
for (const specifier of specifiers) {
141+
if (!isDefaultImport(specifier)) {
142+
continue
143+
}
144+
145+
handleImport(
146+
context,
147+
source,
148+
specifier,
149+
source.value,
150+
specifier.local.name,
151+
exportedIdentifiers
152+
)
153+
}
154+
},
155+
'VariableDeclaration:exit': function(node) {
156+
const {declarations} = node
157+
const {options} = context
158+
159+
if (options.length === 0) {
160+
return
161+
}
162+
163+
for (const declaration of declarations) {
164+
if (!isCommonJSImport(declaration) || context.getScope(declaration).type !== 'module') {
165+
continue
166+
}
167+
168+
handleImport(
169+
context,
170+
node,
171+
declaration,
172+
declaration.init.arguments[0].value,
173+
declaration.id.name,
174+
exportedIdentifiers
175+
)
176+
}
177+
},
178+
}
179+
},
180+
}

0 commit comments

Comments
 (0)