always-return.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. 'use strict'
  2. const getDocsUrl = require('./lib/get-docs-url')
  3. function isFunctionWithBlockStatement(node) {
  4. if (node.type === 'FunctionExpression') {
  5. return true
  6. }
  7. if (node.type === 'ArrowFunctionExpression') {
  8. return node.body.type === 'BlockStatement'
  9. }
  10. return false
  11. }
  12. function isThenCallExpression(node) {
  13. return (
  14. node.type === 'CallExpression' &&
  15. node.callee.type === 'MemberExpression' &&
  16. node.callee.property.name === 'then'
  17. )
  18. }
  19. function isFirstArgument(node) {
  20. return (
  21. node.parent && node.parent.arguments && node.parent.arguments[0] === node
  22. )
  23. }
  24. function isInlineThenFunctionExpression(node) {
  25. return (
  26. isFunctionWithBlockStatement(node) &&
  27. isThenCallExpression(node.parent) &&
  28. isFirstArgument(node)
  29. )
  30. }
  31. function hasParentReturnStatement(node) {
  32. if (node && node.parent && node.parent.type) {
  33. // if the parent is a then, and we haven't returned anything, fail
  34. if (isThenCallExpression(node.parent)) {
  35. return false
  36. }
  37. if (node.parent.type === 'ReturnStatement') {
  38. return true
  39. }
  40. return hasParentReturnStatement(node.parent)
  41. }
  42. return false
  43. }
  44. function peek(arr) {
  45. return arr[arr.length - 1]
  46. }
  47. module.exports = {
  48. meta: {
  49. type: 'problem',
  50. docs: {
  51. url: getDocsUrl('always-return'),
  52. },
  53. },
  54. create(context) {
  55. // funcInfoStack is a stack representing the stack of currently executing
  56. // functions
  57. // funcInfoStack[i].branchIDStack is a stack representing the currently
  58. // executing branches ("codePathSegment"s) within the given function
  59. // funcInfoStack[i].branchInfoMap is an object representing information
  60. // about all branches within the given function
  61. // funcInfoStack[i].branchInfoMap[j].good is a boolean representing whether
  62. // the given branch explicitly `return`s or `throw`s. It starts as `false`
  63. // for every branch and is updated to `true` if a `return` or `throw`
  64. // statement is found
  65. // funcInfoStack[i].branchInfoMap[j].loc is a eslint SourceLocation object
  66. // for the given branch
  67. // example:
  68. // funcInfoStack = [ { branchIDStack: [ 's1_1' ],
  69. // branchInfoMap:
  70. // { s1_1:
  71. // { good: false,
  72. // loc: <loc> } } },
  73. // { branchIDStack: ['s2_1', 's2_4'],
  74. // branchInfoMap:
  75. // { s2_1:
  76. // { good: false,
  77. // loc: <loc> },
  78. // s2_2:
  79. // { good: true,
  80. // loc: <loc> },
  81. // s2_4:
  82. // { good: false,
  83. // loc: <loc> } } } ]
  84. const funcInfoStack = []
  85. function markCurrentBranchAsGood() {
  86. const funcInfo = peek(funcInfoStack)
  87. const currentBranchID = peek(funcInfo.branchIDStack)
  88. if (funcInfo.branchInfoMap[currentBranchID]) {
  89. funcInfo.branchInfoMap[currentBranchID].good = true
  90. }
  91. // else unreachable code
  92. }
  93. return {
  94. ReturnStatement: markCurrentBranchAsGood,
  95. ThrowStatement: markCurrentBranchAsGood,
  96. onCodePathSegmentStart(segment, node) {
  97. const funcInfo = peek(funcInfoStack)
  98. funcInfo.branchIDStack.push(segment.id)
  99. funcInfo.branchInfoMap[segment.id] = { good: false, node }
  100. },
  101. onCodePathSegmentEnd() {
  102. const funcInfo = peek(funcInfoStack)
  103. funcInfo.branchIDStack.pop()
  104. },
  105. onCodePathStart() {
  106. funcInfoStack.push({
  107. branchIDStack: [],
  108. branchInfoMap: {},
  109. })
  110. },
  111. onCodePathEnd(path, node) {
  112. const funcInfo = funcInfoStack.pop()
  113. if (!isInlineThenFunctionExpression(node)) {
  114. return
  115. }
  116. path.finalSegments.forEach((segment) => {
  117. const id = segment.id
  118. const branch = funcInfo.branchInfoMap[id]
  119. if (!branch.good) {
  120. if (hasParentReturnStatement(branch.node)) {
  121. return
  122. }
  123. // check shortcircuit syntax like `x && x()` and `y || x()``
  124. const prevSegments = segment.prevSegments
  125. for (let ii = prevSegments.length - 1; ii >= 0; --ii) {
  126. const prevSegment = prevSegments[ii]
  127. if (funcInfo.branchInfoMap[prevSegment.id].good) return
  128. }
  129. context.report({
  130. message: 'Each then() should return a value or throw',
  131. node: branch.node,
  132. })
  133. }
  134. })
  135. },
  136. }
  137. },
  138. }