diff --git a/CHANGELOG.md b/CHANGELOG.md index a07647c82d..9bc4a25beb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Added - [`dynamic-import-chunkname`]: add `allowEmpty` option to allow empty leading comments ([#2942], thanks [@JiangWeixian]) +- [`no-rename-default`]: Forbid importing a default export by a different name ([#3006], thanks [@whitneyit]) ### Changed - [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit ([#2944], thanks [@mulztob]) diff --git a/README.md b/README.md index d6f107d1c9..925e86ee47 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a | [no-mutable-exports](docs/rules/no-mutable-exports.md) | Forbid the use of mutable exports with `var` or `let`. | | | | | | | | [no-named-as-default](docs/rules/no-named-as-default.md) | Forbid use of exported name as identifier of default export. | | ☑️ 🚸 | | | | | | [no-named-as-default-member](docs/rules/no-named-as-default-member.md) | Forbid use of exported name as property of default export. | | ☑️ 🚸 | | | | | +| [no-rename-default](docs/rules/no-rename-default.md) | Forbid importing a default export by a different name. | | 🚸 | | | | | | [no-unused-modules](docs/rules/no-unused-modules.md) | Forbid modules without exports, or exports without matching import in another module. | | | | | | | ### Module systems diff --git a/config/warnings.js b/config/warnings.js index 5d74143b28..499f3193b2 100644 --- a/config/warnings.js +++ b/config/warnings.js @@ -7,6 +7,7 @@ module.exports = { rules: { 'import/no-named-as-default': 1, 'import/no-named-as-default-member': 1, + 'import/no-rename-default': 1, 'import/no-duplicates': 1, }, }; diff --git a/docs/rules/no-rename-default.md b/docs/rules/no-rename-default.md new file mode 100644 index 0000000000..ec53a6a0ba --- /dev/null +++ b/docs/rules/no-rename-default.md @@ -0,0 +1,31 @@ +# import/no-rename-default + +⚠️ This rule _warns_ in the 🚸 `warnings` config. + + + +Prohibit importing a default export by another name. + +## Rule Details + +Given: + +```js +// api/get-users.js +export default async function getUsers() {} +``` + +...this would be valid: + +```js +import getUsers from './api/get-users.js'; +``` + +...and the following would be reported: + +```js +// Caution: `get-users.js` has a default export `getUsers`. +// This imports `getUsers` as `findUsers`. +// Check if you meant to write `import getUsers from './api/get-users'` instead. +import findUsers from './get-users'; +``` diff --git a/src/index.js b/src/index.js index feafba9003..c32f7ba907 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,7 @@ export const rules = { 'no-named-as-default': require('./rules/no-named-as-default'), 'no-named-as-default-member': require('./rules/no-named-as-default-member'), 'no-anonymous-default-export': require('./rules/no-anonymous-default-export'), + 'no-rename-default': require('./rules/no-rename-default'), 'no-unused-modules': require('./rules/no-unused-modules'), 'no-commonjs': require('./rules/no-commonjs'), diff --git a/src/rules/no-rename-default.js b/src/rules/no-rename-default.js new file mode 100644 index 0000000000..cf776cd67d --- /dev/null +++ b/src/rules/no-rename-default.js @@ -0,0 +1,217 @@ +/** + * @fileOverview Rule to warn about importing a default export by different name + * @author James Whitney + */ + +import docsUrl from '../docsUrl'; +import ExportMapBuilder from '../exportMap/builder'; +import path from 'path'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +/** @type {import('@typescript-eslint/utils').TSESLint.RuleModule} */ +const rule = { + meta: { + type: 'suggestion', + docs: { + category: 'Helpful warnings', + description: 'Forbid importing a default export by a different name.', + recommended: false, + url: docsUrl('no-named-as-default'), + }, + schema: [ + { + type: 'object', + properties: { + commonjs: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + }, + + create(context) { + function findDefaultDestructure(properties) { + const found = properties.find((property) => { + if (property.key.name === 'default') { + return property; + } + }); + return found; + } + + function getDefaultExportName(targetNode) { + if (targetNode.type === 'CallExpression') { + const [argumentNode] = targetNode.arguments; + return getDefaultExportName(argumentNode); + } + if (targetNode.type === 'FunctionDeclaration') { + return targetNode.id.name; + } + if (targetNode.type === 'Identifier') { + return targetNode.name; + } + } + + function getDefaultExportNode(exportMap) { + const defaultExportNode = exportMap.exports.get('default'); + if (defaultExportNode == null) { + return; + } + return defaultExportNode; + } + + function getExportMap(source, context) { + const exportMap = ExportMapBuilder.get(source.value, context); + if (exportMap == null) { + return; + } + if (exportMap.errors.length > 0) { + exportMap.reportErrors(context, source.value); + return; + } + return exportMap; + } + + function handleImport(node) { + + const exportMap = getExportMap(node.parent.source, context); + if (exportMap == null) { + return; + } + + const defaultExportNode = getDefaultExportNode(exportMap); + if (defaultExportNode == null) { + return; + + } + + const defaultExportName = getDefaultExportName(defaultExportNode.declaration); + if (defaultExportName === undefined) { + return; + } + + const importTarget = node.parent.source.value; + const importBasename = path.basename(exportMap.path); + + if (node.type === 'ImportDefaultSpecifier') { + const importName = node.local.name; + + if (importName === defaultExportName) { + return; + } + + context.report({ + node, + message: `Caution: \`${importBasename}\` has a default export \`${defaultExportName}\`. This imports \`${defaultExportName}\` as \`${importName}\`. Check if you meant to write \`import ${defaultExportName} from '${importTarget}'\` instead.`, + }); + + return; + } + + if (node.type !== 'ImportSpecifier') { + return; + } + + if (node.imported.name !== 'default') { + return; + } + + const actualImportedName = node.local.name; + + if (actualImportedName === defaultExportName) { + return; + } + + context.report({ + node, + message: `Caution: \`${importBasename}\` has a default export \`${defaultExportName}\`. This imports \`${defaultExportName}\` as \`${actualImportedName}\`. Check if you meant to write \`import { default as ${defaultExportName} } from '${importTarget}'\` instead.`, + }); + } + + function handleRequire(node) { + const options = context.options[0] || {}; + + if ( + !options.commonjs + || node.type !== 'VariableDeclarator' + || !node.id || !(node.id.type === 'Identifier' || node.id.type === 'ObjectPattern') + || !node.init || node.init.type !== 'CallExpression' + ) { + return; + } + + let defaultDestructure; + if (node.id.type === 'ObjectPattern') { + defaultDestructure = findDefaultDestructure(node.id.properties); + if (defaultDestructure === undefined) { + return; + } + } + + const call = node.init; + const [source] = call.arguments; + + if ( + call.callee.type !== 'Identifier' || call.callee.name !== 'require' || call.arguments.length !== 1 + || source.type !== 'Literal' + ) { + return; + } + + const exportMap = getExportMap(source, context); + if (exportMap == null) { + return; + } + + const defaultExportNode = getDefaultExportNode(exportMap); + if (defaultExportNode == null) { + return; + } + + const defaultExportName = getDefaultExportName(defaultExportNode.declaration); + const requireTarget = source.value; + const requireBasename = path.basename(exportMap.path); + const requireName = node.id.type === 'Identifier' ? node.id.name : defaultDestructure.value.name; + + if (defaultExportName === undefined) { + return; + } + + if (requireName === defaultExportName) { + return; + } + + if (node.id.type === 'Identifier') { + context.report({ + node, + message: `Caution: \`${requireBasename}\` has a default export \`${defaultExportName}\`. This requires \`${defaultExportName}\` as \`${requireName}\`. Check if you meant to write \`const ${defaultExportName} = require('${requireTarget}')\` instead.`, + }); + return; + } + + context.report({ + node, + message: `Caution: \`${requireBasename}\` has a default export \`${defaultExportName}\`. This requires \`${defaultExportName}\` as \`${requireName}\`. Check if you meant to write \`const { default: ${defaultExportName} } = require('${requireTarget}')\` instead.`, + }); + } + + return { + ImportDefaultSpecifier(node) { + handleImport(node); + }, + ImportSpecifier(node) { + handleImport(node); + }, + VariableDeclarator(node) { + handleRequire(node); + }, + }; + }, +}; + +module.exports = rule; diff --git a/tests/files/no-rename-default/binding-fn-get-users-with-logger-and-auth.js b/tests/files/no-rename-default/binding-fn-get-users-with-logger-and-auth.js new file mode 100644 index 0000000000..a5008b7453 --- /dev/null +++ b/tests/files/no-rename-default/binding-fn-get-users-with-logger-and-auth.js @@ -0,0 +1,5 @@ +import getUsers from './default-fn-get-users'; +import withAuth from './hoc-with-auth'; +import withLogger from './hoc-with-logger'; + +export default withLogger(withAuth(getUsers)); diff --git a/tests/files/no-rename-default/binding-fn-get-users-with-logger.js b/tests/files/no-rename-default/binding-fn-get-users-with-logger.js new file mode 100644 index 0000000000..384d6ad9e0 --- /dev/null +++ b/tests/files/no-rename-default/binding-fn-get-users-with-logger.js @@ -0,0 +1,4 @@ +import getUsers from './default-fn-get-users'; +import withLogger from './hoc-with-logger'; + +export default withLogger(getUsers); diff --git a/tests/files/no-rename-default/default-anonymous.js b/tests/files/no-rename-default/default-anonymous.js new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/tests/files/no-rename-default/default-anonymous.js @@ -0,0 +1 @@ +export default {}; diff --git a/tests/files/no-rename-default/default-const-bar.js b/tests/files/no-rename-default/default-const-bar.js new file mode 100644 index 0000000000..da071dcc12 --- /dev/null +++ b/tests/files/no-rename-default/default-const-bar.js @@ -0,0 +1,6 @@ +export const barNamed1 = 'bar-named-1'; +export const barNamed2 = 'bar-named-2'; + +const bar = 'bar'; + +export default bar; diff --git a/tests/files/no-rename-default/default-const-foo.js b/tests/files/no-rename-default/default-const-foo.js new file mode 100644 index 0000000000..a85147d86c --- /dev/null +++ b/tests/files/no-rename-default/default-const-foo.js @@ -0,0 +1,6 @@ +export const fooNamed1 = 'foo-named-1'; +export const fooNamed2 = 'foo-named-2'; + +const foo = 'foo'; + +export default foo; diff --git a/tests/files/no-rename-default/default-fn-get-users-sync.js b/tests/files/no-rename-default/default-fn-get-users-sync.js new file mode 100644 index 0000000000..c1f60a47fe --- /dev/null +++ b/tests/files/no-rename-default/default-fn-get-users-sync.js @@ -0,0 +1 @@ +export default function getUsersSync() {} diff --git a/tests/files/no-rename-default/default-fn-get-users.js b/tests/files/no-rename-default/default-fn-get-users.js new file mode 100644 index 0000000000..9b77ba1961 --- /dev/null +++ b/tests/files/no-rename-default/default-fn-get-users.js @@ -0,0 +1 @@ +export default async function getUsers() {} diff --git a/tests/files/no-rename-default/default-primitive.js b/tests/files/no-rename-default/default-primitive.js new file mode 100644 index 0000000000..05e0871204 --- /dev/null +++ b/tests/files/no-rename-default/default-primitive.js @@ -0,0 +1 @@ +export default 123; diff --git a/tests/files/no-rename-default/hoc-with-auth.js b/tests/files/no-rename-default/hoc-with-auth.js new file mode 100644 index 0000000000..03c9006a28 --- /dev/null +++ b/tests/files/no-rename-default/hoc-with-auth.js @@ -0,0 +1,6 @@ +export default function withAuth(fn) { + return function innerAuth(...args) { + const auth = {}; + return fn.call(null, auth, ...args); + } +} diff --git a/tests/files/no-rename-default/hoc-with-logger.js b/tests/files/no-rename-default/hoc-with-logger.js new file mode 100644 index 0000000000..bead436214 --- /dev/null +++ b/tests/files/no-rename-default/hoc-with-logger.js @@ -0,0 +1,6 @@ +export default function withLogger(fn) { + return function innerLogger(...args) { + console.log(`${fn.name} called`); + return fn.apply(null, args); + } +} diff --git a/tests/src/rules/no-rename-default.js b/tests/src/rules/no-rename-default.js new file mode 100644 index 0000000000..9e27003dd2 --- /dev/null +++ b/tests/src/rules/no-rename-default.js @@ -0,0 +1,326 @@ +import { RuleTester } from 'eslint'; +import { test } from '../utils'; + +const ruleTester = new RuleTester(); +const rule = require('rules/no-rename-default'); + +ruleTester.run('no-rename-default', rule, { + valid: [ + test({ + code: ` + import _ from './no-rename-default/default-anonymous.js' + `, + }), + test({ + code: ` + import _ from './no-rename-default/default-primitive.js' + `, + }), + test({ + code: ` + import foo from './no-rename-default/default-const-foo' + `, + }), + test({ + code: ` + import { fooNamed1 } from './no-rename-default/default-const-foo' + `, + }), + test({ + code: ` + import { fooNamed1, fooNamed2 } from './no-rename-default/default-const-foo' + `, + }), + test({ + code: ` + import { default as foo } from './no-rename-default/default-const-foo' + `, + }), + test({ + code: ` + import { default as foo, fooNamed1 } from './no-rename-default/default-const-foo' + `, + }), + test({ + code: ` + import foo, { fooNamed1 } from './no-rename-default/default-const-foo' + `, + }), + test({ + code: ` + import bar from './no-rename-default/default-const-bar' + import foo from './no-rename-default/default-const-foo' + `, + }), + test({ + code: ` + import getUsers from './no-rename-default/default-fn-get-users' + `, + }), + test({ + code: ` + import getUsersSync from './no-rename-default/default-fn-get-users-sync' + `, + }), + test({ + code: ` + import bar, { barNamed1 } from './no-rename-default/default-const-bar' + import foo, { fooNamed1 } from './no-rename-default/default-const-foo' + `, + }), + test({ + code: ` + const _ = require('./no-rename-default/default-anonymous.js') + `, + options: [{ commonjs: true }], + }), + test({ + code: ` + const _ = require('./no-rename-default/default-primitive.js') + `, + options: [{ commonjs: true }], + }), + test({ + code: ` + const foo = require('./no-rename-default/default-const-foo') + `, + options: [{ commonjs: true }], + }), + test({ + code: ` + const { fooNamed1 } = require('./no-rename-default/default-const-foo') + `, + options: [{ commonjs: true }], + }), + test({ + code: ` + const { fooNamed1, fooNamed2 } = require('./no-rename-default/default-const-foo') + `, + options: [{ commonjs: true }], + }), + test({ + code: ` + const { default: foo } = require('./no-rename-default/default-const-foo') + `, + options: [{ commonjs: true }], + }), + test({ + code: ` + const { default: foo, fooNamed1 } = require('./no-rename-default/default-const-foo') + `, + options: [{ commonjs: true }], + }), + test({ + code: ` + const foo = require('./no-rename-default/default-const-foo') + const { fooNamed1 } = require('./no-rename-default/default-const-foo') + `, + options: [{ commonjs: true }], + }), + test({ + code: ` + const getUsers = require('./no-rename-default/default-fn-get-users') + `, + options: [{ commonjs: true }], + }), + test({ + code: ` + const getUsersSync = require('./no-rename-default/default-fn-get-users-sync') + `, + options: [{ commonjs: true }], + }), + test({ + code: ` + const bar = require('./no-rename-default/default-const-bar') + const { barNamed1 } = require('./no-rename-default/default-const-bar') + const foo = require('./no-rename-default/default-const-foo') + const { fooNamed1 } = require('./no-rename-default/default-const-foo') + `, + options: [{ commonjs: true }], + }), + test({ + code: ` + import getUsers from './no-rename-default/binding-fn-get-users-with-logger' + `, + }), + test({ + code: ` + import getUsers from './no-rename-default/binding-fn-get-users-with-logger-and-auth' + `, + }), + ], + + invalid: [ + test({ + code: ` + import bar from './no-rename-default/default-const-foo' + `, + errors: [{ + message: 'Caution: `default-const-foo.js` has a default export `foo`. This imports `foo` as `bar`. Check if you meant to write `import foo from \'./no-rename-default/default-const-foo\'` instead.', + type: 'ImportDefaultSpecifier', + }], + }), + test({ + code: ` + import { default as bar } from './no-rename-default/default-const-foo' + `, + errors: [{ + message: 'Caution: `default-const-foo.js` has a default export `foo`. This imports `foo` as `bar`. Check if you meant to write `import { default as foo } from \'./no-rename-default/default-const-foo\'` instead.', + type: 'ImportSpecifier', + }], + }), + test({ + code: ` + import { default as bar, fooNamed1 } from './no-rename-default/default-const-foo' + `, + errors: [{ + message: 'Caution: `default-const-foo.js` has a default export `foo`. This imports `foo` as `bar`. Check if you meant to write `import { default as foo } from \'./no-rename-default/default-const-foo\'` instead.', + type: 'ImportSpecifier', + }], + }), + test({ + code: ` + import bar, { fooNamed1 } from './no-rename-default/default-const-foo' + `, + errors: [{ + message: 'Caution: `default-const-foo.js` has a default export `foo`. This imports `foo` as `bar`. Check if you meant to write `import foo from \'./no-rename-default/default-const-foo\'` instead.', + type: 'ImportDefaultSpecifier', + }], + }), + test({ + code: ` + import foo from './no-rename-default/default-const-bar' + import bar from './no-rename-default/default-const-foo' + `, + errors: [{ + message: 'Caution: `default-const-bar.js` has a default export `bar`. This imports `bar` as `foo`. Check if you meant to write `import bar from \'./no-rename-default/default-const-bar\'` instead.', + type: 'ImportDefaultSpecifier', + }, { + message: 'Caution: `default-const-foo.js` has a default export `foo`. This imports `foo` as `bar`. Check if you meant to write `import foo from \'./no-rename-default/default-const-foo\'` instead.', + type: 'ImportDefaultSpecifier', + }], + }), + test({ + code: ` + import findUsers from './no-rename-default/default-fn-get-users' + `, + errors: [{ + message: 'Caution: `default-fn-get-users.js` has a default export `getUsers`. This imports `getUsers` as `findUsers`. Check if you meant to write `import getUsers from \'./no-rename-default/default-fn-get-users\'` instead.', + type: 'ImportDefaultSpecifier', + }], + }), + test({ + code: ` + import findUsersSync from './no-rename-default/default-fn-get-users-sync' + `, + errors: [{ + message: 'Caution: `default-fn-get-users-sync.js` has a default export `getUsersSync`. This imports `getUsersSync` as `findUsersSync`. Check if you meant to write `import getUsersSync from \'./no-rename-default/default-fn-get-users-sync\'` instead.', + type: 'ImportDefaultSpecifier', + }], + }), + test({ + code: ` + import foo, { barNamed1 } from './no-rename-default/default-const-bar' + import bar, { fooNamed1 } from './no-rename-default/default-const-foo' + `, + errors: [{ + message: 'Caution: `default-const-bar.js` has a default export `bar`. This imports `bar` as `foo`. Check if you meant to write `import bar from \'./no-rename-default/default-const-bar\'` instead.', + type: 'ImportDefaultSpecifier', + }, { + message: 'Caution: `default-const-foo.js` has a default export `foo`. This imports `foo` as `bar`. Check if you meant to write `import foo from \'./no-rename-default/default-const-foo\'` instead.', + type: 'ImportDefaultSpecifier', + }], + }), + test({ + code: ` + const bar = require('./no-rename-default/default-const-foo') + `, + options: [{ commonjs: true }], + errors: [{ + message: 'Caution: `default-const-foo.js` has a default export `foo`. This requires `foo` as `bar`. Check if you meant to write `const foo = require(\'./no-rename-default/default-const-foo\')` instead.', + type: 'VariableDeclarator', + }], + }), + test({ + code: ` + const bar = require('./no-rename-default/default-const-foo') + const { fooNamed1 } = require('./no-rename-default/default-const-foo') + `, + options: [{ commonjs: true }], + errors: [{ + message: 'Caution: `default-const-foo.js` has a default export `foo`. This requires `foo` as `bar`. Check if you meant to write `const foo = require(\'./no-rename-default/default-const-foo\')` instead.', + type: 'VariableDeclarator', + }], + }), + test({ + code: ` + const { default: bar } = require('./no-rename-default/default-const-foo') + `, + options: [{ commonjs: true }], + errors: [{ + message: 'Caution: `default-const-foo.js` has a default export `foo`. This requires `foo` as `bar`. Check if you meant to write `const { default: foo } = require(\'./no-rename-default/default-const-foo\')` instead.', + type: 'VariableDeclarator', + }], + }), + test({ + code: ` + const { default: bar, fooNamed1 } = require('./no-rename-default/default-const-foo') + `, + options: [{ commonjs: true }], + errors: [{ + message: 'Caution: `default-const-foo.js` has a default export `foo`. This requires `foo` as `bar`. Check if you meant to write `const { default: foo } = require(\'./no-rename-default/default-const-foo\')` instead.', + type: 'VariableDeclarator', + }], + }), + test({ + code: ` + const foo = require('./no-rename-default/default-const-bar') + const bar = require('./no-rename-default/default-const-foo') + `, + options: [{ commonjs: true }], + errors: [{ + message: 'Caution: `default-const-bar.js` has a default export `bar`. This requires `bar` as `foo`. Check if you meant to write `const bar = require(\'./no-rename-default/default-const-bar\')` instead.', + type: 'VariableDeclarator', + }, { + message: 'Caution: `default-const-foo.js` has a default export `foo`. This requires `foo` as `bar`. Check if you meant to write `const foo = require(\'./no-rename-default/default-const-foo\')` instead.', + type: 'VariableDeclarator', + }], + }), + test({ + code: ` + const findUsers = require('./no-rename-default/default-fn-get-users') + `, + options: [{ commonjs: true }], + errors: [{ + message: 'Caution: `default-fn-get-users.js` has a default export `getUsers`. This requires `getUsers` as `findUsers`. Check if you meant to write `const getUsers = require(\'./no-rename-default/default-fn-get-users\')` instead.', + type: 'VariableDeclarator', + }], + }), + test({ + code: ` + const findUsersSync = require('./no-rename-default/default-fn-get-users-sync') + `, + options: [{ commonjs: true }], + errors: [{ + message: 'Caution: `default-fn-get-users-sync.js` has a default export `getUsersSync`. This requires `getUsersSync` as `findUsersSync`. Check if you meant to write `const getUsersSync = require(\'./no-rename-default/default-fn-get-users-sync\')` instead.', + type: 'VariableDeclarator', + }], + }), + test({ + code: ` + const foo = require('./no-rename-default/default-const-bar') + const { barNamed1 } = require('./no-rename-default/default-const-bar') + const bar = require('./no-rename-default/default-const-foo') + const { fooNamed1 } = require('./no-rename-default/default-const-foo') + `, + options: [{ commonjs: true }], + errors: [{ + message: 'Caution: `default-const-bar.js` has a default export `bar`. This requires `bar` as `foo`. Check if you meant to write `const bar = require(\'./no-rename-default/default-const-bar\')` instead.', + type: 'VariableDeclarator', + }, { + message: 'Caution: `default-const-foo.js` has a default export `foo`. This requires `foo` as `bar`. Check if you meant to write `const foo = require(\'./no-rename-default/default-const-foo\')` instead.', + type: 'VariableDeclarator', + }], + }), + ], +});