prefer-top-level-await.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. 'use strict';
  2. const {findVariable, getFunctionHeadLocation} = require('eslint-utils');
  3. const {matches, memberExpressionSelector} = require('./selectors/index.js');
  4. const ERROR_PROMISE = 'promise';
  5. const ERROR_IIFE = 'iife';
  6. const ERROR_IDENTIFIER = 'identifier';
  7. const SUGGESTION_ADD_AWAIT = 'add-await';
  8. const messages = {
  9. [ERROR_PROMISE]: 'Prefer top-level await over using a promise chain.',
  10. [ERROR_IIFE]: 'Prefer top-level await over an async IIFE.',
  11. [ERROR_IDENTIFIER]: 'Prefer top-level await over an async function `{{name}}` call.',
  12. [SUGGESTION_ADD_AWAIT]: 'Insert `await`.',
  13. };
  14. const promiseMethods = ['then', 'catch', 'finally'];
  15. const topLevelCallExpression = 'CallExpression:not(:function *)';
  16. const iife = [
  17. topLevelCallExpression,
  18. matches([
  19. '[callee.type="FunctionExpression"]',
  20. '[callee.type="ArrowFunctionExpression"]',
  21. ]),
  22. '[callee.async!=false]',
  23. '[callee.generator!=true]',
  24. ].join('');
  25. const promise = [
  26. topLevelCallExpression,
  27. memberExpressionSelector({
  28. path: 'callee',
  29. properties: promiseMethods,
  30. includeOptional: true,
  31. }),
  32. ].join('');
  33. const identifier = [
  34. topLevelCallExpression,
  35. '[callee.type="Identifier"]',
  36. ].join('');
  37. const isPromiseMethodCalleeObject = node =>
  38. node.parent.type === 'MemberExpression'
  39. && node.parent.object === node
  40. && !node.parent.computed
  41. && node.parent.property.type === 'Identifier'
  42. && promiseMethods.includes(node.parent.property.name)
  43. && node.parent.parent.type === 'CallExpression'
  44. && node.parent.parent.callee === node.parent;
  45. const isAwaitArgument = node => {
  46. if (node.parent.type === 'ChainExpression') {
  47. node = node.parent;
  48. }
  49. return node.parent.type === 'AwaitExpression' && node.parent.argument === node;
  50. };
  51. /** @param {import('eslint').Rule.RuleContext} context */
  52. function create(context) {
  53. return {
  54. [promise](node) {
  55. if (isPromiseMethodCalleeObject(node) || isAwaitArgument(node)) {
  56. return;
  57. }
  58. return {
  59. node: node.callee.property,
  60. messageId: ERROR_PROMISE,
  61. };
  62. },
  63. [iife](node) {
  64. if (isPromiseMethodCalleeObject(node) || isAwaitArgument(node)) {
  65. return;
  66. }
  67. return {
  68. node,
  69. loc: getFunctionHeadLocation(node.callee, context.getSourceCode()),
  70. messageId: ERROR_IIFE,
  71. };
  72. },
  73. [identifier](node) {
  74. if (isPromiseMethodCalleeObject(node) || isAwaitArgument(node)) {
  75. return;
  76. }
  77. const variable = findVariable(context.getScope(), node.callee);
  78. if (!variable || variable.defs.length !== 1) {
  79. return;
  80. }
  81. const [definition] = variable.defs;
  82. const value = definition.type === 'Variable' && definition.kind === 'const'
  83. ? definition.node.init
  84. : definition.node;
  85. if (
  86. !(
  87. (
  88. value.type === 'ArrowFunctionExpression'
  89. || value.type === 'FunctionExpression'
  90. || value.type === 'FunctionDeclaration'
  91. ) && !value.generator && value.async
  92. )
  93. ) {
  94. return;
  95. }
  96. return {
  97. node,
  98. messageId: ERROR_IDENTIFIER,
  99. data: {name: node.callee.name},
  100. suggest: [
  101. {
  102. messageId: SUGGESTION_ADD_AWAIT,
  103. fix: fixer => fixer.insertTextBefore(node, 'await '),
  104. },
  105. ],
  106. };
  107. },
  108. };
  109. }
  110. /** @type {import('eslint').Rule.RuleModule} */
  111. module.exports = {
  112. create,
  113. meta: {
  114. type: 'suggestion',
  115. docs: {
  116. description: 'Prefer top-level await over top-level promises and async function calls.',
  117. },
  118. hasSuggestions: true,
  119. messages,
  120. },
  121. };