diff --git a/docs/rules/prefer-export-from.md b/docs/rules/prefer-export-from.md index ec2d8b9959..c1b6f655ac 100644 --- a/docs/rules/prefer-export-from.md +++ b/docs/rules/prefer-export-from.md @@ -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. @@ -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}; +``` diff --git a/readme.md b/readme.md index 559a14351d..2cda9839d1 100644 --- a/readme.md +++ b/readme.md @@ -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. | ✅ | 🔧 | 💡 | diff --git a/rules/prefer-export-from.js b/rules/prefer-export-from.js index 028ff61281..347d40f95d 100644 --- a/rules/prefer-export-from.js +++ b/rules/prefer-export-from.js @@ -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) { @@ -72,7 +74,7 @@ function * removeImportOrExport(node, fixer, sourceCode) { } } -function fix({ +function getFixFunction({ context, imported, exported, @@ -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) { @@ -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; + } + } } }, }; @@ -284,6 +327,8 @@ module.exports = { description: 'Prefer `export…from` when re-exporting.', }, fixable: 'code', + hasSuggestions: true, + schema, messages, }, }; diff --git a/test/prefer-export-from.mjs b/test/prefer-export-from.mjs index f9a552f3f1..5a963c8213 100644 --- a/test/prefer-export-from.mjs +++ b/test/prefer-export-from.mjs @@ -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}]})), +}); diff --git a/test/snapshots/prefer-export-from.mjs.md b/test/snapshots/prefer-export-from.mjs.md index af4c102aae..08ebf6ebf0 100644 --- a/test/snapshots/prefer-export-from.mjs.md +++ b/test/snapshots/prefer-export-from.mjs.md @@ -1081,3 +1081,267 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^ Use \`export…from\` to re-export \`namespace\`.␊ 3 | export default namespace;␊ ` + +## Invalid #1 + 1 | import defaultExport from 'foo'; + 2 | export {defaultExport as default}; + 3 | export {defaultExport as named}; + +> Options + + `␊ + [␊ + {␊ + "ignoreUsedVariables": true␊ + }␊ + ]␊ + ` + +> Output + + `␊ + 1 |␊ + 2 |␊ + 3 |␊ + 4 | export {default, default as named} from 'foo';␊ + ` + +> Error 1/2 + + `␊ + 1 | import defaultExport from 'foo';␊ + > 2 | export {defaultExport as default};␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^ Use \`export…from\` to re-export \`default\`.␊ + 3 | export {defaultExport as named};␊ + ` + +> Error 2/2 + + `␊ + 1 | import defaultExport from 'foo';␊ + 2 | export {defaultExport as default};␊ + > 3 | export {defaultExport as named};␊ + | ^^^^^^^^^^^^^^^^^^^^^^ Use \`export…from\` to re-export \`named\`.␊ + ` + +## Invalid #2 + 1 | import {named} from 'foo'; + 2 | export {named as default}; + 3 | export {named as named}; + +> Options + + `␊ + [␊ + {␊ + "ignoreUsedVariables": true␊ + }␊ + ]␊ + ` + +> Output + + `␊ + 1 |␊ + 2 |␊ + 3 |␊ + 4 | export {named as default, named} from 'foo';␊ + ` + +> Error 1/2 + + `␊ + 1 | import {named} from 'foo';␊ + > 2 | export {named as default};␊ + | ^^^^^^^^^^^^^^^^ Use \`export…from\` to re-export \`default\`.␊ + 3 | export {named as named};␊ + ` + +> Error 2/2 + + `␊ + 1 | import {named} from 'foo';␊ + 2 | export {named as default};␊ + > 3 | export {named as named};␊ + | ^^^^^^^^^^^^^^ Use \`export…from\` to re-export \`named\`.␊ + ` + +## Invalid #3 + 1 | import {named} from 'foo'; + 2 | export default named; + 3 | export {named as named}; + +> Options + + `␊ + [␊ + {␊ + "ignoreUsedVariables": true␊ + }␊ + ]␊ + ` + +> Output + + `␊ + 1 |␊ + 2 |␊ + 3 |␊ + 4 | export {named as default, named} from 'foo';␊ + ` + +> Error 1/2 + + `␊ + 1 | import {named} from 'foo';␊ + > 2 | export default named;␊ + | ^^^^^^^^^^^^^^^^^^^^^ Use \`export…from\` to re-export \`default\`.␊ + 3 | export {named as named};␊ + ` + +> Error 2/2 + + `␊ + 1 | import {named} from 'foo';␊ + 2 | export default named;␊ + > 3 | export {named as named};␊ + | ^^^^^^^^^^^^^^ Use \`export…from\` to re-export \`named\`.␊ + ` + +## Invalid #4 + 1 | import defaultExport, {named} from 'foo'; + 2 | export default defaultExport; + 3 | export {named}; + +> Options + + `␊ + [␊ + {␊ + "ignoreUsedVariables": true␊ + }␊ + ]␊ + ` + +> Output + + `␊ + 1 |␊ + 2 |␊ + 3 |␊ + 4 | export {default, named} from 'foo';␊ + ` + +> Error 1/2 + + `␊ + 1 | import defaultExport, {named} from 'foo';␊ + > 2 | export default defaultExport;␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use \`export…from\` to re-export \`default\`.␊ + 3 | export {named};␊ + ` + +> Error 2/2 + + `␊ + 1 | import defaultExport, {named} from 'foo';␊ + 2 | export default defaultExport;␊ + > 3 | export {named};␊ + | ^^^^^ Use \`export…from\` to re-export \`named\`.␊ + ` + +## Invalid #5 + 1 | import defaultExport, {named} from 'foo'; + 2 | export {defaultExport as default, named}; + +> Options + + `␊ + [␊ + {␊ + "ignoreUsedVariables": true␊ + }␊ + ]␊ + ` + +> Output + + `␊ + 1 |␊ + 2 |␊ + 3 | export {default, named} from 'foo';␊ + ` + +> Error 1/2 + + `␊ + 1 | import defaultExport, {named} from 'foo';␊ + > 2 | export {defaultExport as default, named};␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^ Use \`export…from\` to re-export \`default\`.␊ + ` + +> Error 2/2 + + `␊ + 1 | import defaultExport, {named} from 'foo';␊ + > 2 | export {defaultExport as default, named};␊ + | ^^^^^ Use \`export…from\` to re-export \`named\`.␊ + ` + +## Invalid #6 + 1 | import defaultExport from 'foo'; + 2 | export const variable = defaultExport; + +> Options + + `␊ + [␊ + {␊ + "ignoreUsedVariables": true␊ + }␊ + ]␊ + ` + +> Output + + `␊ + 1 |␊ + 2 |␊ + 3 | export {default as variable} from 'foo';␊ + ` + +> Error 1/1 + + `␊ + 1 | import defaultExport from 'foo';␊ + > 2 | export const variable = defaultExport;␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use \`export…from\` to re-export \`variable\`.␊ + ` + +## Invalid #7 + 1 | import {notUsedNotExported, exported} from 'foo'; + 2 | export {exported}; + +> Options + + `␊ + [␊ + {␊ + "ignoreUsedVariables": true␊ + }␊ + ]␊ + ` + +> Error 1/1 + + `␊ + 1 | import {notUsedNotExported, exported} from 'foo';␊ + > 2 | export {exported};␊ + | ^^^^^^^^ Use \`export…from\` to re-export \`exported\`.␊ + ␊ + --------------------------------------------------------------------------------␊ + Suggestion 1/1: Switch to \`export…from\`.␊ + 1 | import {notUsedNotExported, } from 'foo';␊ + 2 |␊ + 3 | export {exported} from 'foo';␊ + ` diff --git a/test/snapshots/prefer-export-from.mjs.snap b/test/snapshots/prefer-export-from.mjs.snap index 61201efcd9..c280c8f0ee 100644 Binary files a/test/snapshots/prefer-export-from.mjs.snap and b/test/snapshots/prefer-export-from.mjs.snap differ