From d0bcc54e3e5fa01598f4540986ad3c6b194c343f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ko=C5=82odziejski?= Date: Mon, 23 Jul 2018 14:23:37 +0200 Subject: [PATCH] Add rule to enforce default import aliases --- CHANGELOG.md | 3 + README.md | 2 + docs/rules/rename-default-import.md | 63 ++++ src/index.js | 1 + src/rules/rename-default-import.js | 182 ++++++++++++ tests/src/rules/rename-default-import.js | 357 +++++++++++++++++++++++ 6 files changed, 608 insertions(+) create mode 100644 docs/rules/rename-default-import.md create mode 100644 src/rules/rename-default-import.js create mode 100644 tests/src/rules/rename-default-import.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 9427a79ae6..bb731c795b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ 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 - [`order`]: Fix interpreting some external modules being interpreted as internal modules ([#793], [#794] thanks [@ephys]) @@ -512,6 +514,7 @@ for info on changes for earlier releases. [`no-cycle`]: ./docs/rules/no-cycle.md [`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md [`no-named-export`]: ./docs/rules/no-named-export.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 41bbff0a0d..75ebbacd93 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..ac2b9d4c89 --- /dev/null +++ b/docs/rules/rename-default-import.md @@ -0,0 +1,63 @@ +# 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' +``` + +```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 6cbe0a6428..4eb9f995ea 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..f939f41311 --- /dev/null +++ b/src/rules/rename-default-import.js @@ -0,0 +1,182 @@ +/** + * @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: { + docs: { + url: docsUrl('rename-default-import'), + recommended: false, + }, + schema: [ + { + type: 'object', + minProperties: 1, + additionalProperties: { + type: 'string', + }, + }, + ], + fixable: 'code', + }, + 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/rename-default-import.js b/tests/src/rules/rename-default-import.js new file mode 100644 index 0000000000..4354d10b8c --- /dev/null +++ b/tests/src/rules/rename-default-import.js @@ -0,0 +1,357 @@ +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: `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: [{ + 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 {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: [{ + 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 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: [{ + ruleId: 'rename-default-import', + message: `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: [{ + 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`, + }] + }) + ] +})