UNPKG

12.3 kBJavaScriptView Raw
1"use strict";
2var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 if (k2 === undefined) k2 = k;
4 Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5}) : (function(o, m, k, k2) {
6 if (k2 === undefined) k2 = k;
7 o[k2] = m[k];
8}));
9var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10 Object.defineProperty(o, "default", { enumerable: true, value: v });
11}) : function(o, v) {
12 o["default"] = v;
13});
14var __importStar = (this && this.__importStar) || function (mod) {
15 if (mod && mod.__esModule) return mod;
16 var result = {};
17 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18 __setModuleDefault(result, mod);
19 return result;
20};
21Object.defineProperty(exports, "__esModule", { value: true });
22const utils_1 = require("@typescript-eslint/utils");
23const typescript_1 = require("typescript");
24const util = __importStar(require("../util"));
25exports.default = util.createRule({
26 name: 'consistent-type-exports',
27 meta: {
28 type: 'suggestion',
29 docs: {
30 description: 'Enforces consistent usage of type exports',
31 recommended: false,
32 requiresTypeChecking: true,
33 },
34 messages: {
35 typeOverValue: 'All exports in the declaration are only used as types. Use `export type`.',
36 singleExportIsType: 'Type export {{exportNames}} is not a value and should be exported using `export type`.',
37 multipleExportsAreTypes: 'Type exports {{exportNames}} are not values and should be exported using `export type`.',
38 },
39 schema: [
40 {
41 type: 'object',
42 properties: {
43 fixMixedExportsWithInlineTypeSpecifier: {
44 type: 'boolean',
45 },
46 },
47 additionalProperties: false,
48 },
49 ],
50 fixable: 'code',
51 },
52 defaultOptions: [
53 {
54 fixMixedExportsWithInlineTypeSpecifier: false,
55 },
56 ],
57 create(context, [{ fixMixedExportsWithInlineTypeSpecifier }]) {
58 const sourceCode = context.getSourceCode();
59 const sourceExportsMap = {};
60 const parserServices = util.getParserServices(context);
61 return {
62 ExportNamedDeclaration(node) {
63 var _a;
64 // Coerce the source into a string for use as a lookup entry.
65 const source = (_a = getSourceFromExport(node)) !== null && _a !== void 0 ? _a : 'undefined';
66 const sourceExports = (sourceExportsMap[source] || (sourceExportsMap[source] = {
67 source,
68 reportValueExports: [],
69 typeOnlyNamedExport: null,
70 valueOnlyNamedExport: null,
71 }));
72 // Cache the first encountered exports for the package. We will need to come
73 // back to these later when fixing the problems.
74 if (node.exportKind === 'type') {
75 if (sourceExports.typeOnlyNamedExport == null) {
76 // The export is a type export
77 sourceExports.typeOnlyNamedExport = node;
78 }
79 }
80 else if (sourceExports.valueOnlyNamedExport == null) {
81 // The export is a value export
82 sourceExports.valueOnlyNamedExport = node;
83 }
84 // Next for the current export, we will separate type/value specifiers.
85 const typeBasedSpecifiers = [];
86 const inlineTypeSpecifiers = [];
87 const valueSpecifiers = [];
88 // Note: it is valid to export values as types. We will avoid reporting errors
89 // when this is encountered.
90 if (node.exportKind !== 'type') {
91 for (const specifier of node.specifiers) {
92 if (specifier.exportKind === 'type') {
93 inlineTypeSpecifiers.push(specifier);
94 continue;
95 }
96 const isTypeBased = isSpecifierTypeBased(parserServices, specifier);
97 if (isTypeBased === true) {
98 typeBasedSpecifiers.push(specifier);
99 }
100 else if (isTypeBased === false) {
101 // When isTypeBased is undefined, we should avoid reporting them.
102 valueSpecifiers.push(specifier);
103 }
104 }
105 }
106 if ((node.exportKind === 'value' && typeBasedSpecifiers.length) ||
107 (node.exportKind === 'type' && valueSpecifiers.length)) {
108 sourceExports.reportValueExports.push({
109 node,
110 typeBasedSpecifiers,
111 valueSpecifiers,
112 inlineTypeSpecifiers,
113 });
114 }
115 },
116 'Program:exit'() {
117 for (const sourceExports of Object.values(sourceExportsMap)) {
118 // If this export has no issues, move on.
119 if (sourceExports.reportValueExports.length === 0) {
120 continue;
121 }
122 for (const report of sourceExports.reportValueExports) {
123 if (report.valueSpecifiers.length === 0) {
124 // Export is all type-only with no type specifiers; convert the entire export to `export type`.
125 context.report({
126 node: report.node,
127 messageId: 'typeOverValue',
128 *fix(fixer) {
129 yield* fixExportInsertType(fixer, sourceCode, report.node);
130 },
131 });
132 continue;
133 }
134 // We have both type and value violations.
135 const allExportNames = report.typeBasedSpecifiers.map(specifier => `${specifier.local.name}`);
136 if (allExportNames.length === 1) {
137 const exportNames = allExportNames[0];
138 context.report({
139 node: report.node,
140 messageId: 'singleExportIsType',
141 data: { exportNames },
142 *fix(fixer) {
143 if (fixMixedExportsWithInlineTypeSpecifier) {
144 yield* fixAddTypeSpecifierToNamedExports(fixer, report);
145 }
146 else {
147 yield* fixSeparateNamedExports(fixer, sourceCode, report);
148 }
149 },
150 });
151 }
152 else {
153 const exportNames = util.formatWordList(allExportNames);
154 context.report({
155 node: report.node,
156 messageId: 'multipleExportsAreTypes',
157 data: { exportNames },
158 *fix(fixer) {
159 if (fixMixedExportsWithInlineTypeSpecifier) {
160 yield* fixAddTypeSpecifierToNamedExports(fixer, report);
161 }
162 else {
163 yield* fixSeparateNamedExports(fixer, sourceCode, report);
164 }
165 },
166 });
167 }
168 }
169 }
170 },
171 };
172 },
173});
174/**
175 * Helper for identifying if an export specifier resolves to a
176 * JavaScript value or a TypeScript type.
177 *
178 * @returns True/false if is a type or not, or undefined if the specifier
179 * can't be resolved.
180 */
181function isSpecifierTypeBased(parserServices, specifier) {
182 const checker = parserServices.program.getTypeChecker();
183 const node = parserServices.esTreeNodeToTSNodeMap.get(specifier.exported);
184 const symbol = checker.getSymbolAtLocation(node);
185 const aliasedSymbol = checker.getAliasedSymbol(symbol);
186 if (!aliasedSymbol || aliasedSymbol.escapedName === 'unknown') {
187 return undefined;
188 }
189 return !(aliasedSymbol.flags & typescript_1.SymbolFlags.Value);
190}
191/**
192 * Inserts "type" into an export.
193 *
194 * Example:
195 *
196 * export type { Foo } from 'foo';
197 * ^^^^
198 */
199function* fixExportInsertType(fixer, sourceCode, node) {
200 const exportToken = util.nullThrows(sourceCode.getFirstToken(node), util.NullThrowsReasons.MissingToken('export', node.type));
201 yield fixer.insertTextAfter(exportToken, ' type');
202 for (const specifier of node.specifiers) {
203 if (specifier.exportKind === 'type') {
204 const kindToken = util.nullThrows(sourceCode.getFirstToken(specifier), util.NullThrowsReasons.MissingToken('export', specifier.type));
205 const firstTokenAfter = util.nullThrows(sourceCode.getTokenAfter(kindToken, {
206 includeComments: true,
207 }), 'Missing token following the export kind.');
208 yield fixer.removeRange([kindToken.range[0], firstTokenAfter.range[0]]);
209 }
210 }
211}
212/**
213 * Separates the exports which mismatch the kind of export the given
214 * node represents. For example, a type export's named specifiers which
215 * represent values will be inserted in a separate `export` statement.
216 */
217function* fixSeparateNamedExports(fixer, sourceCode, report) {
218 const { node, typeBasedSpecifiers, inlineTypeSpecifiers, valueSpecifiers } = report;
219 const typeSpecifiers = typeBasedSpecifiers.concat(inlineTypeSpecifiers);
220 const source = getSourceFromExport(node);
221 const specifierNames = typeSpecifiers.map(getSpecifierText).join(', ');
222 const exportToken = util.nullThrows(sourceCode.getFirstToken(node), util.NullThrowsReasons.MissingToken('export', node.type));
223 // Filter the bad exports from the current line.
224 const filteredSpecifierNames = valueSpecifiers
225 .map(getSpecifierText)
226 .join(', ');
227 const openToken = util.nullThrows(sourceCode.getFirstToken(node, util.isOpeningBraceToken), util.NullThrowsReasons.MissingToken('{', node.type));
228 const closeToken = util.nullThrows(sourceCode.getLastToken(node, util.isClosingBraceToken), util.NullThrowsReasons.MissingToken('}', node.type));
229 // Remove exports from the current line which we're going to re-insert.
230 yield fixer.replaceTextRange([openToken.range[1], closeToken.range[0]], ` ${filteredSpecifierNames} `);
231 // Insert the bad exports into a new export line above.
232 yield fixer.insertTextBefore(exportToken, `export type { ${specifierNames} }${source ? ` from '${source}'` : ''};\n`);
233}
234function* fixAddTypeSpecifierToNamedExports(fixer, report) {
235 if (report.node.exportKind === 'type') {
236 return;
237 }
238 for (const specifier of report.typeBasedSpecifiers) {
239 yield fixer.insertTextBefore(specifier, 'type ');
240 }
241}
242/**
243 * Returns the source of the export, or undefined if the named export has no source.
244 */
245function getSourceFromExport(node) {
246 var _a;
247 if (((_a = node.source) === null || _a === void 0 ? void 0 : _a.type) === utils_1.AST_NODE_TYPES.Literal &&
248 typeof node.source.value === 'string') {
249 return node.source.value;
250 }
251 return undefined;
252}
253/**
254 * Returns the specifier text for the export. If it is aliased, we take care to return
255 * the proper formatting.
256 */
257function getSpecifierText(specifier) {
258 return `${specifier.local.name}${specifier.exported.name !== specifier.local.name
259 ? ` as ${specifier.exported.name}`
260 : ''}`;
261}
262//# sourceMappingURL=consistent-type-exports.js.map
\No newline at end of file