diff --git a/CHANGELOG.md b/CHANGELOG.md index eb1666b49..8ffab41e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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]) @@ -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 diff --git a/README.md b/README.md index e08e72ffa..3e48bedf3 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/docs/rules/enforce-import-name.md b/docs/rules/enforce-import-name.md new file mode 100644 index 000000000..b261e2c9c --- /dev/null +++ b/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`. diff --git a/src/index.js b/src/index.js index d0a98b7cb..e07405408 100644 --- a/src/index.js +++ b/src/index.js @@ -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'), diff --git a/src/rules/enforce-import-name.js b/src/rules/enforce-import-name.js new file mode 100644 index 000000000..95e2bd210 --- /dev/null +++ b/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 + ) + } + }, + } + }, +} diff --git a/tests/src/rules/enforce-import-name.js b/tests/src/rules/enforce-import-name.js new file mode 100644 index 000000000..2c6e1bd9a --- /dev/null +++ b/tests/src/rules/enforce-import-name.js @@ -0,0 +1,302 @@ +import { test } from '../utils' + +import { RuleTester } from 'eslint' + +const ruleTester = new RuleTester() +const rule = require('rules/enforce-import-name') + +ruleTester.run('enforce-import-name', rule, { + valid: [ + test({ + code: `import PropTypes from 'prop-types';`, + options: [], + }), + test({ + code: `import PropTypes from 'prop-types';`, + options: [{'foo': 'Foo'}], + }), + test({ + code: `import PropTypes from 'prop-types';`, + options: [{'prop-types': 'PropTypes'}], + }), + test({ + code: `import PropTypes, {Foo} from 'prop-types';`, + options: [{'prop-types': 'PropTypes'}], + }), + test({ + code: `import {default as PropTypes} from 'prop-types';`, + options: [{'prop-types': 'PropTypes'}], + }), + test({ + code: `import {Foo} from 'prop-types';`, + options: [{'prop-types': 'PropTypes'}], + }), + test({ + code: `import {Foo, default as PropTypes} from 'prop-types';`, + options: [{'prop-types': 'PropTypes'}], + }), + test({ + code: `import * as PropTypes from 'prop-types';`, + options: [{'prop-types': 'PropTypes'}], + }), + test({ + code: `import * as propTypes from 'prop-types';`, + options: [{'prop-types': 'PropTypes'}], + }), + test({ + code: `const PropTypes = require('prop-types');`, + options: [{'prop-types': 'PropTypes'}], + }), + test({ + code: `const object = require('prop-types').object;`, + options: [{'prop-types': 'PropTypes'}], + }), + test({ + code: `const PropTypes = require('prop-types');`, + options: [], + }), + test({ + code: `require('prop-types');`, + options: [{'prop-types': 'PropTypes'}], + }), + ], + invalid: [ + test({ + code: `import propTypes from 'prop-types';`, + options: [{'prop-types': 'PropTypes'}], + output: `import PropTypes from 'prop-types';`, + errors: [`Default import from 'prop-types' should be aliased to PropTypes, not propTypes`], + }), + test({ + code: `const propTypes = require('prop-types');`, + options: [{'prop-types': 'PropTypes'}], + output: `const PropTypes = require('prop-types');`, + errors: [`Default import from 'prop-types' should be aliased to PropTypes, not propTypes`], + }), + test({ + code: `import propTypes, {B} from 'prop-types';`, + options: [{'prop-types': 'PropTypes'}], + output: `import PropTypes, {B} from 'prop-types';`, + errors: [`Default import from 'prop-types' should be aliased to PropTypes, not propTypes`], + }), + test({ + code: `import {default as propTypes} from 'prop-types';`, + options: [{'prop-types': 'PropTypes'}], + output: `import {default as PropTypes} from 'prop-types';`, + errors: [`Default import from 'prop-types' should be aliased to PropTypes, not propTypes`], + }), + test({ + code: `import {default as propTypes, foo} from 'prop-types';`, + options: [{'prop-types': 'PropTypes'}], + output: `import {default as PropTypes, foo} from 'prop-types';`, + errors: [`Default import from 'prop-types' should be aliased to PropTypes, not propTypes`], + }), + test({ + code: `import propTypes from 'prop-types';import foo from 'foo';`, + options: [{'prop-types': 'PropTypes', 'foo': 'Foo'}], + output: `import PropTypes from 'prop-types';import Foo from 'foo';`, + errors: [ + `Default import from 'prop-types' should be aliased to PropTypes, not propTypes`, + `Default import from 'foo' should be aliased to Foo, not foo`, + ], + }), + test({ + code: ` + import propTypes from 'prop-types'; + + const obj = { + foo: propTypes.string + } + `, + options: [{'prop-types': 'PropTypes'}], + output: ` + import PropTypes from 'prop-types'; + + const obj = { + foo: PropTypes.string + } + `, + errors: [ + `Default import from 'prop-types' should be aliased to PropTypes, not propTypes`, + `Using incorrect binding name 'propTypes' instead of PropTypes for default import from package prop-types`, + ], + }), + test({ + code: ` + import propTypes from 'prop-types'; + + const obj = { + foo: propTypes.string + } + + export {propTypes}; + `, + options: [{'prop-types': 'PropTypes'}], + output: ` + import propTypes from 'prop-types'; + + const obj = { + foo: propTypes.string + } + + export {propTypes}; + `, + errors: [ + `Default import from 'prop-types' should be aliased to PropTypes, not propTypes`, + `Using incorrect binding name 'propTypes' instead of PropTypes for default import from package prop-types`, + `Using incorrect binding name 'propTypes' instead of PropTypes for default import from package prop-types`, + ], + }), + test({ + code: ` + import propTypes from 'prop-types'; + + const obj = { + foo: propTypes.string + } + + export {propTypes as PropTypes, obj}; + `, + options: [{'prop-types': 'PropTypes'}], + output: ` + import PropTypes from 'prop-types'; + + const obj = { + foo: PropTypes.string + } + + export {PropTypes as PropTypes, obj}; + `, + errors: [ + `Default import from 'prop-types' should be aliased to PropTypes, not propTypes`, + `Using incorrect binding name 'propTypes' instead of PropTypes for default import from package prop-types`, + `Using incorrect binding name 'propTypes' instead of PropTypes for default import from package prop-types`, + ], + }), + test({ + code: ` + import propTypes from 'prop-types'; + + const obj = { + foo: propTypes.string + } + + export function props() { + return propTypes; + }; + + export class A { + get b() { + return propTypes.number; + } + }; + `, + options: [{'prop-types': 'PropTypes'}], + output: ` + import PropTypes from 'prop-types'; + + const obj = { + foo: PropTypes.string + } + + export function props() { + return PropTypes; + }; + + export class A { + get b() { + return PropTypes.number; + } + }; + `, + errors: [ + `Default import from 'prop-types' should be aliased to PropTypes, not propTypes`, + `Using incorrect binding name 'propTypes' instead of PropTypes for default import from package prop-types`, + `Using incorrect binding name 'propTypes' instead of PropTypes for default import from package prop-types`, + `Using incorrect binding name 'propTypes' instead of PropTypes for default import from package prop-types`, + ], + }), + test({ + code: ` + const func = function (require) { + const b = require(); + }; + + const propTypes = require('prop-types'); + `, + options: [{'prop-types': 'PropTypes'}], + output: ` + const func = function (require) { + const b = require(); + }; + + const PropTypes = require('prop-types'); + `, + errors: [`Default import from 'prop-types' should be aliased to PropTypes, not propTypes`], + }), + test({ + code: ` + const propTypes = require('prop-types'); + + const obj = { + foo: propTypes.string + } + `, + options: [{'prop-types': 'PropTypes'}], + output: ` + const PropTypes = require('prop-types'); + + const obj = { + foo: PropTypes.string + } + `, + errors: [ + `Default import from 'prop-types' should be aliased to PropTypes, not propTypes`, + `Using incorrect binding name 'propTypes' instead of PropTypes for default import from package prop-types`, + ], + }), + test({ + code: ` + import foo from 'bar'; + const a = foo.foo(); + const b = bar(foo); + const c = (foo) => { + foo(); + }; + c(foo) + const d = (bar) => { + bar(); + }; + d(foo); + const e = () => { + foo(); + }; + `, + options: [{'bar': 'Foo'}], + output: ` + import Foo from 'bar'; + const a = Foo.foo(); + const b = bar(Foo); + const c = (foo) => { + foo(); + }; + c(Foo) + const d = (bar) => { + bar(); + }; + d(Foo); + const e = () => { + Foo(); + }; + `, + errors: [ + `Default import from 'bar' should be aliased to Foo, not foo`, + `Using incorrect binding name 'foo' instead of Foo for default import from package bar`, + `Using incorrect binding name 'foo' instead of Foo for default import from package bar`, + `Using incorrect binding name 'foo' instead of Foo for default import from package bar`, + `Using incorrect binding name 'foo' instead of Foo for default import from package bar`, + `Using incorrect binding name 'foo' instead of Foo for default import from package bar`, + ], + }), + ], +})