always-return.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  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. docs: {
  50. url: getDocsUrl('always-return')
  51. }
  52. },
  53. create: function(context) {
  54. // funcInfoStack is a stack representing the stack of currently executing
  55. // functions
  56. // funcInfoStack[i].branchIDStack is a stack representing the currently
  57. // executing branches ("codePathSegment"s) within the given function
  58. // funcInfoStack[i].branchInfoMap is an object representing information
  59. // about all branches within the given function
  60. // funcInfoStack[i].branchInfoMap[j].good is a boolean representing whether
  61. // the given branch explictly `return`s or `throw`s. It starts as `false`
  62. // for every branch and is updated to `true` if a `return` or `throw`
  63. // statement is found
  64. // funcInfoStack[i].branchInfoMap[j].loc is a eslint SourceLocation object
  65. // for the given branch
  66. // example:
  67. // funcInfoStack = [ { branchIDStack: [ 's1_1' ],
  68. // branchInfoMap:
  69. // { s1_1:
  70. // { good: false,
  71. // loc: <loc> } } },
  72. // { branchIDStack: ['s2_1', 's2_4'],
  73. // branchInfoMap:
  74. // { s2_1:
  75. // { good: false,
  76. // loc: <loc> },
  77. // s2_2:
  78. // { good: true,
  79. // loc: <loc> },
  80. // s2_4:
  81. // { good: false,
  82. // loc: <loc> } } } ]
  83. const funcInfoStack = []
  84. function markCurrentBranchAsGood() {
  85. const funcInfo = peek(funcInfoStack)
  86. const currentBranchID = peek(funcInfo.branchIDStack)
  87. if (funcInfo.branchInfoMap[currentBranchID]) {
  88. funcInfo.branchInfoMap[currentBranchID].good = true
  89. }
  90. // else unreachable code
  91. }
  92. return {
  93. ReturnStatement: markCurrentBranchAsGood,
  94. ThrowStatement: markCurrentBranchAsGood,
  95. onCodePathSegmentStart: function(segment, node) {
  96. const funcInfo = peek(funcInfoStack)
  97. funcInfo.branchIDStack.push(segment.id)
  98. funcInfo.branchInfoMap[segment.id] = { good: false, node: node }
  99. },
  100. onCodePathSegmentEnd: function(segment, node) {
  101. const funcInfo = peek(funcInfoStack)
  102. funcInfo.branchIDStack.pop()
  103. },
  104. onCodePathStart: function(path, node) {
  105. funcInfoStack.push({
  106. branchIDStack: [],
  107. branchInfoMap: {}
  108. })
  109. },
  110. onCodePathEnd: function(path, node) {
  111. const funcInfo = funcInfoStack.pop()
  112. if (!isInlineThenFunctionExpression(node)) {
  113. return
  114. }
  115. path.finalSegments.forEach(segment => {
  116. const id = segment.id
  117. const branch = funcInfo.branchInfoMap[id]
  118. if (!branch.good) {
  119. if (hasParentReturnStatement(branch.node)) {
  120. return
  121. }
  122. // check shortcircuit syntax like `x && x()` and `y || x()``
  123. const prevSegments = segment.prevSegments
  124. for (let ii = prevSegments.length - 1; ii >= 0; --ii) {
  125. const prevSegment = prevSegments[ii]
  126. if (funcInfo.branchInfoMap[prevSegment.id].good) return
  127. }
  128. context.report({
  129. message: 'Each then() should return a value or throw',
  130. node: branch.node
  131. })
  132. }
  133. })
  134. }
  135. }
  136. }
  137. }