| 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,	},};
 |