Skip to content

Commit

Permalink
[New] no-rename-default: Forbid importing a default export by a dif…
Browse files Browse the repository at this point in the history
…ferent name
  • Loading branch information
whitneyit committed May 2, 2024
1 parent e1bd0ba commit d46657f
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions config/warnings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
31 changes: 31 additions & 0 deletions docs/rules/no-rename-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# import/no-rename-default

⚠️ This rule _warns_ in the 🚸 `warnings` config.

<!-- end auto-generated rule header -->

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';
```
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
157 changes: 157 additions & 0 deletions src/rules/no-rename-default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* @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 getDefaultExportName(defaultExportNode) {
return defaultExportNode.declaration.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;
}

return {
ImportDeclaration(node) {
const exportMap = getExportMap(node.source, context);
if (exportMap == null) {
return;
}

const defaultExportNode = getDefaultExportNode(exportMap);
if (defaultExportNode == null) {
return;
}

const defaultExportName = getDefaultExportName(defaultExportNode);
const importTarget = node.source.value;
const importBasename = path.basename(exportMap.path);

node.specifiers.forEach((importClause) => {
const importName = importClause.local.name;

// No named default export
if (defaultExportName === undefined) {
return;
}

// The name of the import matches the name of the default export.
if (importName === defaultExportName) {
return;
}

context.report({
node: importClause,
message: `Caution: \`${importBasename}\` has a default export \`${defaultExportName}\`. This imports \`${defaultExportName}\` as \`${importName}\`. Check if you meant to write \`import ${defaultExportName} from '${importTarget}'\` instead.`,
});
});
},
VariableDeclarator(node) {
const options = context.options[0] || {};

if (
!options.commonjs
|| node.type !== 'VariableDeclarator'
// return if it's not an object destructure or it's an empty object destructure
|| !node.id || node.id.type !== 'Identifier'
// return if there is no call expression on the right side
|| !node.init || node.init.type !== 'CallExpression'
) {
return;
}

const call = node.init;
const [source] = call.arguments;

if (
// return if it's not a commonjs require statement
call.callee.type !== 'Identifier' || call.callee.name !== 'require' || call.arguments.length !== 1
// return if it's not a string source
|| 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);
const requireTarget = source.value;
const requireBasename = path.basename(exportMap.path);
const requireName = node.id.name;

// No named default export
if (defaultExportName === undefined) {
return;
}

// The name of the require matches the name of the default export.
if (requireName === defaultExportName) {
return;
}

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.`,
});
},
};
},
};

module.exports = rule;
1 change: 1 addition & 0 deletions tests/files/no-rename-default/anon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default {};
3 changes: 3 additions & 0 deletions tests/files/no-rename-default/named-bar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const bar = 'bar';

export default bar;
3 changes: 3 additions & 0 deletions tests/files/no-rename-default/named-foo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const foo = 'foo';

export default foo;
1 change: 1 addition & 0 deletions tests/files/no-rename-default/primitive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 123;
94 changes: 94 additions & 0 deletions tests/src/rules/no-rename-default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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/anon.js'
`,
}),
test({
code: `
import bar from './no-rename-default/named-bar'
import foo from './no-rename-default/named-foo'
`,
}),
test({
code: `
import _ from './no-rename-default/primitive'
`,
}),
test({
code: `
const _ = require('./no-rename-default/anon.js')
`,
options: [{ commonjs: true }],
}),
test({
code: `
const bar = require('./no-rename-default/named-bar')
const foo = require('./no-rename-default/named-foo')
`,
options: [{ commonjs: true }],
}),
test({
code: `
const _ = require('./no-rename-default/primitive')
`,
options: [{ commonjs: true }],
}),
],

invalid: [
test({
code: `
import bar from './no-rename-default/named-foo'
`,
errors: [{
message: 'Caution: `named-foo.js` has a default export `foo`. This imports `foo` as `bar`. Check if you meant to write `import foo from \'./no-rename-default/named-foo\'` instead.',
type: 'ImportDefaultSpecifier',
}],
}),
test({
code: `
import foo from './no-rename-default/named-bar'
import bar from './no-rename-default/named-foo'
`,
errors: [{
message: 'Caution: `named-bar.js` has a default export `bar`. This imports `bar` as `foo`. Check if you meant to write `import bar from \'./no-rename-default/named-bar\'` instead.',
type: 'ImportDefaultSpecifier',
}, {
message: 'Caution: `named-foo.js` has a default export `foo`. This imports `foo` as `bar`. Check if you meant to write `import foo from \'./no-rename-default/named-foo\'` instead.',
type: 'ImportDefaultSpecifier',
}],
}),
test({
code: `
const bar = require('./no-rename-default/named-foo')
`,
options: [{ commonjs: true }],
errors: [{
message: 'Caution: `named-foo.js` has a default export `foo`. This requires `foo` as `bar`. Check if you meant to write `const foo = require(\'./no-rename-default/named-foo\')` instead.',
type: 'VariableDeclarator',
}],
}),
test({
code: `
const foo = require('./no-rename-default/named-bar')
const bar = require('./no-rename-default/named-foo')
`,
options: [{ commonjs: true }],
errors: [{
message: 'Caution: `named-bar.js` has a default export `bar`. This requires `bar` as `foo`. Check if you meant to write `const bar = require(\'./no-rename-default/named-bar\')` instead.',
type: 'VariableDeclarator',
}, {
message: 'Caution: `named-foo.js` has a default export `foo`. This requires `foo` as `bar`. Check if you meant to write `const foo = require(\'./no-rename-default/named-foo\')` instead.',
type: 'VariableDeclarator',
}],
}),
],
});

0 comments on commit d46657f

Please sign in to comment.