123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- 'use strict';
- const {
- isCommaToken,
- isOpeningBraceToken,
- isClosingBraceToken,
- } = require('eslint-utils');
- const MESSAGE_ID_ERROR = 'error';
- const MESSAGE_ID_SUGGESTION = 'suggestion';
- const messages = {
- [MESSAGE_ID_ERROR]: 'Use `export…from` to re-export `{{exported}}`.',
- [MESSAGE_ID_SUGGESTION]: 'Switch to `export…from`.',
- };
- function * removeSpecifier(node, fixer, sourceCode) {
- const {parent} = node;
- const {specifiers} = parent;
- if (specifiers.length === 1) {
- yield * removeImportOrExport(parent, fixer, sourceCode);
- return;
- }
- switch (node.type) {
- case 'ImportSpecifier': {
- const hasOtherSpecifiers = specifiers.some(specifier => specifier !== node && specifier.type === node.type);
- if (!hasOtherSpecifiers) {
- const closingBraceToken = sourceCode.getTokenAfter(node, isClosingBraceToken);
- // If there are other specifiers, they have to be the default import specifier
- // And the default import has to write before the named import specifiers
- // So there must be a comma before
- const commaToken = sourceCode.getTokenBefore(node, isCommaToken);
- yield fixer.replaceTextRange([commaToken.range[0], closingBraceToken.range[1]], '');
- return;
- }
- // Fallthrough
- }
- case 'ExportSpecifier':
- case 'ImportNamespaceSpecifier':
- case 'ImportDefaultSpecifier': {
- yield fixer.remove(node);
- const tokenAfter = sourceCode.getTokenAfter(node);
- if (isCommaToken(tokenAfter)) {
- yield fixer.remove(tokenAfter);
- }
- break;
- }
- // No default
- }
- }
- function * removeImportOrExport(node, fixer, sourceCode) {
- switch (node.type) {
- case 'ImportSpecifier':
- case 'ExportSpecifier':
- case 'ImportDefaultSpecifier':
- case 'ImportNamespaceSpecifier': {
- yield * removeSpecifier(node, fixer, sourceCode);
- return;
- }
- case 'ImportDeclaration':
- case 'ExportDefaultDeclaration':
- case 'ExportNamedDeclaration': {
- yield fixer.remove(node);
- }
- // No default
- }
- }
- function getFixFunction({
- context,
- imported,
- exported,
- exportDeclarations,
- program,
- }) {
- const sourceCode = context.getSourceCode();
- const sourceNode = imported.declaration.source;
- const sourceValue = sourceNode.value;
- const sourceText = sourceCode.getText(sourceNode);
- const exportDeclaration = exportDeclarations.find(({source}) => source.value === sourceValue);
- /** @param {import('eslint').Rule.RuleFixer} fixer */
- return function * (fixer) {
- if (imported.name === '*') {
- yield fixer.insertTextAfter(
- program,
- `\nexport * as ${exported.name} from ${sourceText};`,
- );
- } else {
- const specifier = exported.name === imported.name
- ? exported.name
- : `${imported.name} as ${exported.name}`;
- if (exportDeclaration) {
- const lastSpecifier = exportDeclaration.specifiers[exportDeclaration.specifiers.length - 1];
- // `export {} from 'foo';`
- if (lastSpecifier) {
- yield fixer.insertTextAfter(lastSpecifier, `, ${specifier}`);
- } else {
- const openingBraceToken = sourceCode.getFirstToken(exportDeclaration, isOpeningBraceToken);
- yield fixer.insertTextAfter(openingBraceToken, specifier);
- }
- } else {
- yield fixer.insertTextAfter(
- program,
- `\nexport {${specifier}} from ${sourceText};`,
- );
- }
- }
- if (imported.variable.references.length === 1) {
- yield * removeImportOrExport(imported.node, fixer, sourceCode);
- }
- yield * removeImportOrExport(exported.node, fixer, sourceCode);
- };
- }
- function getImportedName(specifier) {
- switch (specifier.type) {
- case 'ImportDefaultSpecifier':
- return 'default';
- case 'ImportSpecifier':
- return specifier.imported.name;
- case 'ImportNamespaceSpecifier':
- return '*';
- // No default
- }
- }
- function getExported(identifier, context) {
- const {parent} = identifier;
- switch (parent.type) {
- case 'ExportDefaultDeclaration':
- return {
- node: parent,
- name: 'default',
- };
- case 'ExportSpecifier':
- return {
- node: parent,
- name: parent.exported.name,
- };
- case 'VariableDeclarator': {
- if (
- parent.init === identifier
- && parent.id.type === 'Identifier'
- && !parent.id.typeAnnotation
- && parent.parent.type === 'VariableDeclaration'
- && parent.parent.kind === 'const'
- && parent.parent.declarations.length === 1
- && parent.parent.declarations[0] === parent
- && parent.parent.parent.type === 'ExportNamedDeclaration'
- && isVariableUnused(parent, context)
- ) {
- return {
- node: parent.parent.parent,
- name: parent.id.name,
- };
- }
- break;
- }
- // No default
- }
- }
- function isVariableUnused(node, context) {
- const variables = context.getDeclaredVariables(node);
- /* istanbul ignore next */
- if (variables.length !== 1) {
- return false;
- }
- const [{identifiers, references}] = variables;
- return identifiers.length === 1
- && identifiers[0] === node.id
- && references.length === 1
- && references[0].identifier === node.id;
- }
- function getImported(variable) {
- const specifier = variable.identifiers[0].parent;
- return {
- name: getImportedName(specifier),
- node: specifier,
- declaration: specifier.parent,
- variable,
- };
- }
- function getExports(imported, context) {
- const exports = [];
- for (const {identifier} of imported.variable.references) {
- const exported = getExported(identifier, context);
- if (!exported) {
- continue;
- }
- /*
- There is no substitution for:
- ```js
- import * as foo from 'foo';
- export default foo;
- ```
- */
- if (imported.name === '*' && exported.name === 'default') {
- continue;
- }
- 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 {ignoreUsedVariables} = {ignoreUsedVariables: false, ...context.options[0]};
- const importDeclarations = new Set();
- const exportDeclarations = [];
- return {
- 'ImportDeclaration[specifiers.length>0]'(node) {
- importDeclarations.add(node);
- },
- // `ExportAllDeclaration` and `ExportDefaultDeclaration` can't be reused
- 'ExportNamedDeclaration[source.type="Literal"]'(node) {
- exportDeclarations.push(node);
- },
- * 'Program:exit'(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;
- }
- }
- }
- },
- };
- }
- /** @type {import('eslint').Rule.RuleModule} */
- module.exports = {
- create,
- meta: {
- type: 'suggestion',
- docs: {
- description: 'Prefer `export…from` when re-exporting.',
- },
- fixable: 'code',
- hasSuggestions: true,
- schema,
- messages,
- },
- };
|