Skip to content

Commit

Permalink
prefer-export-from: Add ignoreUsedVariables option (#1590)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
fisker and sindresorhus committed Nov 10, 2021
1 parent 7c2867d commit a8d52e4
Show file tree
Hide file tree
Showing 6 changed files with 489 additions and 44 deletions.
33 changes: 32 additions & 1 deletion docs/rules/prefer-export-from.md
Expand Up @@ -2,7 +2,7 @@

*This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.*

🔧 *This rule is [auto-fixable](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).*
🔧💡 *This rule is [auto-fixable](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) and provides [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).*

When re-exporting from a module, it's unnecessary to import and then export. It can be done in a single `export…from` declaration.

Expand Down Expand Up @@ -61,3 +61,34 @@ export {
import * as namespace from './foo.js';
export default namespace;
```

## Options

### ignoreUsedVariables

Type: `boolean`\
Default: `false`

When `true`, if an import is used in other places than just a re-export, all variables in the import declaration will be ignored.

#### Fail

```js
// eslint unicorn/prefer-export-from: ["error", {"ignoreUsedVariables": false}]
import {named1, named2} from './foo.js';

use(named1);

export {named1, named2};
```

#### Pass

```js
// eslint unicorn/prefer-export-from: ["error", {"ignoreUsedVariables": true}]
import {named1, named2} from './foo.js';

use(named1);

export {named1, named2};
```
2 changes: 1 addition & 1 deletion readme.md
Expand Up @@ -212,7 +212,7 @@ Each rule has emojis denoting:
| [prefer-dom-node-dataset](docs/rules/prefer-dom-node-dataset.md) | Prefer using `.dataset` on DOM elements over `.setAttribute(…)`. || 🔧 | |
| [prefer-dom-node-remove](docs/rules/prefer-dom-node-remove.md) | Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`. || 🔧 | 💡 |
| [prefer-dom-node-text-content](docs/rules/prefer-dom-node-text-content.md) | Prefer `.textContent` over `.innerText`. || | 💡 |
| [prefer-export-from](docs/rules/prefer-export-from.md) | Prefer `export…from` when re-exporting. || 🔧 | |
| [prefer-export-from](docs/rules/prefer-export-from.md) | Prefer `export…from` when re-exporting. || 🔧 | 💡 |
| [prefer-includes](docs/rules/prefer-includes.md) | Prefer `.includes()` over `.indexOf()` and `Array#some()` when checking for existence or non-existence. || 🔧 | 💡 |
| [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) | Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. || 🔧 | |
| [prefer-math-trunc](docs/rules/prefer-math-trunc.md) | Enforce the use of `Math.trunc` instead of bitwise operators. || 🔧 | 💡 |
Expand Down
129 changes: 87 additions & 42 deletions rules/prefer-export-from.js
Expand Up @@ -5,9 +5,11 @@ const {
isClosingBraceToken,
} = require('eslint-utils');

const MESSAGE_ID = 'prefer-export-from';
const MESSAGE_ID_ERROR = 'error';
const MESSAGE_ID_SUGGESTION = 'suggestion';
const messages = {
[MESSAGE_ID]: 'Use `export…from` to re-export `{{exported}}`.',
[MESSAGE_ID_ERROR]: 'Use `export…from` to re-export `{{exported}}`.',
[MESSAGE_ID_SUGGESTION]: 'Switch to `export…from`.',
};

function * removeSpecifier(node, fixer, sourceCode) {
Expand Down Expand Up @@ -72,7 +74,7 @@ function * removeImportOrExport(node, fixer, sourceCode) {
}
}

function fix({
function getFixFunction({
context,
imported,
exported,
Expand Down Expand Up @@ -193,28 +195,19 @@ function isVariableUnused(node, context) {
&& references[0].identifier === node.id;
}

function * getProblems({
context,
variable,
program,
exportDeclarations,
}) {
const {identifiers, references} = variable;

if (identifiers.length !== 1 || references.length === 0) {
return;
}

const specifier = identifiers[0].parent;

const imported = {
function getImported(variable) {
const specifier = variable.identifiers[0].parent;
return {
name: getImportedName(specifier),
node: specifier,
declaration: specifier.parent,
variable,
};
}

for (const {identifier} of references) {
function getExports(imported, context) {
const exports = [];
for (const {identifier} of imported.variable.references) {
const exported = getExported(identifier, context);

if (!exported) {
Expand All @@ -233,44 +226,94 @@ function * getProblems({
continue;
}

yield {
node: exported.node,
messageId: MESSAGE_ID,
data: {
exported: exported.name,
},
fix: fix({
context,
imported,
exported,
exportDeclarations,
program,
}),
};
exports.push(exported);
}

return exports;
}

const schema = [
{
type: 'object',
additionalProperties: false,
properties: {
ignoreUsedVariables: {
type: 'boolean',
default: false,
},
},
},
];

/** @param {import('eslint').Rule.RuleContext} context */
function create(context) {
const variables = [];
const {ignoreUsedVariables} = {ignoreUsedVariables: false, ...context.options[0]};
const importDeclarations = new Set();
const exportDeclarations = [];

return {
'ImportDeclaration[specifiers.length>0]'(node) {
variables.push(...context.getDeclaredVariables(node));
importDeclarations.add(node);
},
// `ExportAllDeclaration` and `ExportDefaultDeclaration` can't be reused
'ExportNamedDeclaration[source.type="Literal"]'(node) {
exportDeclarations.push(node);
},
* 'Program:exit'(program) {
for (const variable of variables) {
yield * getProblems({
context,
variable,
exportDeclarations,
program,
});
for (const importDeclaration of importDeclarations) {
const variables = context.getDeclaredVariables(importDeclaration)
.map(variable => {
const imported = getImported(variable);
const exports = getExports(imported, context);

return {
variable,
imported,
exports,
};
});

if (
ignoreUsedVariables
&& variables.some(({variable, exports}) => variable.references.length !== exports.length)
) {
continue;
}

const shouldUseSuggestion = ignoreUsedVariables
&& variables.some(({variable}) => variable.references.length === 0);

for (const {imported, exports} of variables) {
for (const exported of exports) {
const problem = {
node: exported.node,
messageId: MESSAGE_ID_ERROR,
data: {
exported: exported.name,
},
};
const fix = getFixFunction({
context,
imported,
exported,
exportDeclarations,
program,
});

if (shouldUseSuggestion) {
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION,
fix,
},
];
} else {
problem.fix = fix;
}

yield problem;
}
}
}
},
};
Expand All @@ -284,6 +327,8 @@ module.exports = {
description: 'Prefer `export…from` when re-exporting.',
},
fixable: 'code',
hasSuggestions: true,
schema,
messages,
},
};
105 changes: 105 additions & 0 deletions test/prefer-export-from.mjs
Expand Up @@ -289,3 +289,108 @@ test.typescript({
],
invalid: [],
});

// `ignoreUsedVariables`
test.snapshot({
valid: [
outdent`
import defaultExport from 'foo';
use(defaultExport);
export default defaultExport;
`,
outdent`
import defaultExport from 'foo';
use(defaultExport);
export {defaultExport};
`,
outdent`
import {named} from 'foo';
use(named);
export {named};
`,
outdent`
import {named} from 'foo';
use(named);
export default named;
`,
outdent`
import * as namespace from 'foo';
use(namespace);
export {namespace};
`,
outdent`
import * as namespace from 'foo';
use(namespace);
export default namespace;
`,
outdent`
import * as namespace from 'foo';
export {namespace as default};
export {namespace as named};
`,
outdent`
import * as namespace from 'foo';
export default namespace;
export {namespace as named};
`,
outdent`
import defaultExport, {named} from 'foo';
use(defaultExport);
export {named};
`,
outdent`
import defaultExport, {named} from 'foo';
use(named);
export {defaultExport};
`,
outdent`
import {named1, named2} from 'foo';
use(named1);
export {named2};
`,
outdent`
import defaultExport, {named1, named2} from 'foo';
use(defaultExport);
export {named1, named2};
`,
outdent`
import defaultExport, {named1, named2} from 'foo';
use(named1);
export {defaultExport, named2};
`,
].map(code => ({code, options: [{ignoreUsedVariables: true}]})),
invalid: [
outdent`
import defaultExport from 'foo';
export {defaultExport as default};
export {defaultExport as named};
`,
outdent`
import {named} from 'foo';
export {named as default};
export {named as named};
`,
outdent`
import {named} from 'foo';
export default named;
export {named as named};
`,
outdent`
import defaultExport, {named} from 'foo';
export default defaultExport;
export {named};
`,
outdent`
import defaultExport, {named} from 'foo';
export {defaultExport as default, named};
`,
outdent`
import defaultExport from 'foo';
export const variable = defaultExport;
`,
outdent`
import {notUsedNotExported, exported} from 'foo';
export {exported};
`,
].map(code => ({code, options: [{ignoreUsedVariables: true}]})),
});

0 comments on commit a8d52e4

Please sign in to comment.