prefer-top-level-await.js 3.4 KB

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