securityos/node_modules/eslint-plugin-unicorn/rules/prefer-export-from.js

407 lines
10 KiB
JavaScript
Raw Normal View History

2024-09-06 15:32:35 +00:00
'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,
},
};