all files / rules/ no-constructor-bind.js

100% Statements 53/53
96.88% Branches 31/32
100% Functions 14/14
100% Lines 52/52
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165                                            14×           10×               10×                 10×                                                                     10×           16×     18× 18×     18×                       18×     14×           14×                    
/**
 * @fileoverview Use class arrow functions instead of binding in the constructor
 * @author Mark Battersby
 */
'use strict'
 
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
 
module.exports = {
  meta: {
    docs: {
      description:
        'Use class arrow functions instead of binding in the constructor',
      category: 'ECMAScript 7',
      recommended: false
    },
    fixable: 'code',
    schema: []
  },
 
  create: function(context) {
    let thisClass = {}
 
    //----------------------------------------------------------------------
    // Helpers
    //----------------------------------------------------------------------
 
    function assigningToThis(node) {
      return (
        node.left &&
        node.left.type === 'MemberExpression' &&
        node.left.object &&
        node.left.object.type === 'ThisExpression'
      )
    }
 
    function callingBind(node) {
      return (
        node.right &&
        node.right.type === 'CallExpression' &&
        node.right.callee &&
        node.right.callee.property &&
        node.right.callee.property.name === 'bind'
      )
    }
 
    function passingThis(node) {
      return (
        node.right &&
        node.right.arguments &&
        node.right.arguments.length === 1 &&
        node.right.arguments[0].type === 'ThisExpression'
      )
    }
 
    function methodNameFromCall(node) {
      return node.right.callee.object.property.name
    }
 
    function findMethodDef(name) {
      return thisClass.classMethods[name]
    }
 
    function getParamsFromLines(params, lines) {
      if (params.length) {
        const start = params[0].loc.start
        const end = params[params.length - 1].loc.end
 
        return getTextFromRange(lines, start, end)
      } else {
        return '()'
      }
    }
 
    function convertToArrowFunction(node) {
      const { params, body } = node.value
      const sourceCode = context.getSourceCode()
      const paramsText = getParamsFromLines(params, sourceCode.getLines())
      const bodyText =
        body.type === 'BlockStatement' ? sourceCode.getText(body) : null
 
      return `${node.key.name} = ${paramsText} => ${bodyText}`
    }
 
    function getTextFromRange(lines, locStart, locEnd) {
      const _lines = lines.slice(locStart.line - 1, locEnd.line)
      const params = _lines
        .map((s, i) => {
          if (i === 0) {
            return s.slice(
              locStart.column - 1,
              locStart.line === locEnd.line ? locEnd.column + 1 : undefined
            )
          }
          if (i === _lines.length - 1) {
            return s.slice(0, locEnd.column + 1)
          }
          return s
        })
        .join('\n')
      return params.charAt(0) === '(' ? params : `(${params.trim()})`
    }
 
    //----------------------------------------------------------------------
    // Public
    //----------------------------------------------------------------------
 
    function checkAssignment(node) {
      if (assigningToThis(node) && callingBind(node) && passingThis(node)) {
        logBoundMethod(node)
      }
    }
 
    function logBoundMethod(node) {
      thisClass.boundMethods.push(node)
    }
 
    function logMethodDefinition(node) {
      thisClass.classMethods[node.key.name] = node
    }
 
    function init() {
      thisClass.boundMethods = []
      thisClass.classMethods = {}
    }
 
    function report() {
      thisClass.boundMethods.forEach(node => {
        context.report({
          node: node,
          message: 'use arrow functions instead of binding in the constructor',
          fix: fixer => {
            const definition = findMethodDef(methodNameFromCall(node))
            if (!definition) return
 
            return [
              fixer.remove(node),
              fixer.replaceText(definition, convertToArrowFunction(definition))
            ]
          }
        })
      })
 
      thisClass = {}
    }
 
    const constructorDefinition = [
      'MethodDefinition[kind="constructor"]',
      'ExpressionStatement',
      'AssignmentExpression'
    ].join(' ')
 
    return {
      [constructorDefinition]: checkAssignment,
      MethodDefinition: logMethodDefinition,
      'ClassDeclaration': init,
      'ClassDeclaration:exit': report,
      'ClassExpression': init,
      'ClassExpression:exit': report
    }
  }
}