astro-ghostcms/.pnpm-store/v3/files/0b/6aa006c54b42f585de2b20834d5...

243 lines
11 KiB
Plaintext
Raw Normal View History

2024-02-14 19:45:06 +00:00
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("@typescript-eslint/utils");
const typescript_1 = require("typescript");
const util_1 = require("../util");
exports.default = (0, util_1.createRule)({
name: 'consistent-type-exports',
meta: {
type: 'suggestion',
docs: {
description: 'Enforce consistent usage of type exports',
requiresTypeChecking: true,
},
messages: {
typeOverValue: 'All exports in the declaration are only used as types. Use `export type`.',
singleExportIsType: 'Type export {{exportNames}} is not a value and should be exported using `export type`.',
multipleExportsAreTypes: 'Type exports {{exportNames}} are not values and should be exported using `export type`.',
},
schema: [
{
type: 'object',
properties: {
fixMixedExportsWithInlineTypeSpecifier: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
fixable: 'code',
},
defaultOptions: [
{
fixMixedExportsWithInlineTypeSpecifier: false,
},
],
create(context, [{ fixMixedExportsWithInlineTypeSpecifier }]) {
const sourceExportsMap = {};
const services = (0, util_1.getParserServices)(context);
/**
* Helper for identifying if an export specifier resolves to a
* JavaScript value or a TypeScript type.
*
* @returns True/false if is a type or not, or undefined if the specifier
* can't be resolved.
*/
function isSpecifierTypeBased(specifier) {
const checker = services.program.getTypeChecker();
const symbol = services.getSymbolAtLocation(specifier.exported);
if (!symbol) {
return undefined;
}
const aliasedSymbol = checker.getAliasedSymbol(symbol);
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
if (aliasedSymbol.escapedName === 'unknown') {
return undefined;
}
return !(aliasedSymbol.flags & typescript_1.SymbolFlags.Value);
}
return {
ExportNamedDeclaration(node) {
// Coerce the source into a string for use as a lookup entry.
const source = getSourceFromExport(node) ?? 'undefined';
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const sourceExports = (sourceExportsMap[source] ||= {
source,
reportValueExports: [],
typeOnlyNamedExport: null,
valueOnlyNamedExport: null,
});
// Cache the first encountered exports for the package. We will need to come
// back to these later when fixing the problems.
if (node.exportKind === 'type') {
if (sourceExports.typeOnlyNamedExport == null) {
// The export is a type export
sourceExports.typeOnlyNamedExport = node;
}
}
else if (sourceExports.valueOnlyNamedExport == null) {
// The export is a value export
sourceExports.valueOnlyNamedExport = node;
}
// Next for the current export, we will separate type/value specifiers.
const typeBasedSpecifiers = [];
const inlineTypeSpecifiers = [];
const valueSpecifiers = [];
// Note: it is valid to export values as types. We will avoid reporting errors
// when this is encountered.
if (node.exportKind !== 'type') {
for (const specifier of node.specifiers) {
if (specifier.exportKind === 'type') {
inlineTypeSpecifiers.push(specifier);
continue;
}
const isTypeBased = isSpecifierTypeBased(specifier);
if (isTypeBased === true) {
typeBasedSpecifiers.push(specifier);
}
else if (isTypeBased === false) {
// When isTypeBased is undefined, we should avoid reporting them.
valueSpecifiers.push(specifier);
}
}
}
if ((node.exportKind === 'value' && typeBasedSpecifiers.length) ||
(node.exportKind === 'type' && valueSpecifiers.length)) {
sourceExports.reportValueExports.push({
node,
typeBasedSpecifiers,
valueSpecifiers,
inlineTypeSpecifiers,
});
}
},
'Program:exit'() {
for (const sourceExports of Object.values(sourceExportsMap)) {
// If this export has no issues, move on.
if (sourceExports.reportValueExports.length === 0) {
continue;
}
for (const report of sourceExports.reportValueExports) {
if (report.valueSpecifiers.length === 0) {
// Export is all type-only with no type specifiers; convert the entire export to `export type`.
context.report({
node: report.node,
messageId: 'typeOverValue',
*fix(fixer) {
yield* fixExportInsertType(fixer, context.sourceCode, report.node);
},
});
continue;
}
// We have both type and value violations.
const allExportNames = report.typeBasedSpecifiers.map(specifier => specifier.local.name);
if (allExportNames.length === 1) {
const exportNames = allExportNames[0];
context.report({
node: report.node,
messageId: 'singleExportIsType',
data: { exportNames },
*fix(fixer) {
if (fixMixedExportsWithInlineTypeSpecifier) {
yield* fixAddTypeSpecifierToNamedExports(fixer, report);
}
else {
yield* fixSeparateNamedExports(fixer, context.sourceCode, report);
}
},
});
}
else {
const exportNames = (0, util_1.formatWordList)(allExportNames);
context.report({
node: report.node,
messageId: 'multipleExportsAreTypes',
data: { exportNames },
*fix(fixer) {
if (fixMixedExportsWithInlineTypeSpecifier) {
yield* fixAddTypeSpecifierToNamedExports(fixer, report);
}
else {
yield* fixSeparateNamedExports(fixer, context.sourceCode, report);
}
},
});
}
}
}
},
};
},
});
/**
* Inserts "type" into an export.
*
* Example:
*
* export type { Foo } from 'foo';
* ^^^^
*/
function* fixExportInsertType(fixer, sourceCode, node) {
const exportToken = (0, util_1.nullThrows)(sourceCode.getFirstToken(node), util_1.NullThrowsReasons.MissingToken('export', node.type));
yield fixer.insertTextAfter(exportToken, ' type');
for (const specifier of node.specifiers) {
if (specifier.exportKind === 'type') {
const kindToken = (0, util_1.nullThrows)(sourceCode.getFirstToken(specifier), util_1.NullThrowsReasons.MissingToken('export', specifier.type));
const firstTokenAfter = (0, util_1.nullThrows)(sourceCode.getTokenAfter(kindToken, {
includeComments: true,
}), 'Missing token following the export kind.');
yield fixer.removeRange([kindToken.range[0], firstTokenAfter.range[0]]);
}
}
}
/**
* Separates the exports which mismatch the kind of export the given
* node represents. For example, a type export's named specifiers which
* represent values will be inserted in a separate `export` statement.
*/
function* fixSeparateNamedExports(fixer, sourceCode, report) {
const { node, typeBasedSpecifiers, inlineTypeSpecifiers, valueSpecifiers } = report;
const typeSpecifiers = typeBasedSpecifiers.concat(inlineTypeSpecifiers);
const source = getSourceFromExport(node);
const specifierNames = typeSpecifiers.map(getSpecifierText).join(', ');
const exportToken = (0, util_1.nullThrows)(sourceCode.getFirstToken(node), util_1.NullThrowsReasons.MissingToken('export', node.type));
// Filter the bad exports from the current line.
const filteredSpecifierNames = valueSpecifiers
.map(getSpecifierText)
.join(', ');
const openToken = (0, util_1.nullThrows)(sourceCode.getFirstToken(node, util_1.isOpeningBraceToken), util_1.NullThrowsReasons.MissingToken('{', node.type));
const closeToken = (0, util_1.nullThrows)(sourceCode.getLastToken(node, util_1.isClosingBraceToken), util_1.NullThrowsReasons.MissingToken('}', node.type));
// Remove exports from the current line which we're going to re-insert.
yield fixer.replaceTextRange([openToken.range[1], closeToken.range[0]], ` ${filteredSpecifierNames} `);
// Insert the bad exports into a new export line above.
yield fixer.insertTextBefore(exportToken, `export type { ${specifierNames} }${source ? ` from '${source}'` : ''};\n`);
}
function* fixAddTypeSpecifierToNamedExports(fixer, report) {
if (report.node.exportKind === 'type') {
return;
}
for (const specifier of report.typeBasedSpecifiers) {
yield fixer.insertTextBefore(specifier, 'type ');
}
}
/**
* Returns the source of the export, or undefined if the named export has no source.
*/
function getSourceFromExport(node) {
if (node.source?.type === utils_1.AST_NODE_TYPES.Literal &&
typeof node.source.value === 'string') {
return node.source.value;
}
return undefined;
}
/**
* Returns the specifier text for the export. If it is aliased, we take care to return
* the proper formatting.
*/
function getSpecifierText(specifier) {
return `${specifier.local.name}${specifier.exported.name !== specifier.local.name
? ` as ${specifier.exported.name}`
: ''}`;
}
//# sourceMappingURL=consistent-type-exports.js.map