Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rule to enforce default import naming #1143

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
)
}
},
}
},
}