Skip to content

Commit 68a276f

Browse files
committed
Add rule to enforce default import aliases
1 parent 112a0bf commit 68a276f

File tree

6 files changed

+607
-0
lines changed

6 files changed

+607
-0
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
44
This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).
55

66
## [Unreleased]
7+
- Add [`rename-default-import`] rule: Enforce default import naming
78

89
### Added
910
- [`group-exports`]: make aggregate module exports valid ([#1472], thanks [@atikenny])
@@ -606,6 +607,7 @@ for info on changes for earlier releases.
606607
[`order`]: ./docs/rules/order.md
607608
[`prefer-default-export`]: ./docs/rules/prefer-default-export.md
608609
[`unambiguous`]: ./docs/rules/unambiguous.md
610+
[`rename-default-import`]: ./docs/rules/rename-default-import.md
609611

610612
[`memo-parser`]: ./memo-parser/README.md
611613

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
9494
* Forbid anonymous values as default exports ([`no-anonymous-default-export`])
9595
* Prefer named exports to be grouped together in a single export declaration ([`group-exports`])
9696
* Enforce a leading comment with the webpackChunkName for dynamic imports ([`dynamic-import-chunkname`])
97+
* Enforce a specific binding name for the default package import ([`rename-default-import`])
9798

9899
[`first`]: ./docs/rules/first.md
99100
[`exports-last`]: ./docs/rules/exports-last.md
@@ -111,6 +112,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
111112
[`no-default-export`]: ./docs/rules/no-default-export.md
112113
[`no-named-export`]: ./docs/rules/no-named-export.md
113114
[`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md
115+
[`rename-default-import`]: ./docs/rules/rename-default-import.md
114116

115117
## Support
116118

docs/rules/rename-default-import.md

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

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

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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+
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 = exportedIdentifiers.indexOf(importAlias) !== -1
51+
52+
context.report({
53+
node: node,
54+
message: `Default import from '${packageName}' should be aliased to `
55+
+ `${mappings[packageName]}, not ${importAlias}`,
56+
fix: skipFixing ? null : fixImportOrRequire(specifierOrDeclaration, mappings[packageName]),
57+
})
58+
59+
for (const variableReference of references) {
60+
if (specifierOrDeclaration.type === 'VariableDeclarator' && variableReference.init) {
61+
continue
62+
}
63+
64+
context.report({
65+
node: variableReference.identifier,
66+
message: `Using incorrect binding name '${variableReference.identifier.name}' `
67+
+ `instead of ${mappings[packageName]} for `
68+
+ `default import from package ${packageName}`,
69+
fix: fixer => {
70+
if (skipFixing) {
71+
return
72+
}
73+
74+
return fixer.replaceText(variableReference.identifier, mappings[packageName])
75+
},
76+
})
77+
}
78+
}
79+
80+
function fixImportOrRequire(node, text) {
81+
return function(fixer) {
82+
let newAlias = text
83+
let nodeOrToken
84+
if (node.type === 'VariableDeclarator') {
85+
nodeOrToken = node.id
86+
newAlias = text
87+
} else {
88+
nodeOrToken = node
89+
if (node.imported && node.imported.name === 'default') {
90+
newAlias = `default as ${text}`
91+
} else {
92+
newAlias = text
93+
}
94+
}
95+
96+
return fixer.replaceText(nodeOrToken, newAlias)
97+
}
98+
}
99+
100+
module.exports = {
101+
meta: {
102+
docs: {
103+
url: docsUrl('rename-default-import'),
104+
recommended: false,
105+
},
106+
schema: [
107+
{
108+
type: 'object',
109+
minProperties: 1,
110+
additionalProperties: {
111+
type: 'string',
112+
},
113+
},
114+
],
115+
fixable: 'code',
116+
},
117+
create: function(context) {
118+
const exportedIdentifiers = []
119+
return {
120+
'Program': function(programNode) {
121+
const {body} = programNode
122+
123+
body.forEach((node) => {
124+
if (node.type === 'ExportNamedDeclaration') {
125+
node.specifiers.forEach((specifier) => {
126+
const {exported: {name}} = specifier
127+
if (exportedIdentifiers.indexOf(name) === -1) {
128+
exportedIdentifiers.push(name)
129+
}
130+
})
131+
}
132+
})
133+
},
134+
'ImportDeclaration:exit': function(node) {
135+
const {source, specifiers} = node
136+
const {options} = context
137+
138+
if (options.length === 0) {
139+
return
140+
}
141+
142+
for (const specifier of specifiers) {
143+
if (!isDefaultImport(specifier)) {
144+
continue
145+
}
146+
147+
handleImport(
148+
context,
149+
source,
150+
specifier,
151+
source.value,
152+
specifier.local.name,
153+
exportedIdentifiers
154+
)
155+
}
156+
},
157+
'VariableDeclaration:exit': function(node) {
158+
const {declarations} = node
159+
const {options} = context
160+
161+
if (options.length === 0) {
162+
return
163+
}
164+
165+
for (const declaration of declarations) {
166+
if (!isCommonJSImport(declaration) || context.getScope(declaration).type !== 'module') {
167+
continue
168+
}
169+
170+
handleImport(
171+
context,
172+
node,
173+
declaration,
174+
declaration.init.arguments[0].value,
175+
declaration.id.name,
176+
exportedIdentifiers
177+
)
178+
}
179+
},
180+
}
181+
},
182+
}

0 commit comments

Comments
 (0)