Skip to content

Commit

Permalink
Add rule to enforce default import aliases
Browse files Browse the repository at this point in the history
  • Loading branch information
mic4ael committed Oct 16, 2018
1 parent b4a2f11 commit 0da6220
Show file tree
Hide file tree
Showing 6 changed files with 504 additions and 2 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Expand Up @@ -4,15 +4,17 @@ This project adheres to [Semantic Versioning](http://semver.org/).
This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).

## [Unreleased]
- Add [`rename-default-import`] rule: Enforce default import naming

### Fixed
- [`no-extraneous-dependencies`]: `packageDir` option with array value was clobbering package deps instead of merging them ([#1175]/[#1176], thanks [@aravindet] & [@pzhine])


## [2.14.0] - 2018-08-13
* 69e0187 (HEAD -> master, source/master, origin/master, origin/HEAD) Merge pull request #1151 from jf248/jsx
|\
|\
| * e30a757 (source/pr/1151, fork/jsx) Add JSX check to namespace rule
|/
|/
* 8252344 (source/pr/1148) Add error to output when module loaded as resolver has invalid API
### Added
- [`no-useless-path-segments`]: add commonJS (CJS) support ([#1128], thanks [@1pete])
Expand Down Expand Up @@ -493,6 +495,7 @@ for info on changes for earlier releases.
[`no-default-export`]: ./docs/rules/no-default-export.md
[`no-useless-path-segments`]: ./docs/rules/no-useless-path-segments.md
[`no-cycle`]: ./docs/rules/no-cycle.md
[`rename-default-import`]: ./docs/rules/rename-default-import.md

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

Expand Down
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -90,6 +90,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
* Forbid anonymous values as default exports ([`no-anonymous-default-export`])
* Prefer named exports to be grouped together in a single export declaration ([`group-exports`])
* Enforce a leading comment with the webpackChunkName for dynamic imports ([`dynamic-import-chunkname`])
* Enforce a specific binding name for the default package import ([`rename-default-import`])

[`first`]: ./docs/rules/first.md
[`exports-last`]: ./docs/rules/exports-last.md
Expand All @@ -107,6 +108,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
[`no-default-export`]: ./docs/rules/no-default-export.md
[`no-named-export`]: ./docs/rules/no-named-export.md
[`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md
[`rename-default-import`]: ./docs/rules/rename-default-import.md

## Installation

Expand Down
56 changes: 56 additions & 0 deletions docs/rules/rename-default-import.md
@@ -0,0 +1,56 @@
# import/rename-default-import

This rule will enforce a specific binding name for a default package import.
Works for ES6 imports and CJS require.


## Rule Details

Given:

There is a package `prop-types` with a default export

and

```json
// .eslintrc
{
"rules": {
"import/rename-default-import": [
"warn", {
"prop-types": "PropTypes", // key: name of the module, value: desired binding for default import
}
]
}
}
```

The following is considered valid:

```js
import {default as PropTypes} from 'prop-types'

import PropTypes from 'prop-types'
```

...and the following cases are reported:

```js
import propTypes from 'prop-types';
import {default as propTypes} from 'prop-types';
```


## When not to use it

As long as you don't want to enforce specific naming for default imports.

## Options

This rule accepts an object which is a mapping
between package name and the binding name that should be used for default imports.
For example, a configuration like the one below

`{'prop-types': 'PropTypes'}`

specifies that default import for the package `prop-types` should be aliased to `PropTypes`.
1 change: 1 addition & 0 deletions src/index.js
Expand Up @@ -38,6 +38,7 @@ export const rules = {
'no-unassigned-import': require('./rules/no-unassigned-import'),
'no-useless-path-segments': require('./rules/no-useless-path-segments'),
'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'),
'rename-default-import': require('./rules/rename-default-import'),

// export
'exports-last': require('./rules/exports-last'),
Expand Down
147 changes: 147 additions & 0 deletions src/rules/rename-default-import.js
@@ -0,0 +1,147 @@
/**
* @fileoverview Rule to enforce aliases for default imports
* @author Michał Kołodziejski
*/

import docsUrl from '../docsUrl'


function isDefaultImport(specifier) {
if (specifier.type === 'ImportDefaultSpecifier') {
return true
} else if (specifier.type === 'ImportSpecifier' && specifier.imported.name === 'default') {
return true
}
return false
}

function isCommonJSImport(declaration) {
const variableInit = declaration.init
if (variableInit.type === 'CallExpression') {
return variableInit.callee.name === 'require'
}
return false
}

function handleImport(context, node, specifierOrDeclaration, packageName, importAlias) {
const mappings = context.options[0] || {}
const expectedAlias = mappings[packageName]

if (expectedAlias === undefined || expectedAlias === importAlias) {
return
}

let declaredVariables
if (specifierOrDeclaration.type === 'VariableDeclarator') {
declaredVariables = context.getDeclaredVariables(specifierOrDeclaration.parent)[0]
} else {
declaredVariables = context.getDeclaredVariables(specifierOrDeclaration)[0]
}

const references = declaredVariables.references
const skipFixing = references.some((ref) => ref.identifier.parent.type === 'ExportSpecifier')

context.report({
node: node,
message: `Default import from '${packageName}' should be aliased to `
+ `${mappings[packageName]}, not ${importAlias}`,
fix: skipFixing ? null : fixImportOrRequire(specifierOrDeclaration, mappings[packageName]),
})

for (const variableReference of references) {
if (specifierOrDeclaration.type === 'VariableDeclarator' && variableReference.init) {
continue
}

context.report({
node: variableReference.identifier,
message: `Using incorrect binding name '${variableReference.identifier.name}' `
+ `instead of ${mappings[packageName]} for `
+ `default import from package ${packageName}`,
fix: fixer => {
if (skipFixing) {
return
}

return fixer.replaceText(variableReference.identifier, mappings[packageName])
},
})
}
}

function fixImportOrRequire(node, text) {
return function(fixer) {
let newAlias = text
let nodeOrToken
if (node.type === 'VariableDeclarator') {
nodeOrToken = node.id
newAlias = text
} else {
nodeOrToken = node
if (node.imported && node.imported.name === 'default') {
newAlias = `default as ${text}`
} else {
newAlias = text
}
}

return fixer.replaceText(nodeOrToken, newAlias)
}
}

module.exports = {
meta: {
docs: {
url: docsUrl('rename-default-import'),
recommended: false,
},
schema: [
{
type: 'object',
},
],
fixable: 'code',
},
create: function(context) {
return {
'ImportDeclaration': function(node) {
const {source, specifiers} = node
const {options} = context

if (options.length === 0 || Object.keys(options[0]).length === 0) {
return
}

for (const specifier of specifiers) {
if (!isDefaultImport(specifier)) {
continue
}

handleImport(context, source, specifier, source.value, specifier.local.name)
}
},
'VariableDeclaration': function(node) {
const {declarations} = node
const {options} = context

if (options.length === 0 || Object.keys(options[0]).length === 0) {
return
}

for (const declaration of declarations) {
if (!isCommonJSImport(declaration)) {
continue
}

handleImport(
context,
node,
declaration,
declaration.init.arguments[0].value,
declaration.id.name
)
}
},
}
},
}

0 comments on commit 0da6220

Please sign in to comment.