123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- 'use strict';
- const {
- isCommaToken,
- isOpeningBraceToken,
- isClosingBraceToken,
- } = require('@eslint-community/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`.',
- };
- // Default import/export can be `Identifier`, have to use `Symbol.for`
- const DEFAULT_SPECIFIER_NAME = Symbol.for('default');
- const NAMESPACE_SPECIFIER_NAME = Symbol('NAMESPACE_SPECIFIER_NAME');
- const getSpecifierName = node => {
- switch (node.type) {
- case 'Identifier': {
- return Symbol.for(node.name);
- }
- case 'Literal': {
- return node.value;
- }
- // No default
- }
- };
- const isTypeExport = specifier => specifier.exportKind === 'type' || specifier.parent.exportKind === 'type';
- const isTypeImport = specifier => specifier.importKind === 'type' || specifier.parent.importKind === 'type';
- 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 getSourceAndAssertionsText(declaration, sourceCode) {
- const keywordFromToken = sourceCode.getTokenBefore(
- declaration.source,
- token => token.type === 'Identifier' && token.value === 'from',
- );
- const [start] = keywordFromToken.range;
- const [, end] = declaration.range;
- return sourceCode.text.slice(start, end);
- }
- function getFixFunction({
- sourceCode,
- imported,
- exported,
- exportDeclarations,
- program,
- }) {
- const importDeclaration = imported.declaration;
- const sourceNode = importDeclaration.source;
- const sourceValue = sourceNode.value;
- const shouldExportAsType = imported.isTypeImport || exported.isTypeExport;
- let exportDeclaration;
- if (shouldExportAsType) {
- // If a type export declaration already exists, reuse it, else use a value export declaration with an inline type specifier.
- exportDeclaration = exportDeclarations.find(({source, exportKind}) => source.value === sourceValue && exportKind === 'type');
- }
- if (!exportDeclaration) {
- exportDeclaration = exportDeclarations.find(({source, exportKind}) => source.value === sourceValue && exportKind !== 'type');
- }
- /** @param {import('eslint').Rule.RuleFixer} fixer */
- return function * (fixer) {
- if (imported.name === NAMESPACE_SPECIFIER_NAME) {
- yield fixer.insertTextAfter(
- program,
- `\nexport * as ${exported.text} ${getSourceAndAssertionsText(importDeclaration, sourceCode)}`,
- );
- } else {
- let specifierText = exported.name === imported.name
- ? exported.text
- : `${imported.text} as ${exported.text}`;
- // Add an inline type specifier if the value is a type and the export deceleration is a value deceleration
- if (shouldExportAsType && (!exportDeclaration || exportDeclaration.exportKind !== 'type')) {
- specifierText = `type ${specifierText}`;
- }
- if (exportDeclaration) {
- const lastSpecifier = exportDeclaration.specifiers[exportDeclaration.specifiers.length - 1];
- // `export {} from 'foo';`
- if (lastSpecifier) {
- yield fixer.insertTextAfter(lastSpecifier, `, ${specifierText}`);
- } else {
- const openingBraceToken = sourceCode.getFirstToken(exportDeclaration, isOpeningBraceToken);
- yield fixer.insertTextAfter(openingBraceToken, specifierText);
- }
- } else {
- yield fixer.insertTextAfter(
- program,
- `\nexport {${specifierText}} ${getSourceAndAssertionsText(importDeclaration, sourceCode)}`,
- );
- }
- }
- if (imported.variable.references.length === 1) {
- yield * removeImportOrExport(imported.node, fixer, sourceCode);
- }
- yield * removeImportOrExport(exported.node, fixer, sourceCode);
- };
- }
- function getExported(identifier, context, sourceCode) {
- const {parent} = identifier;
- switch (parent.type) {
- case 'ExportDefaultDeclaration': {
- return {
- node: parent,
- name: DEFAULT_SPECIFIER_NAME,
- text: 'default',
- isTypeExport: isTypeExport(parent),
- };
- }
- case 'ExportSpecifier': {
- return {
- node: parent,
- name: getSpecifierName(parent.exported),
- text: sourceCode.getText(parent.exported),
- isTypeExport: isTypeExport(parent),
- };
- }
- 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: Symbol.for(parent.id.name),
- text: sourceCode.getText(parent.id),
- };
- }
- break;
- }
- // No default
- }
- }
- function isVariableUnused(node, context) {
- const variables = context.getDeclaredVariables(node);
- /* c8 ignore next 3 */
- 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, sourceCode) {
- const specifier = variable.defs[0].node;
- const result = {
- node: specifier,
- declaration: specifier.parent,
- variable,
- isTypeImport: isTypeImport(specifier),
- };
- switch (specifier.type) {
- case 'ImportDefaultSpecifier': {
- return {
- name: DEFAULT_SPECIFIER_NAME,
- text: 'default',
- ...result,
- };
- }
- case 'ImportSpecifier': {
- return {
- name: getSpecifierName(specifier.imported),
- text: sourceCode.getText(specifier.imported),
- ...result,
- };
- }
- case 'ImportNamespaceSpecifier': {
- return {
- name: NAMESPACE_SPECIFIER_NAME,
- text: '*',
- ...result,
- };
- }
- // No default
- }
- }
- function getExports(imported, context, sourceCode) {
- const exports = [];
- for (const {identifier} of imported.variable.references) {
- const exported = getExported(identifier, context, sourceCode);
- if (!exported) {
- continue;
- }
- /*
- There is no substitution for:
- ```js
- import * as foo from 'foo';
- export default foo;
- ```
- */
- if (imported.name === NAMESPACE_SPECIFIER_NAME && exported.name === DEFAULT_SPECIFIER_NAME) {
- 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 sourceCode = context.getSourceCode();
- 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) {
- let variables = context.getDeclaredVariables(importDeclaration);
- if (variables.some(variable => variable.defs.length !== 1 || variable.defs[0].parent !== importDeclaration)) {
- continue;
- }
- variables = variables.map(variable => {
- const imported = getImported(variable, sourceCode);
- const exports = getExports(imported, context, sourceCode);
- 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.text,
- },
- };
- const fix = getFixFunction({
- sourceCode,
- 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,
- },
- };
|