Skip to content

Commit 6194ac0

Browse files
committed
Add rule to enforce default import aliases
1 parent 0d6d12e commit 6194ac0

File tree

6 files changed

+553
-0
lines changed

6 files changed

+553
-0
lines changed

CHANGELOG.md

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

77
## [Unreleased]
8+
- Add [`rename-default-import`] rule: Enforce default import naming ([#1143], thanks [@mic4ael])
89

910
### Added
1011
- [`import/default`]: support default export in TSExportAssignment ([#1528], thanks [@joaovieira])
@@ -686,6 +687,7 @@ for info on changes for earlier releases.
686687
[`order`]: ./docs/rules/order.md
687688
[`prefer-default-export`]: ./docs/rules/prefer-default-export.md
688689
[`unambiguous`]: ./docs/rules/unambiguous.md
690+
[`rename-default-import`]: ./docs/rules/rename-default-import.md
689691

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

README.md

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

+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

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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+
type: 'suggestion',
103+
docs: {
104+
url: docsUrl('rename-default-import'),
105+
recommended: false,
106+
},
107+
fixable: 'code',
108+
schema: [
109+
{
110+
type: 'object',
111+
minProperties: 1,
112+
additionalProperties: {
113+
type: 'string',
114+
},
115+
},
116+
],
117+
},
118+
create: function(context) {
119+
const exportedIdentifiers = []
120+
return {
121+
'Program': function(programNode) {
122+
const {body} = programNode
123+
124+
body.forEach((node) => {
125+
if (node.type === 'ExportNamedDeclaration') {
126+
node.specifiers.forEach((specifier) => {
127+
const {exported: {name}} = specifier
128+
if (exportedIdentifiers.indexOf(name) === -1) {
129+
exportedIdentifiers.push(name)
130+
}
131+
})
132+
}
133+
})
134+
},
135+
'ImportDeclaration:exit': function(node) {
136+
const {source, specifiers} = node
137+
const {options} = context
138+
139+
if (options.length === 0) {
140+
return
141+
}
142+
143+
for (const specifier of specifiers) {
144+
if (!isDefaultImport(specifier)) {
145+
continue
146+
}
147+
148+
handleImport(
149+
context,
150+
source,
151+
specifier,
152+
source.value,
153+
specifier.local.name,
154+
exportedIdentifiers
155+
)
156+
}
157+
},
158+
'VariableDeclaration:exit': function(node) {
159+
const {declarations} = node
160+
const {options} = context
161+
162+
if (options.length === 0) {
163+
return
164+
}
165+
166+
for (const declaration of declarations) {
167+
if (!isCommonJSImport(declaration) || context.getScope(declaration).type !== 'module') {
168+
continue
169+
}
170+
171+
handleImport(
172+
context,
173+
node,
174+
declaration,
175+
declaration.init.arguments[0].value,
176+
declaration.id.name,
177+
exportedIdentifiers
178+
)
179+
}
180+
},
181+
}
182+
},
183+
}

0 commit comments

Comments
 (0)