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 authored and ljharb committed May 2, 2024
1 parent d0231c0 commit 4dd8492
Show file tree
Hide file tree
Showing 33 changed files with 1,440 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
- [`dynamic-import-chunkname`]: Allow empty chunk name when webpackMode: 'eager' is set; add suggestions to remove name in eager mode ([#3004], thanks [@amsardesai])
- [`no-unused-modules`]: Add `ignoreUnusedTypeExports` option ([#3011], thanks [@silverwind])
- add support for Flat Config ([#3018], thanks [@michaelfaith])
- [`no-rename-default`]: Forbid importing a default export by a different name ([#3006], thanks [@whitneyit])

### Fixed
- [`no-extraneous-dependencies`]: allow wrong path ([#3012], thanks [@chabb])
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
29 changes: 29 additions & 0 deletions docs/rules/no-rename-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# import/no-rename-default

<!-- 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 @@ -22,6 +22,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
278 changes: 278 additions & 0 deletions src/rules/no-rename-default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
/**
* @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('eslint').Rule.RuleModule} */
module.exports = {
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: {
default: false,
type: 'boolean',
},
preventRenamingBindings: {
default: true,
type: 'boolean',
},
},
additionalProperties: false,
},
],
},

create(context) {
const {
commonjs = false,
preventRenamingBindings = true,
} = context.options[0] || {};

function getDefaultExportName(targetNode) {
if (targetNode == null) {
return;
}
switch (targetNode.type) {
case 'AssignmentExpression': {
if (!preventRenamingBindings) {
// Allow assignments to be renamed when the `preventRenamingBindings`
// option is set to `false`.
//
// export default Foo = 1;
return;
}
return targetNode.left.name;
}
case 'CallExpression': {
const [argumentNode] = targetNode.arguments;
return getDefaultExportName(argumentNode);
}
case 'ClassDeclaration': {
if (targetNode.id && typeof targetNode.id.name === 'string') {
return targetNode.id.name;
}
// Here we have an anonymous class. We can skip here.
return;
}
case 'ExportSpecifier': {
return targetNode.local.name;
}
case 'FunctionDeclaration': {
return targetNode.id.name;
}
case 'Identifier': {
if (!preventRenamingBindings) {
// Allow identifier to be renamed when the `preventRenamingBindings`
// option is set to `false`.
//
// const foo = 'foo';
// export default foo;
return;
}
return targetNode.name;
}
default:
// This type of node is not handled.
// Returning `undefined` here signifies this and causes the check to
// exit early.
}
}

function getDefaultExportNode(exportMap) {
const defaultExportNode = exportMap.exports.get('default');
if (defaultExportNode == null) {
return;
}

if (defaultExportNode.type === 'ExportDefaultDeclaration') {
return defaultExportNode.declaration;
}

if (defaultExportNode.type === 'ExportNamedDeclaration') {
return defaultExportNode.specifiers.find((specifier) => specifier.exported.name === 'default');
}
}

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);
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.`,
data: {
defaultExportName,
importBasename,
importName,
importTarget,
},
});

return;
}

if (node.type !== 'ImportSpecifier' || 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.`,
data: {
actualImportedName,
defaultExportName,
importBasename,
importTarget,
},
});
}

function handleRequire(node) {
if (
!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 = node.id.properties.find((property) => property.key.name === 'default');
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);
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;
}

const data = {
defaultExportName,
requireBasename,
requireName,
requireTarget,
};

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

return {
ImportDefaultSpecifier(node) {
handleImport(node);
},
ImportSpecifier(node) {
handleImport(node);
},
VariableDeclarator(node) {
handleRequire(node);
},
};
},
};
1 change: 1 addition & 0 deletions tests/files/no-rename-default/anonymous-arrow-async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default async () => {};
1 change: 1 addition & 0 deletions tests/files/no-rename-default/anonymous-arrow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => {};
1 change: 1 addition & 0 deletions tests/files/no-rename-default/anonymous-class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default class {};
1 change: 1 addition & 0 deletions tests/files/no-rename-default/anonymous-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default {};
1 change: 1 addition & 0 deletions tests/files/no-rename-default/anonymous-primitive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 123;
1 change: 1 addition & 0 deletions tests/files/no-rename-default/assign-arrow-async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default arrowAsync = async () => {};
1 change: 1 addition & 0 deletions tests/files/no-rename-default/assign-arrow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default arrow = () => {};
1 change: 1 addition & 0 deletions tests/files/no-rename-default/assign-class-named.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default User = class MyUser {}
1 change: 1 addition & 0 deletions tests/files/no-rename-default/assign-class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default User = class {}
1 change: 1 addition & 0 deletions tests/files/no-rename-default/assign-fn-named.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default fn = function myFn() {};
1 change: 1 addition & 0 deletions tests/files/no-rename-default/assign-fn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default fn = function () {};
1 change: 1 addition & 0 deletions tests/files/no-rename-default/assign-generator-named.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default generator = function* myGenerator() {};
1 change: 1 addition & 0 deletions tests/files/no-rename-default/assign-generator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default generator = function* () {};
1 change: 1 addition & 0 deletions tests/files/no-rename-default/class-user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default class User {};
6 changes: 6 additions & 0 deletions tests/files/no-rename-default/const-bar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const barNamed1 = 'bar-named-1';
export const barNamed2 = 'bar-named-2';

const bar = 'bar';

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

const foo = 'foo';

export default foo;
1 change: 1 addition & 0 deletions tests/files/no-rename-default/fn-get-users-sync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function getUsersSync() {}
1 change: 1 addition & 0 deletions tests/files/no-rename-default/fn-get-users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default async function getUsers() {}
1 change: 1 addition & 0 deletions tests/files/no-rename-default/generator-reader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function* reader() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const foo = function bar() {};

export default foo;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function bar() {}

export default bar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import foo from '../default-const-foo';
import withLogger from './hoc-with-logger';

export default withLogger(foo);
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import getUsers from '../default-fn-get-users';
import withLogger from './hoc-with-logger';

export default withLogger(getUsers);
Loading

0 comments on commit 4dd8492

Please sign in to comment.