Permalink
Browse files

A way more reliable line matcher, fix for issue #1

  • Loading branch information...
1 parent 3367bca commit 179df2b1732a92bd6b04da5df2561b1fb338da7f @xenomuta committed Jan 29, 2013
Showing with 268 additions and 95 deletions.
  1. +43 −0 lib/block_finder.coffee
  2. +23 −28 lib/coffee-trace.coffee
  3. +122 −0 lib/cs_js_source_mapping.coffee
  4. +0 −66 lib/line_finder.coffee
  5. +79 −0 lib/quicksilver_score.coffee
  6. +1 −1 package.json
@@ -0,0 +1,43 @@
+fs = require("fs")
+coffee = require("coffee-script")
+{source_line_mappings} = require("./cs_js_source_mapping")
+ScoredString = require("./quicksilver_score")
+
+module.exports = (file, trace_lines=[]) ->
+ # Read coffee-script source code from file
+ source = fs.readFileSync(file).toString()
+
+ # Map lines - Taken from @showell's https://github.com/showell/CoffeeScriptLineMatcher
+ cs_lines = source.split '\n'
+ js_lines = coffee.compile(source).split '\n'
+ mapping = source_line_mappings cs_lines, js_lines
+
+ # Get exception line
+ {line, source} = trace_lines.map((l)->l if l.crash_line).filter((l)->l)[0]
+
+ # Return matching line, matching block or 0
+ s_index = 0
+ d_index = 0
+
+ # Find matching block and try to find matching line too
+ for match, idx in mapping
+ # Found block, now try lines...
+ if line > d_index and line <= d_index + match[1]
+ js_crash_line = source.replace(/\W/g, '')
+ block = {lines: cs_lines.slice(s_index, s_index + match[0]), start: s_index + 1, end: s_index + match[0] + 1, crash_line: '?' }
+ res = { score: 0, line: 0 }
+
+ # Make a fuzzy compare on javascript crash line and each coffee-script line in block
+ for cline, idx2 in block.lines
+ score = new ScoredString(js_crash_line).score cline.replace(/\W/g, ''), 0
+ if score > res.score
+ res.score = score
+ res.line = idx2 - 1
+ # Pick the highest scoring line match as crash line
+ block.crash_line = if res.line > 0 then res.line + s_index else undefined
+ return block
+ s_index += match[0]
+ d_index += match[1]
+
+ # ...otherwise, return same old sad and boring null
+ return null
@@ -5,13 +5,12 @@
# just require this in your code and be happy...
#
draw_coffee_cup = ->
- console.error "\n\x1b[0;33m ( ("
- console.error " ) )"
- console.error " ________"
- console.error " | |]"
- console.error " \\ /"
- console.error " `----' ❛●•・\x1b[0m"
-
+ console.error """\n\x1b[0;33m ( (
+ `)
+ ________・•.
+ | |] ・
+ \\ / ❛
+ `----' ・❛●•・\x1b[0m"""
module.exports = coffee_trace = (options={ascii_art:true})->
process.on 'uncaughtException', (err) ->
@@ -22,10 +21,11 @@ module.exports = coffee_trace = (options={ascii_art:true})->
line_col = Number(coffee_trace[3])
if /\.coffee$/.test filename
- line_finder = require __dirname + '/line_finder'
- coffee_source = require('fs').readFileSync(filename).toString('utf8')
+ # Match corresponding coffee-script lines' block
+ block_finder = require __dirname + '/block_finder'
+ coffee_source = require('fs').readFileSync(filename, 'utf8')
lines = require('coffee-script').compile(coffee_source).split(/\n/)
- max_line_length = 2
+ max_line_length = 2
trace_lines = lines.map (_,l) ->
l += 1
if (line_num >= l - margin) and (line_num <= l + margin)
@@ -37,35 +37,30 @@ module.exports = coffee_trace = (options={ascii_art:true})->
console.error "\x1b[1;41;37m ❛●•・Coffee-Trace \x1b[0m"
# If coffee-script crash line found then print source-code
- if (coffee_line = line_finder(filename, trace_lines))?
- console.error "\n\x1b[0;33m CoffeeScript:\x1b[0m\x1b[1;37m #{filename}\x1b[33m:\x1b[32m#{coffee_line}\x1b[0m"
-
- coffee_lines = coffee_source.split(/\r\n|\n/)
- for i in [(coffee_line - margin)..(coffee_line + margin)]
- continue if i < 1
- line = coffee_lines[i - 1]
- _line_num = new Array((coffee_line + margin).toString().length - i.toString().length + 1).join(' ') + i.toString()
- if i is coffee_line
- console.error " \x1b[1;31m✘\x1b[36m " + _line_num + ": \x1b[1;37m" + line
+ if (block = block_finder(filename, trace_lines))?
+ {start,end,lines,crash_line} = block
+ console.error "\n\x1b[0;33m CoffeeScript:\x1b[0m\x1b[1;37m #{filename}\x1b[33m:\x1b[32m#{crash_line}\x1b[0m"
+ for line, idx in lines
+ i = start + idx
+ _line_num = new Array((crash_line + margin).toString().length - i.toString().length + 1).join(' ') + i.toString()
+ if i is crash_line
+ console.error " \x1b[1;31m✘\x1b[36m #{_line_num}: \x1b[1;37m#{line}"
else
- console.error " \x1b[0;36m " + _line_num + ": \x1b[1;30m" + line
+ console.error " \x1b[0;36m #{_line_num}: \x1b[1;30m#{line}"
console.error "\n\x1b[0;33m Javascript:\x1b[0m\x1b[1;37m #{filename}\x1b[33m:\x1b[32m#{line_num}\x1b[33m:\x1b[32m#{line_col}\x1b[0m"
trace_lines.forEach (l) ->
_line_num = new Array(line_num.toString().length - l.line.toString().length).join(' ') + l.line
if l.line is line_num
console.error "\x1b[#{line_num.toString().length + line_col + 5}C\x1b[1A\x1b[1;31m⬇"
- console.error " \x1b[1;31m✘\x1b[36m " + _line_num + ": \x1b[1;37m" + l.source
+ console.error " \x1b[1;31m✘\x1b[36m #{_line_num}: \x1b[1;37m#{l.source}"
else
- console.error " \x1b[0;36m " + _line_num + ": \x1b[1;30m" + l.source
+ console.error " \x1b[0;36m #{_line_num}: \x1b[1;30m#{l.source}"
max_line_length = l.source.length if l.source.length > max_line_length
spaces = new Array(Math.round(max_line_length / 2)).join(" ")
- # console.error "\x1b[0;33m",spaces,"•●•∙Coffee Script ∙•●•∙\x1b[0m"
- # if line_num > 1
- # console.error "\x1b[0;33m\x1b[#{(margin*2)+3}A",spaces,"•●•∙•●•∙\x1b[#{(margin*2)+3}B\x1b[0m"
-
-
+
+ stack[1] = stack[1].replace(/\.coffee:/, ".js:")
console.error "\x1b[0m\n", stack.join("\n")
console.error()
process.exit()
@@ -0,0 +1,122 @@
+# This module attempts to find source line mappings between CS code
+# and JS code. Yes, this an enormous hack. :)
+
+get_line_matcher = (line) ->
+ # return a function that returns true iff a JS
+ # line is likely generated from a CS line
+ line = line.split('# ')[0].trim()
+ return null if line == ''
+
+ # simple if statements
+ if line.match /^if \S+$/
+ expr = line[3...].replace /@/g, 'this.'
+ return (line) ->
+ line.trim().indexOf("if \(#{expr}\)") == 0
+
+ # do statements
+ if line.match /^do .*->$/
+ return (line) ->
+ line.match /\(function\(.*\) {/
+
+ # requires
+ if line.indexOf(" = require") > 0
+ matches = line.match /["'].*?["']/g
+ if matches
+ s = matches[0]
+ return (line) ->
+ line.indexOf("= require(#{s})") > 0
+
+ # classes
+ matches = line.match /^class ([@A-Za-z0-9_\.\[\]]+)/g
+ if matches
+ s = matches[0]
+ s = s.replace "class ", ""
+ return (line) ->
+ ~line.indexOf(s + " =")
+
+ # assignments
+ matches = line.match /^([\$@A-Za-z0-9_\.\[\]]+)\s+(=|\+=)/g
+ if matches
+ [lhs, op] = matches[0].split /\s+/
+ if lhs.length > 2
+ lhs = lhs.replace '@', '.'
+ return (line) ->
+ ~line.indexOf(lhs + " " + op)
+
+ # objects
+ matches = line.match /^@?([A-Za-z0-9_]+\s*: )/g
+ if matches and matches.indexOf('{') == -1
+ lhs = matches[0].replace '@', ''
+ lhs = lhs.trim()
+ lhs = lhs[0...lhs.length-1].trim()
+ return null if lhs in ['constructor', 'class']
+ return (line) ->
+ line.trim().indexOf(lhs+':') == 0 or line.trim().indexOf(lhs+' =') > 0
+
+ # multiple simple args
+ matches = line.match /\(\S+, .*?\) ->/g
+ if matches
+ s = matches[0]
+ s = s.replace "->", "{"
+ return (line) -> line.indexOf(s) > 0
+
+ # strings | regexes
+ matches = line.match /"[^"]+?"|'[^']+?'|\/[^\/]+?\//g
+ if matches
+ for str in matches
+ if str.length >= 5
+ return (line) -> line.indexOf(str) >= 0
+
+ # try
+ if line.match /^try$/
+ return (line) -> line.trim() == 'try {'
+
+ # catch
+ if line.match /^catch /g
+ catch_var = line.split(' ')[1]
+ return (line) -> line.trim().indexOf("} catch (#{catch_var}) {") == 0
+
+ null
+
+
+is_comment_line = (line) ->
+ line = line.trim()
+ return line == '' or line[0] == '#'
+
+exports.source_line_mappings = (coffee_lines, js_lines) ->
+ # Return an array of source line mappings, where each mapping
+ # is an array with these elements:
+ # CS line number (zero-based)
+ # JS line number (zero-based)
+ #
+ # Not every CS line gets a mapping, but ideally enough lines get
+ # mapped to help out downstream tools.
+ curr_cs_line = 0
+ curr_js_line = 0
+ matches = []
+
+ find_js_match = (line_matcher) ->
+ for k in [curr_js_line...js_lines.length]
+ return k if line_matcher js_lines[k]
+ null
+
+ create_match_for_prior_comment_lines = (cs_line, js_line) ->
+ # Work backward from cs_line to get all comments and blank lines...
+ first_comment_line = cs_line
+ while curr_cs_line <= first_comment_line-1 and is_comment_line coffee_lines[first_comment_line-1]
+ first_comment_line -= 1
+ if first_comment_line < cs_line
+ matches.push [first_comment_line, js_line]
+
+ for line, cs_line in coffee_lines
+ line_matcher = get_line_matcher line
+ if line_matcher
+ js_line = find_js_match(line_matcher)
+ if js_line? and curr_js_line < js_line
+ create_match_for_prior_comment_lines cs_line, js_line
+ matches.push [cs_line, js_line]
+ curr_cs_line = cs_line
+ curr_js_line = js_line
+ matches.push [coffee_lines.length, js_lines.length]
+ matches
+
@@ -1,66 +0,0 @@
-fs = require 'fs'
-coffee = require 'coffee-script'
-
-module.exports = (file, trace_lines=[]) ->
- # Indentator used
- indent = undefined
- # Read all source code lines
- source = fs.readFileSync(file).toString('utf8').split(/\n/)
- # Get exception line
- crash_line = trace_lines.map((l)->l.source if l.crash_line).filter((l)->l)[0]
-
- # Find indentator being used
- for line in source
- break if /^[ \t]/.test(line) and (indent = line.match(/^(\t| +)/)[1])
-
- # Compile blocks from level to level until compiled lines match traced_lines
- last_index = 0
- found = 0
- matched_block = []
- for indent_level in [1..255]
- indent_rx = new RegExp "^#{indent}{#{indent_level}}"
- for line, i in source
- line_num = i
- block = []
- continue if i < last_index # Carry on after last block
- if indent_rx.test(line) or line.replace(/[ \t]+/g, '').length is 0
- block.push(source[i-1]) if i > 0
- while (line = source[i]) and (indent_rx.test(line) or line.replace(/[ \t]+/g, '').length is 0)
- i++
- block.push line
- last_index = i
- try
- compiled = coffee.compile block.join("\n")
- total_match = 0
- for trace_line in trace_lines
- if compiled.indexOf(trace_line.source) > -1
- ++total_match
- else
- --total_match if total_match > 0
- matched_block = block.map((l) -> { line: line_num++, source: l }) if (found = total_match is trace_lines.length)
- catch e
- compiled = ""
-
- # Try to match javascript crash line with coffeescript line number
- if matched_block
- # console.log "\x1b[01;37;#{Math.round(Math.random()*5)+41}m",matched_block,"\x1b[0m\n"
- portions = crash_line.split(/[,\{\}\(\)\t ]+/).filter (p) -> p and p.length
- match_line = 0
- score = -999999
-
- for l in matched_block
- num = l.line
- line = l.source
- total_match = 0
- for p in portions
- if line.indexOf(p) > -1
- while line.indexOf(p) > -1
- total_match++
- line = line.replace p, ''
- else
- total_match--
- if total_match > score
- score = total_match
- match_line = num
-
- return match_line
Oops, something went wrong. Retry.

0 comments on commit 179df2b

Please sign in to comment.