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 51d4f6d
Show file tree
Hide file tree
Showing 17 changed files with 615 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
217 changes: 217 additions & 0 deletions src/rules/no-rename-default.js
Original file line number Diff line number Diff line change
@@ -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;
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,4 @@
import getUsers from './default-fn-get-users';
import withLogger from './hoc-with-logger';

export default withLogger(getUsers);
1 change: 1 addition & 0 deletions tests/files/no-rename-default/default-anonymous.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default {};
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-primitive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 123;
6 changes: 6 additions & 0 deletions tests/files/no-rename-default/hoc-with-auth.js
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);
}
}
6 changes: 6 additions & 0 deletions tests/files/no-rename-default/hoc-with-logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default function withLogger(fn) {
return function innerLogger(...args) {
console.log(`${fn.name} called`);
return fn.apply(null, args);
}
}

0 comments on commit 51d4f6d

Please sign in to comment.