always-return.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  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. // istanbul ignore else -- not reachable given not checking `Program`
  33. if (node && node.parent && node.parent.type) {
  34. // if the parent is a then, and we haven't returned anything, fail
  35. if (isThenCallExpression(node.parent)) {
  36. return false
  37. }
  38. if (node.parent.type === 'ReturnStatement') {
  39. return true
  40. }
  41. return hasParentReturnStatement(node.parent)
  42. }
  43. // istanbul ignore next -- not reachable given not checking `Program`
  44. return false
  45. }
  46. function peek(arr) {
  47. return arr[arr.length - 1]
  48. }
  49. module.exports = {
  50. meta: {
  51. type: 'problem',
  52. docs: {
  53. url: getDocsUrl('always-return'),
  54. },
  55. },
  56. create(context) {
  57. // funcInfoStack is a stack representing the stack of currently executing
  58. // functions
  59. // funcInfoStack[i].branchIDStack is a stack representing the currently
  60. // executing branches ("codePathSegment"s) within the given function
  61. // funcInfoStack[i].branchInfoMap is an object representing information
  62. // about all branches within the given function
  63. // funcInfoStack[i].branchInfoMap[j].good is a boolean representing whether
  64. // the given branch explicitly `return`s or `throw`s. It starts as `false`
  65. // for every branch and is updated to `true` if a `return` or `throw`
  66. // statement is found
  67. // funcInfoStack[i].branchInfoMap[j].loc is a eslint SourceLocation object
  68. // for the given branch
  69. // example:
  70. // funcInfoStack = [ { branchIDStack: [ 's1_1' ],
  71. // branchInfoMap:
  72. // { s1_1:
  73. // { good: false,
  74. // loc: <loc> } } },
  75. // { branchIDStack: ['s2_1', 's2_4'],
  76. // branchInfoMap:
  77. // { s2_1:
  78. // { good: false,
  79. // loc: <loc> },
  80. // s2_2:
  81. // { good: true,
  82. // loc: <loc> },
  83. // s2_4:
  84. // { good: false,
  85. // loc: <loc> } } } ]
  86. const funcInfoStack = []
  87. function markCurrentBranchAsGood() {
  88. const funcInfo = peek(funcInfoStack)
  89. const currentBranchID = peek(funcInfo.branchIDStack)
  90. if (funcInfo.branchInfoMap[currentBranchID]) {
  91. funcInfo.branchInfoMap[currentBranchID].good = true
  92. }
  93. // else unreachable code
  94. }
  95. return {
  96. ReturnStatement: markCurrentBranchAsGood,
  97. ThrowStatement: markCurrentBranchAsGood,
  98. onCodePathSegmentStart(segment, node) {
  99. const funcInfo = peek(funcInfoStack)
  100. funcInfo.branchIDStack.push(segment.id)
  101. funcInfo.branchInfoMap[segment.id] = { good: false, node }
  102. },
  103. onCodePathSegmentEnd() {
  104. const funcInfo = peek(funcInfoStack)
  105. funcInfo.branchIDStack.pop()
  106. },
  107. onCodePathStart() {
  108. funcInfoStack.push({
  109. branchIDStack: [],
  110. branchInfoMap: {},
  111. })
  112. },
  113. onCodePathEnd(path, node) {
  114. const funcInfo = funcInfoStack.pop()
  115. if (!isInlineThenFunctionExpression(node)) {
  116. return
  117. }
  118. path.finalSegments.forEach((segment) => {
  119. const id = segment.id
  120. const branch = funcInfo.branchInfoMap[id]
  121. if (!branch.good) {
  122. if (hasParentReturnStatement(branch.node)) {
  123. return
  124. }
  125. context.report({
  126. message: 'Each then() should return a value or throw',
  127. node: branch.node,
  128. })
  129. }
  130. })
  131. },
  132. }
  133. },
  134. }