diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a7254bc70..d57744a2a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,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 [`rename-default-import`] rule: Enforce default import naming ## [2.13.0] - 2018-06-24 ### Added @@ -470,6 +471,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 diff --git a/README.md b/README.md index 53b2640627..6a7442320a 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,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 an alias for the default package import ([`rename-default-import`]) [`first`]: ./docs/rules/first.md [`exports-last`]: ./docs/rules/exports-last.md @@ -105,6 +106,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a [`group-exports`]: ./docs/rules/group-exports.md [`no-default-export`]: ./docs/rules/no-default-export.md [`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md +[`rename-default-import`]: ./docs/rules/rename-default-import.md ## Installation diff --git a/docs/rules/rename-default-import.md b/docs/rules/rename-default-import.md new file mode 100644 index 0000000000..f2361e09f7 --- /dev/null +++ b/docs/rules/rename-default-import.md @@ -0,0 +1,56 @@ +# import/rename-default-import + +This rule will enforce an alias for a default package import. Only ES6 imports are processed. + + +## Rule Details + +Given: + +```js +// ./foo.js +export default function () { return 'Foo' } +``` + +and + +```yaml +// .eslintrc +rules: + import/rename-default-import: + - warn + - prop-types: PropTypes + Foo: Foo // default for package Foo should be aliased to Foo +``` + +The following is considered valid: + +```js +import Foo from './foo' + +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 names and the aliases 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 7df67867f5..c6abc755d8 100644 --- a/src/index.js +++ b/src/index.js @@ -37,6 +37,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'), diff --git a/src/rules/rename-default-import.js b/src/rules/rename-default-import.js new file mode 100644 index 0000000000..935696929e --- /dev/null +++ b/src/rules/rename-default-import.js @@ -0,0 +1,87 @@ +/** + * @fileoverview Rule to enforce aliases for default imports + * @author Michał Kołodziejski + */ + +import docsUrl from '../docsUrl' + + +function isDefaultImport(specifier) { + return specifier.type === 'ImportDefaultSpecifier' || specifier.imported.name === 'default' +} + + +function handleImport(context, specifier, source) { + const {value: packageName} = source + const {local: {name: importAlias}} = specifier + const mappings = context.options[0] + + if (!Object.keys(mappings).includes(packageName)) { + return + } + + if (mappings[packageName] !== importAlias) { + context.report({ + node: source, + message: `Default import from '${packageName}' should be aliased to ` + + `${mappings[packageName]}, not ${importAlias}`, + fix: fixer => { + let newAlias = mappings[packageName] + if (specifier.imported && specifier.imported.name === 'default') { + newAlias = `default as ${mappings[packageName]}` + } + + return fixer.replaceText(specifier, newAlias) + }, + }) + + const declaredVariable = context.getDeclaredVariables(specifier)[0] + for (const variableReference of declaredVariable.references) { + context.report({ + node: variableReference.identifier, + message: `Using incorrect alias '${variableReference.identifier.name}' ` + + `instead of ${mappings[packageName]} for ` + + `default import from package ${packageName}`, + fix: fixer => { + return fixer.replaceText(variableReference.identifier, mappings[packageName]) + }, + }) + } + } +} + + +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.options + + if (options.length === 0 || Object.keys(options[0]).length === 0) { + return + } + + for (const specifier of specifiers) { + if (!isDefaultImport(specifier)) { + continue + } + + handleImport(context, specifier, source) + } + }, + } + }, +} diff --git a/tests/src/rules/rename-default-import.js b/tests/src/rules/rename-default-import.js new file mode 100644 index 0000000000..94ca53d434 --- /dev/null +++ b/tests/src/rules/rename-default-import.js @@ -0,0 +1,160 @@ +import { test } from '../utils' + +import { RuleTester } from 'eslint' + +const ruleTester = new RuleTester(), + rule = require('rules/rename-default-import') + +ruleTester.run('rename-default-import', rule, { + valid: [ + test({ + code: ` + import PropTypes from 'prop-types'; + `, + options: [], + }), + 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'}], + }), + ], + invalid: [ + test({ + code: `import propTypes from 'prop-types';`, + options: [{'prop-types': 'PropTypes'}], + output: `import PropTypes from 'prop-types';`, + errors: [{ + ruleId: 'rename-default-import', + message: `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: [{ + ruleId: 'rename-default-import', + message: `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: [{ + ruleId: 'rename-default-import', + message: `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: [{ + ruleId: 'rename-default-import', + message: `Default import from 'prop-types' should be aliased to PropTypes, not propTypes`, + }, { + ruleId: 'rename-default-import', + message: `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: [{ + ruleId: 'rename-default-import', + message: `Default import from 'prop-types' should be aliased to PropTypes, not propTypes`, + }, { + ruleId: 'rename-default-import', + message: `Using incorrect alias '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: [{ + ruleId: 'rename-default-import', + message: `Default import from 'bar' should be aliased to Foo, not foo`, + }, { + ruleId: 'rename-default-import', + message: `Using incorrect alias 'foo' instead of Foo for default import from package bar`, + }, { + ruleId: 'rename-default-import', + message: `Using incorrect alias 'foo' instead of Foo for default import from package bar`, + }, { + ruleId: 'rename-default-import', + message: `Using incorrect alias 'foo' instead of Foo for default import from package bar`, + }, { + ruleId: 'rename-default-import', + message: `Using incorrect alias 'foo' instead of Foo for default import from package bar`, + }, { + ruleId: 'rename-default-import', + message: `Using incorrect alias 'foo' instead of Foo for default import from package bar`, + }] + }) + ] +})