prefer-export-from.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. 'use strict';
  2. const {
  3. isCommaToken,
  4. isOpeningBraceToken,
  5. isClosingBraceToken,
  6. } = require('eslint-utils');
  7. const MESSAGE_ID_ERROR = 'error';
  8. const MESSAGE_ID_SUGGESTION = 'suggestion';
  9. const messages = {
  10. [MESSAGE_ID_ERROR]: 'Use `export…from` to re-export `{{exported}}`.',
  11. [MESSAGE_ID_SUGGESTION]: 'Switch to `export…from`.',
  12. };
  13. function * removeSpecifier(node, fixer, sourceCode) {
  14. const {parent} = node;
  15. const {specifiers} = parent;
  16. if (specifiers.length === 1) {
  17. yield * removeImportOrExport(parent, fixer, sourceCode);
  18. return;
  19. }
  20. switch (node.type) {
  21. case 'ImportSpecifier': {
  22. const hasOtherSpecifiers = specifiers.some(specifier => specifier !== node && specifier.type === node.type);
  23. if (!hasOtherSpecifiers) {
  24. const closingBraceToken = sourceCode.getTokenAfter(node, isClosingBraceToken);
  25. // If there are other specifiers, they have to be the default import specifier
  26. // And the default import has to write before the named import specifiers
  27. // So there must be a comma before
  28. const commaToken = sourceCode.getTokenBefore(node, isCommaToken);
  29. yield fixer.replaceTextRange([commaToken.range[0], closingBraceToken.range[1]], '');
  30. return;
  31. }
  32. // Fallthrough
  33. }
  34. case 'ExportSpecifier':
  35. case 'ImportNamespaceSpecifier':
  36. case 'ImportDefaultSpecifier': {
  37. yield fixer.remove(node);
  38. const tokenAfter = sourceCode.getTokenAfter(node);
  39. if (isCommaToken(tokenAfter)) {
  40. yield fixer.remove(tokenAfter);
  41. }
  42. break;
  43. }
  44. // No default
  45. }
  46. }
  47. function * removeImportOrExport(node, fixer, sourceCode) {
  48. switch (node.type) {
  49. case 'ImportSpecifier':
  50. case 'ExportSpecifier':
  51. case 'ImportDefaultSpecifier':
  52. case 'ImportNamespaceSpecifier': {
  53. yield * removeSpecifier(node, fixer, sourceCode);
  54. return;
  55. }
  56. case 'ImportDeclaration':
  57. case 'ExportDefaultDeclaration':
  58. case 'ExportNamedDeclaration': {
  59. yield fixer.remove(node);
  60. }
  61. // No default
  62. }
  63. }
  64. function getFixFunction({
  65. context,
  66. imported,
  67. exported,
  68. exportDeclarations,
  69. program,
  70. }) {
  71. const sourceCode = context.getSourceCode();
  72. const sourceNode = imported.declaration.source;
  73. const sourceValue = sourceNode.value;
  74. const sourceText = sourceCode.getText(sourceNode);
  75. const exportDeclaration = exportDeclarations.find(({source}) => source.value === sourceValue);
  76. /** @param {import('eslint').Rule.RuleFixer} fixer */
  77. return function * (fixer) {
  78. if (imported.name === '*') {
  79. yield fixer.insertTextAfter(
  80. program,
  81. `\nexport * as ${exported.name} from ${sourceText};`,
  82. );
  83. } else {
  84. const specifier = exported.name === imported.name
  85. ? exported.name
  86. : `${imported.name} as ${exported.name}`;
  87. if (exportDeclaration) {
  88. const lastSpecifier = exportDeclaration.specifiers[exportDeclaration.specifiers.length - 1];
  89. // `export {} from 'foo';`
  90. if (lastSpecifier) {
  91. yield fixer.insertTextAfter(lastSpecifier, `, ${specifier}`);
  92. } else {
  93. const openingBraceToken = sourceCode.getFirstToken(exportDeclaration, isOpeningBraceToken);
  94. yield fixer.insertTextAfter(openingBraceToken, specifier);
  95. }
  96. } else {
  97. yield fixer.insertTextAfter(
  98. program,
  99. `\nexport {${specifier}} from ${sourceText};`,
  100. );
  101. }
  102. }
  103. if (imported.variable.references.length === 1) {
  104. yield * removeImportOrExport(imported.node, fixer, sourceCode);
  105. }
  106. yield * removeImportOrExport(exported.node, fixer, sourceCode);
  107. };
  108. }
  109. function getImportedName(specifier) {
  110. switch (specifier.type) {
  111. case 'ImportDefaultSpecifier':
  112. return 'default';
  113. case 'ImportSpecifier':
  114. return specifier.imported.name;
  115. case 'ImportNamespaceSpecifier':
  116. return '*';
  117. // No default
  118. }
  119. }
  120. function getExported(identifier, context) {
  121. const {parent} = identifier;
  122. switch (parent.type) {
  123. case 'ExportDefaultDeclaration':
  124. return {
  125. node: parent,
  126. name: 'default',
  127. };
  128. case 'ExportSpecifier':
  129. return {
  130. node: parent,
  131. name: parent.exported.name,
  132. };
  133. case 'VariableDeclarator': {
  134. if (
  135. parent.init === identifier
  136. && parent.id.type === 'Identifier'
  137. && !parent.id.typeAnnotation
  138. && parent.parent.type === 'VariableDeclaration'
  139. && parent.parent.kind === 'const'
  140. && parent.parent.declarations.length === 1
  141. && parent.parent.declarations[0] === parent
  142. && parent.parent.parent.type === 'ExportNamedDeclaration'
  143. && isVariableUnused(parent, context)
  144. ) {
  145. return {
  146. node: parent.parent.parent,
  147. name: parent.id.name,
  148. };
  149. }
  150. break;
  151. }
  152. // No default
  153. }
  154. }
  155. function isVariableUnused(node, context) {
  156. const variables = context.getDeclaredVariables(node);
  157. /* istanbul ignore next */
  158. if (variables.length !== 1) {
  159. return false;
  160. }
  161. const [{identifiers, references}] = variables;
  162. return identifiers.length === 1
  163. && identifiers[0] === node.id
  164. && references.length === 1
  165. && references[0].identifier === node.id;
  166. }
  167. function getImported(variable) {
  168. const specifier = variable.identifiers[0].parent;
  169. return {
  170. name: getImportedName(specifier),
  171. node: specifier,
  172. declaration: specifier.parent,
  173. variable,
  174. };
  175. }
  176. function getExports(imported, context) {
  177. const exports = [];
  178. for (const {identifier} of imported.variable.references) {
  179. const exported = getExported(identifier, context);
  180. if (!exported) {
  181. continue;
  182. }
  183. /*
  184. There is no substitution for:
  185. ```js
  186. import * as foo from 'foo';
  187. export default foo;
  188. ```
  189. */
  190. if (imported.name === '*' && exported.name === 'default') {
  191. continue;
  192. }
  193. exports.push(exported);
  194. }
  195. return exports;
  196. }
  197. const schema = [
  198. {
  199. type: 'object',
  200. additionalProperties: false,
  201. properties: {
  202. ignoreUsedVariables: {
  203. type: 'boolean',
  204. default: false,
  205. },
  206. },
  207. },
  208. ];
  209. /** @param {import('eslint').Rule.RuleContext} context */
  210. function create(context) {
  211. const {ignoreUsedVariables} = {ignoreUsedVariables: false, ...context.options[0]};
  212. const importDeclarations = new Set();
  213. const exportDeclarations = [];
  214. return {
  215. 'ImportDeclaration[specifiers.length>0]'(node) {
  216. importDeclarations.add(node);
  217. },
  218. // `ExportAllDeclaration` and `ExportDefaultDeclaration` can't be reused
  219. 'ExportNamedDeclaration[source.type="Literal"]'(node) {
  220. exportDeclarations.push(node);
  221. },
  222. * 'Program:exit'(program) {
  223. for (const importDeclaration of importDeclarations) {
  224. const variables = context.getDeclaredVariables(importDeclaration)
  225. .map(variable => {
  226. const imported = getImported(variable);
  227. const exports = getExports(imported, context);
  228. return {
  229. variable,
  230. imported,
  231. exports,
  232. };
  233. });
  234. if (
  235. ignoreUsedVariables
  236. && variables.some(({variable, exports}) => variable.references.length !== exports.length)
  237. ) {
  238. continue;
  239. }
  240. const shouldUseSuggestion = ignoreUsedVariables
  241. && variables.some(({variable}) => variable.references.length === 0);
  242. for (const {imported, exports} of variables) {
  243. for (const exported of exports) {
  244. const problem = {
  245. node: exported.node,
  246. messageId: MESSAGE_ID_ERROR,
  247. data: {
  248. exported: exported.name,
  249. },
  250. };
  251. const fix = getFixFunction({
  252. context,
  253. imported,
  254. exported,
  255. exportDeclarations,
  256. program,
  257. });
  258. if (shouldUseSuggestion) {
  259. problem.suggest = [
  260. {
  261. messageId: MESSAGE_ID_SUGGESTION,
  262. fix,
  263. },
  264. ];
  265. } else {
  266. problem.fix = fix;
  267. }
  268. yield problem;
  269. }
  270. }
  271. }
  272. },
  273. };
  274. }
  275. /** @type {import('eslint').Rule.RuleModule} */
  276. module.exports = {
  277. create,
  278. meta: {
  279. type: 'suggestion',
  280. docs: {
  281. description: 'Prefer `export…from` when re-exporting.',
  282. },
  283. fixable: 'code',
  284. hasSuggestions: true,
  285. schema,
  286. messages,
  287. },
  288. };