ParsedError.coffee 5.12 KB
Newer Older
liang ce committed
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 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
sysPath = require 'path'

module.exports = class ParsedError
  constructor: (@error) ->
    do @_parse

  _parse: ->
    @_trace = []
    @_kind = 'Error'
    @_wrapper = ''

    @_wrapper = String @error.wrapper if @error.wrapper?

    unless typeof @error is 'object'
      @_message = String @error
    else
      @_stack = @error.stack

      if @error.kind?
        @_kind = String @error.kind
      else if typeof @_stack is 'string'
        if m = @_stack.match /^([a-zA-Z0-9\_\$]+):\ /
          @_kind = m[1]

      if typeof @_stack is 'string'
        @_parseStack()
      else
        @_message = @error.message? and String(@error.message) or ''

    return

  _parseStack: ->
    messageLines = []
    reachedTrace = no

    for line in @_stack.split '\n'
      continue if line.trim() is ''
      if reachedTrace
        @_trace.push @_parseTraceItem line
      else
        if line.match /^\s*at\s.+/
          reachedTrace = yes
          @_trace.push @_parseTraceItem line
        else
          messageLines.push line

    message = messageLines.join '\n'
    if message.substr(0, @_kind.length) is @_kind
      message =
        message
        .substr(@_kind.length, message.length)
        .replace(/^\:\s+/, '')

    @_message = message

    return

  _parseTraceItem: (text) ->
    text = text.trim()

    return if text is ''
    return text unless text.match /^at\ /

    # remove the 'at ' part
    text = text.replace /^at /, ''

    return if text in ['Error (<anonymous>)', 'Error (<anonymous>:null:null)']

    original = text

    # the part that comes before the address
    what = null

    # address, including path to module and line/col
    addr = null

    # path to module
    path = null

    # module dir
    dir = null

    # module basename
    file = null

    # line number (if using a compiler, the line number of the module
    # in that compiler will be used)
    line = null

    # column, same as above
    col = null

    # if using a compiler, this will translate to the line number of
    # the js equivalent of that module
    jsLine = null

    # like above
    jsCol = null

    # path that doesn't include `node_module` dirs
    shortenedPath = null

    # like above
    shortenedAddr = null

    packageName = '[current]'

    # pick out the address
    if m = text.match /\(([^\)]+)\)$/
      addr = m[1].trim()

    if addr?
      what = text.substr 0, text.length - addr.length - 2
      what = what.trim()

    # might not have a 'what' clause
    unless addr?
      addr = text.trim()

    addr = @_fixPath addr
    remaining = addr

    # remove the <js> clause if the file is a compiled one
    if m = remaining.match /\,\ <js>:(\d+):(\d+)$/
      jsLine = m[1]
      jsCol = m[2]
      remaining = remaining.substr 0, remaining.length - m[0].length

    # the line/col part
    if m = remaining.match /:(\d+):(\d+)$/
      line = m[1]
      col = m[2]
      remaining = remaining.substr 0, remaining.length - m[0].length
      path = remaining

    # file and dir
    if path?
      file = sysPath.basename path
      dir = sysPath.dirname path

      if dir is '.' then dir = ''

      path = @_fixPath path
      file = @_fixPath file
      dir = @_fixPath dir

    if dir?
      d = dir.replace /[\\]{1,2}/g, '/'
      if m = d.match ///
          node_modules/([^/]+)(?!.*node_modules.*)
        ///

        packageName = m[1]

    unless jsLine?
      jsLine = line
      jsCol = col

    if path?
      r = @_rectifyPath path
      shortenedPath = r.path
      shortenedAddr = shortenedPath + addr.substr(path.length, addr.length)
      packages = r.packages

    original: original
    what: what
    addr: addr
    path: path
    dir: dir
    file: file
    line: parseInt line
    col: parseInt col
    jsLine: parseInt jsLine
    jsCol: parseInt jsCol
    packageName: packageName
    shortenedPath: shortenedPath
    shortenedAddr: shortenedAddr
    packages: packages || []

  _getMessage: -> @_message
  _getKind: -> @_kind
  _getWrapper: -> @_wrapper
  _getStack: -> @_stack
  _getArguments: -> @error.arguments
  _getType: -> @error.type
  _getTrace: -> @_trace
  _fixPath: (path) -> path.replace(///[\\]{1,2}///g, '/')

  _rectifyPath: (path, nameForCurrentPackage) ->
    path = String path
    remaining = path

    return path: path, packages: [] unless m = path.match /^(.+?)\/node_modules\/(.+)$/

    parts = []
    packages = []

    if typeof nameForCurrentPackage is 'string'
      parts.push "[#{nameForCurrentPackage}]"
      packages.push "[#{nameForCurrentPackage}]"
    else
      parts.push "[#{m[1].match(/([^\/]+)$/)[1]}]"
      packages.push m[1].match(/([^\/]+)$/)[1]

    rest = m[2]

    while m = rest.match /([^\/]+)\/node_modules\/(.+)$/
      parts.push "[#{m[1]}]"
      packages.push m[1]
      rest = m[2]

    if m = rest.match /([^\/]+)\/(.+)$/
      parts.push "[#{m[1]}]"
      packages.push m[1]
      rest = m[2]

    parts.push rest

    path: parts.join "/"
    packages: packages

for prop in ['message', 'kind', 'arguments', 'type', 'stack', 'trace', 'wrapper'] then do ->
  methodName = '_get' + prop[0].toUpperCase() + prop.substr(1, prop.length)

  Object.defineProperty ParsedError::, prop,
    get: -> this[methodName]()