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 3, 2024
1 parent e1bd0ba commit 6788026
Show file tree
Hide file tree
Showing 33 changed files with 1,394 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
254 changes: 254 additions & 0 deletions src/rules/no-rename-default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
/**
* @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: {
default: false,
type: 'boolean',
},
preventRenamingBindings: {
default: true,
type: 'boolean',
},
},
additionalProperties: false,
},
],
},

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

function findDefaultDestructure(properties) {
const found = properties.find((property) => {
if (property.key.name === 'default') {
return property;
}
});
return found;
}

function getDefaultExportName(targetNode) {
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 '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;
}
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) {
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 = 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;
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/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/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/default-anonymous-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default {};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 123;
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/default-assign-arrow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default arrow = () => {};
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/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/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/default-assign-fn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default fn = function () {};
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/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/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/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/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/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/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/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);
Original file line number Diff line number Diff line change
@@ -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));
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default function withAuth(fn) {
return function innerAuth(...args) {
const auth = {};
return fn.call(null, auth, ...args);
}
}

0 comments on commit 6788026

Please sign in to comment.