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 Sep 15, 2020
1 parent fef718c commit f48590d
Show file tree
Hide file tree
Showing 6 changed files with 553 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).

## [Unreleased]
- Add [`enforce-import-name`] rule: Enforce default import naming ([#1143], thanks [@mic4ael])

### Fixed
- [`default`]/TypeScript: avoid crash on `export =` with a MemberExpression ([#1841], thanks [@ljharb])
Expand Down Expand Up @@ -728,6 +729,7 @@ for info on changes for earlier releases.
[`order`]: ./docs/rules/order.md
[`prefer-default-export`]: ./docs/rules/prefer-default-export.md
[`unambiguous`]: ./docs/rules/unambiguous.md
[`enforce-import-name`]: ./docs/rules/enforce-import-name.md

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

Expand Down
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -92,6 +92,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 ([`enforce-import-name`])

[`first`]: ./docs/rules/first.md
[`exports-last`]: ./docs/rules/exports-last.md
Expand All @@ -109,6 +110,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
[`enforce-import-name`]: ./docs/rules/enforce-import-name.md

## `eslint-plugin-import` for enterprise

Expand Down
63 changes: 63 additions & 0 deletions docs/rules/enforce-import-name.md
@@ -0,0 +1,63 @@
# import/enforce-import-name

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/enforce-import-name": [
"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'
```

```js
const PropTypes = require('prop-types');
```

...and the following cases are reported:

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

```js
const propTypes = require('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 @@ -39,6 +39,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'),
'enforce-import-name': require('./rules/enforce-import-name'),

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

import docsUrl from '../docsUrl'
import has from 'has'


function isDefaultImport(specifier) {
if (specifier.type === 'ImportDefaultSpecifier') {
return true
}
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,
exportedIdentifiers
) {
const mappings = context.options[0] || {}

if (!has(mappings, packageName) || mappings[packageName] === importAlias) {
return
}

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

const references = declaredVariables ? declaredVariables.references : []
const skipFixing = exportedIdentifiers.indexOf(importAlias) !== -1

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: {
type: 'suggestion',
docs: {
url: docsUrl('enforce-import-name'),
recommended: false,
},
fixable: 'code',
schema: [
{
type: 'object',
minProperties: 1,
additionalProperties: {
type: 'string',
},
},
],
},
create: function(context) {
const exportedIdentifiers = []
return {
'Program': function(programNode) {
const {body} = programNode

body.forEach((node) => {
if (node.type === 'ExportNamedDeclaration') {
node.specifiers.forEach((specifier) => {
const {exported: {name}} = specifier
if (exportedIdentifiers.indexOf(name) === -1) {
exportedIdentifiers.push(name)
}
})
}
})
},
'ImportDeclaration:exit': function(node) {
const {source, specifiers} = node
const {options} = context

if (options.length === 0) {
return
}

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

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

if (options.length === 0) {
return
}

for (const declaration of declarations) {
if (!isCommonJSImport(declaration) || context.getScope(declaration).type !== 'module') {
continue
}

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

0 comments on commit f48590d

Please sign in to comment.