| 'use strict' |
| |
| const getDocsUrl = require('./lib/get-docs-url') |
| |
| function isFunctionWithBlockStatement(node) { |
| if (node.type === 'FunctionExpression') { |
| return true |
| } |
| if (node.type === 'ArrowFunctionExpression') { |
| return node.body.type === 'BlockStatement' |
| } |
| return false |
| } |
| |
| function isThenCallExpression(node) { |
| return ( |
| node.type === 'CallExpression' && |
| node.callee.type === 'MemberExpression' && |
| node.callee.property.name === 'then' |
| ) |
| } |
| |
| function isFirstArgument(node) { |
| return ( |
| node.parent && node.parent.arguments && node.parent.arguments[0] === node |
| ) |
| } |
| |
| function isInlineThenFunctionExpression(node) { |
| return ( |
| isFunctionWithBlockStatement(node) && |
| isThenCallExpression(node.parent) && |
| isFirstArgument(node) |
| ) |
| } |
| |
| function hasParentReturnStatement(node) { |
| if (node && node.parent && node.parent.type) { |
| // if the parent is a then, and we haven't returned anything, fail |
| if (isThenCallExpression(node.parent)) { |
| return false |
| } |
| |
| if (node.parent.type === 'ReturnStatement') { |
| return true |
| } |
| return hasParentReturnStatement(node.parent) |
| } |
| |
| return false |
| } |
| |
| function peek(arr) { |
| return arr[arr.length - 1] |
| } |
| |
| module.exports = { |
| meta: { |
| docs: { |
| url: getDocsUrl('always-return') |
| } |
| }, |
| create: function(context) { |
| // funcInfoStack is a stack representing the stack of currently executing |
| // functions |
| // funcInfoStack[i].branchIDStack is a stack representing the currently |
| // executing branches ("codePathSegment"s) within the given function |
| // funcInfoStack[i].branchInfoMap is an object representing information |
| // about all branches within the given function |
| // funcInfoStack[i].branchInfoMap[j].good is a boolean representing whether |
| // the given branch explictly `return`s or `throw`s. It starts as `false` |
| // for every branch and is updated to `true` if a `return` or `throw` |
| // statement is found |
| // funcInfoStack[i].branchInfoMap[j].loc is a eslint SourceLocation object |
| // for the given branch |
| // example: |
| // funcInfoStack = [ { branchIDStack: [ 's1_1' ], |
| // branchInfoMap: |
| // { s1_1: |
| // { good: false, |
| // loc: <loc> } } }, |
| // { branchIDStack: ['s2_1', 's2_4'], |
| // branchInfoMap: |
| // { s2_1: |
| // { good: false, |
| // loc: <loc> }, |
| // s2_2: |
| // { good: true, |
| // loc: <loc> }, |
| // s2_4: |
| // { good: false, |
| // loc: <loc> } } } ] |
| const funcInfoStack = [] |
| |
| function markCurrentBranchAsGood() { |
| const funcInfo = peek(funcInfoStack) |
| const currentBranchID = peek(funcInfo.branchIDStack) |
| if (funcInfo.branchInfoMap[currentBranchID]) { |
| funcInfo.branchInfoMap[currentBranchID].good = true |
| } |
| // else unreachable code |
| } |
| |
| return { |
| ReturnStatement: markCurrentBranchAsGood, |
| ThrowStatement: markCurrentBranchAsGood, |
| |
| onCodePathSegmentStart: function(segment, node) { |
| const funcInfo = peek(funcInfoStack) |
| funcInfo.branchIDStack.push(segment.id) |
| funcInfo.branchInfoMap[segment.id] = { good: false, node: node } |
| }, |
| |
| onCodePathSegmentEnd: function(segment, node) { |
| const funcInfo = peek(funcInfoStack) |
| funcInfo.branchIDStack.pop() |
| }, |
| |
| onCodePathStart: function(path, node) { |
| funcInfoStack.push({ |
| branchIDStack: [], |
| branchInfoMap: {} |
| }) |
| }, |
| |
| onCodePathEnd: function(path, node) { |
| const funcInfo = funcInfoStack.pop() |
| |
| if (!isInlineThenFunctionExpression(node)) { |
| return |
| } |
| |
| path.finalSegments.forEach(segment => { |
| const id = segment.id |
| const branch = funcInfo.branchInfoMap[id] |
| if (!branch.good) { |
| if (hasParentReturnStatement(branch.node)) { |
| return |
| } |
| |
| // check shortcircuit syntax like `x && x()` and `y || x()`` |
| const prevSegments = segment.prevSegments |
| for (let ii = prevSegments.length - 1; ii >= 0; --ii) { |
| const prevSegment = prevSegments[ii] |
| if (funcInfo.branchInfoMap[prevSegment.id].good) return |
| } |
| |
| context.report({ |
| message: 'Each then() should return a value or throw', |
| node: branch.node |
| }) |
| } |
| }) |
| } |
| } |
| } |
| } |