From 53f8bf9292e06e9c71fc44a9d4f8224a9bf24bcd Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Wed, 25 Feb 2015 14:13:34 -0600 Subject: [PATCH] Improve Syntax and Runtime error backtraces --- lib/execjs/external_runtime.rb | 38 +++++++++++---- lib/execjs/ruby_racer_runtime.rb | 32 +++++++------ lib/execjs/support/jsc_runner.js | 4 +- lib/execjs/support/jscript_runner.js | 4 +- lib/execjs/support/node_runner.js | 4 +- lib/execjs/support/spidermonkey_runner.js | 4 +- test/test_execjs.rb | 58 +++++++++++++++++++---- 7 files changed, 103 insertions(+), 41 deletions(-) diff --git a/lib/execjs/external_runtime.rb b/lib/execjs/external_runtime.rb index bd51eae..7cb0a9d 100644 --- a/lib/execjs/external_runtime.rb +++ b/lib/execjs/external_runtime.rb @@ -24,12 +24,12 @@ module ExecJS def exec(source, options = {}) source = encode(source) - source = "#{@source}\n#{source}" if @source + source = "#{@source}\n#{source}" if @source != "" source = @runtime.compile_source(source) tmpfile = write_to_tempfile(source) begin - extract_result(@runtime.exec_runtime(tmpfile.path)) + extract_result(@runtime.exec_runtime(tmpfile.path), tmpfile.path) ensure File.unlink(tmpfile) end @@ -57,14 +57,25 @@ module ExecJS tmpfile end - def extract_result(output) - status, value = output.empty? ? [] : ::JSON.parse(output, create_additions: false) + def extract_result(output, filename) + status, value, stack = output.empty? ? [] : ::JSON.parse(output, create_additions: false) if status == "ok" value - elsif value =~ /SyntaxError:/ - raise RuntimeError, value else - raise ProgramError, value + stack ||= "" + real_filename = File.realpath(filename) + stack = stack.split("\n").map do |line| + line.sub(" at ", "") + .sub(real_filename, "(execjs)") + .sub(filename, "(execjs)") + .strip + end + stack.reject! { |line| ["eval code", "eval@[native code]"].include?(line) } + stack.shift unless stack[0].to_s.include?("(execjs)") + error_class = value =~ /SyntaxError:/ ? RuntimeError : ProgramError + error = error_class.new(value) + error.set_backtrace(stack + caller) + raise error end end end @@ -158,7 +169,7 @@ module ExecJS if $?.success? output else - raise RuntimeError, output + raise exec_runtime_error(output) end end @@ -181,7 +192,7 @@ module ExecJS if $?.success? output else - raise RuntimeError, output + raise exec_runtime_error(output) end end else @@ -193,13 +204,20 @@ module ExecJS if $?.success? output else - raise RuntimeError, output + raise exec_runtime_error(output) end end end # Internally exposed for Context. public :exec_runtime + def exec_runtime_error(output) + error = RuntimeError.new(output) + lineno = output.split("\n")[0][/:(\d+)$/, 1] || 1 + error.set_backtrace(["(execjs):#{lineno}"] + caller) + error + end + def which(command) Array(command).find do |name| name, args = name.split(/\s+/, 2) diff --git a/lib/execjs/ruby_racer_runtime.rb b/lib/execjs/ruby_racer_runtime.rb index d7c36b7..eee99fb 100644 --- a/lib/execjs/ruby_racer_runtime.rb +++ b/lib/execjs/ruby_racer_runtime.rb @@ -12,11 +12,7 @@ module ExecJS begin @v8_context.eval(source) rescue ::V8::JSError => e - if e.value["name"] == "SyntaxError" - raise RuntimeError, e.value.to_s - else - raise ProgramError, e.value.to_s - end + raise wrap_error(e) end end end @@ -37,11 +33,7 @@ module ExecJS begin unbox @v8_context.eval("(#{source})") rescue ::V8::JSError => e - if e.value["name"] == "SyntaxError" - raise RuntimeError, e.value.to_s - else - raise ProgramError, e.value.to_s - end + raise wrap_error(e) end end end @@ -52,11 +44,7 @@ module ExecJS begin unbox @v8_context.eval(properties).call(*args) rescue ::V8::JSError => e - if e.value["name"] == "SyntaxError" - raise RuntimeError, e.value.to_s - else - raise ProgramError, e.value.to_s - end + raise wrap_error(e) end end end @@ -96,6 +84,20 @@ module ExecJS result end end + + def wrap_error(e) + error_class = e.value["name"] == "SyntaxError" ? RuntimeError : ProgramError + + stack = e.value["stack"] || "" + stack = stack.split("\n") + stack.shift + stack = [e.message[/:\d+:\d+/, 0]].compact if stack.empty? + stack = stack.map { |line| line.sub(" at ", "").sub("", "(execjs)").strip } + + error = error_class.new(e.value.to_s) + error.set_backtrace(stack + caller) + error + end end def name diff --git a/lib/execjs/support/jsc_runner.js b/lib/execjs/support/jsc_runner.js index f7fbedb..c57a944 100644 --- a/lib/execjs/support/jsc_runner.js +++ b/lib/execjs/support/jsc_runner.js @@ -9,10 +9,10 @@ try { print(JSON.stringify(['ok', result])); } catch (err) { - print('["err"]'); + print(JSON.stringify(['err', '' + err, err.stack])); } } } catch (err) { - print(JSON.stringify(['err', '' + err])); + print(JSON.stringify(['err', '' + err, err.stack])); } }); diff --git a/lib/execjs/support/jscript_runner.js b/lib/execjs/support/jscript_runner.js index 4619e56..fc92b4a 100644 --- a/lib/execjs/support/jscript_runner.js +++ b/lib/execjs/support/jscript_runner.js @@ -13,10 +13,10 @@ try { print(JSON.stringify(['ok', result])); } catch (err) { - print('["err"]'); + print(JSON.stringify(['err', err.name + ': ' + err.message, err.stack])); } } } catch (err) { - print(JSON.stringify(['err', err.name + ': ' + err.message])); + print(JSON.stringify(['err', err.name + ': ' + err.message, err.stack])); } }); diff --git a/lib/execjs/support/node_runner.js b/lib/execjs/support/node_runner.js index 46210f9..ab0c6ec 100644 --- a/lib/execjs/support/node_runner.js +++ b/lib/execjs/support/node_runner.js @@ -11,10 +11,10 @@ try { print(JSON.stringify(['ok', result])); } catch (err) { - print('["err"]'); + print(JSON.stringify(['err', '' + err, err.stack])); } } } catch (err) { - print(JSON.stringify(['err', '' + err])); + print(JSON.stringify(['err', '' + err, err.stack])); } }); diff --git a/lib/execjs/support/spidermonkey_runner.js b/lib/execjs/support/spidermonkey_runner.js index f7fbedb..c57a944 100644 --- a/lib/execjs/support/spidermonkey_runner.js +++ b/lib/execjs/support/spidermonkey_runner.js @@ -9,10 +9,10 @@ try { print(JSON.stringify(['ok', result])); } catch (err) { - print('["err"]'); + print(JSON.stringify(['err', '' + err, err.stack])); } } } catch (err) { - print(JSON.stringify(['err', '' + err])); + print(JSON.stringify(['err', '' + err, err.stack])); } }); diff --git a/test/test_execjs.rb b/test/test_execjs.rb index 20b8a53..d0d7774 100644 --- a/test/test_execjs.rb +++ b/test/test_execjs.rb @@ -253,38 +253,80 @@ class TestExecJS < Test end def test_exec_syntax_error - assert_raises ExecJS::RuntimeError do + begin ExecJS.exec(")") + flunk + rescue ExecJS::RuntimeError => e + assert e + assert e.backtrace[0].include?("(execjs):1"), e.backtrace.join("\n") end end def test_eval_syntax_error - assert_raises ExecJS::RuntimeError do + begin ExecJS.eval(")") + flunk + rescue ExecJS::RuntimeError => e + assert e + assert e.backtrace[0].include?("(execjs):1"), e.backtrace.join("\n") end end def test_compile_syntax_error - assert_raises ExecJS::RuntimeError do + begin ExecJS.compile(")") + flunk + rescue ExecJS::RuntimeError => e + assert e + assert e.backtrace[0].include?("(execjs):1"), e.backtrace.join("\n") end end - def test_exec_thrown_exception + def test_exec_thrown_error + begin + ExecJS.exec("throw new Error('hello')") + flunk + rescue ExecJS::ProgramError => e + assert e + assert e.backtrace[0].include?("(execjs):1"), e.backtrace.join("\n") + end + end + + def test_eval_thrown_error + begin + ExecJS.eval("(function(){ throw new Error('hello') })()") + flunk + rescue ExecJS::ProgramError => e + assert e + assert e.backtrace[0].include?("(execjs):1"), e.backtrace.join("\n") + end + end + + def test_compile_thrown_error + begin + ExecJS.compile("throw new Error('hello')") + flunk + rescue ExecJS::ProgramError => e + assert e + assert e.backtrace[0].include?("(execjs):1"), e.backtrace.join("\n") + end + end + + def test_exec_thrown_string assert_raises ExecJS::ProgramError do ExecJS.exec("throw 'hello'") end end - def test_eval_thrown_exception + def test_eval_thrown_string assert_raises ExecJS::ProgramError do - ExecJS.exec("throw 'hello'") + ExecJS.eval("(function(){ throw 'hello' })()") end end - def test_compile_thrown_exception + def test_compile_thrown_string assert_raises ExecJS::ProgramError do - ExecJS.exec("throw 'hello'") + ExecJS.compile("throw 'hello'") end end