diff --git a/CHANGELOG.md b/CHANGELOG.md index a0b8a590d9..00620c0b9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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]) @@ -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 diff --git a/README.md b/README.md index 6f826a85b7..76d112eb07 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/docs/rules/rename-default-import.md b/docs/rules/rename-default-import.md new file mode 100644 index 0000000000..512a6ef15d --- /dev/null +++ b/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`. diff --git a/src/index.js b/src/index.js index f5794595d6..a2b42ebd18 100644 --- a/src/index.js +++ b/src/index.js @@ -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'), diff --git a/src/rules/rename-default-import.js b/src/rules/rename-default-import.js new file mode 100644 index 0000000000..f42f759edc --- /dev/null +++ b/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 + ) + } + }, + } + }, +} diff --git a/tests/src/rules/rename-default-import.js b/tests/src/rules/rename-default-import.js new file mode 100644 index 0000000000..44b9d8eaff --- /dev/null +++ b/tests/src/rules/rename-default-import.js @@ -0,0 +1,293 @@ +import { test } from '../utils' + +import { RuleTester } from 'eslint' + +const ruleTester = new RuleTester() +const 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: [{'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: `const PropTypes = require('prop-types');`, + options: [{'prop-types': 'PropTypes'}] + }), + test({ + code: `const object = require('prop-types').object;`, + 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: `const propTypes = require('prop-types');`, + options: [{'prop-types': 'PropTypes'}], + output: `const PropTypes = require('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 {default as propTypes, foo} from 'prop-types';`, + options: [{'prop-types': 'PropTypes'}], + output: `import {default as PropTypes, foo} 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 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: [{ + ruleId: 'rename-default-import', + message: `Default import from 'prop-types' should be aliased to PropTypes, not propTypes`, + }, { + ruleId: 'rename-default-import', + message: `Using incorrect binding name 'propTypes' instead of PropTypes for default import from package prop-types` + }, { + ruleId: 'rename-default-import', + message: `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: [{ + ruleId: 'rename-default-import', + message: `Default import from 'prop-types' should be aliased to PropTypes, not propTypes`, + }, { + ruleId: 'rename-default-import', + message: `Using incorrect binding name 'propTypes' instead of PropTypes for default import from package prop-types` + }, { + ruleId: 'rename-default-import', + message: `Using incorrect binding name 'propTypes' instead of PropTypes for default import from package prop-types` + }, { + ruleId: 'rename-default-import', + message: `Using incorrect binding name 'propTypes' instead of PropTypes for default import from package prop-types` + }], + }), + 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: [{ + ruleId: 'rename-default-import', + message: `Default import from 'prop-types' should be aliased to PropTypes, not propTypes`, + }, { + ruleId: 'rename-default-import', + message: `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: [{ + ruleId: 'rename-default-import', + message: `Default import from 'bar' should be aliased to Foo, not foo`, + }, { + ruleId: 'rename-default-import', + message: `Using incorrect binding name 'foo' instead of Foo for default import from package bar`, + }, { + ruleId: 'rename-default-import', + message: `Using incorrect binding name 'foo' instead of Foo for default import from package bar`, + }, { + ruleId: 'rename-default-import', + message: `Using incorrect binding name 'foo' instead of Foo for default import from package bar`, + }, { + ruleId: 'rename-default-import', + message: `Using incorrect binding name 'foo' instead of Foo for default import from package bar`, + }, { + ruleId: 'rename-default-import', + message: `Using incorrect binding name 'foo' instead of Foo for default import from package bar`, + }] + }) + ] +})