diff --git a/lib/irb.rb b/lib/irb.rb index 5cb91a293..45a59087b 100644 --- a/lib/irb.rb +++ b/lib/irb.rb @@ -1242,27 +1242,33 @@ def handle_exception(exc) irb_bug = true else irb_bug = false - # This is mostly to make IRB work nicely with Rails console's backtrace filtering, which patches WorkSpace#filter_backtrace - # In such use case, we want to filter the exception's backtrace before its displayed through Exception#full_message - # And we clone the exception object in order to avoid mutating the original exception - # TODO: introduce better API to expose exception backtrace externally - backtrace = exc.backtrace.map { |l| @context.workspace.filter_backtrace(l) }.compact + # To support backtrace filtering while utilizing Exception#full_message, we need to clone + # the exception to avoid modifying the original exception's backtrace. exc = exc.clone - exc.set_backtrace(backtrace) - end + filtered_backtrace = exc.backtrace.map { |l| @context.workspace.filter_backtrace(l) }.compact + backtrace_filter = IRB.conf[:BACKTRACE_FILTER] - if RUBY_VERSION < '3.0.0' - if STDOUT.tty? - message = exc.full_message(order: :bottom) - order = :bottom - else - message = exc.full_message(order: :top) - order = :top + if backtrace_filter + if backtrace_filter.respond_to?(:call) + filtered_backtrace = backtrace_filter.call(filtered_backtrace) + else + warn "IRB.conf[:BACKTRACE_FILTER] #{backtrace_filter} should respond to `call` method" + end end - else # '3.0.0' <= RUBY_VERSION - message = exc.full_message(order: :top) - order = :top + + exc.set_backtrace(filtered_backtrace) end + + highlight = Color.colorable? + + order = + if RUBY_VERSION < '3.0.0' + STDOUT.tty? ? :bottom : :top + else # '3.0.0' <= RUBY_VERSION + :top + end + + message = exc.full_message(order: order, highlight: highlight) message = convert_invalid_byte_sequence(message, exc.message.encoding) message = encode_with_invalid_byte_sequence(message, IRB.conf[:LC_MESSAGES].encoding) unless message.encoding.to_s.casecmp?(IRB.conf[:LC_MESSAGES].encoding.to_s) message = message.gsub(/((?:^\t.+$\n)+)/) { |m| diff --git a/test/irb/helper.rb b/test/irb/helper.rb index 1614b42ad..591bd05b7 100644 --- a/test/irb/helper.rb +++ b/test/irb/helper.rb @@ -196,7 +196,7 @@ def type(command) end def write_ruby(program) - @ruby_file = Tempfile.create(%w{irb- .rb}) + @ruby_file = Tempfile.create(%w{irbtest- .rb}) @tmpfiles << @ruby_file @ruby_file.write(program) @ruby_file.close diff --git a/test/irb/test_irb.rb b/test/irb/test_irb.rb index b05fc80f7..28be74408 100644 --- a/test/irb/test_irb.rb +++ b/test/irb/test_irb.rb @@ -823,4 +823,95 @@ def build_irb IRB::Irb.new(workspace, TestInputMethod.new) end end + + class BacktraceFilteringTest < TestIRB::IntegrationTestCase + def test_backtrace_filtering + write_ruby <<~'RUBY' + def foo + raise "error" + end + + def bar + foo + end + + binding.irb + RUBY + + output = run_ruby_file do + type "bar" + type "exit" + end + + assert_match(/irbtest-.*\.rb:2:in (`|'Object#)foo': error \(RuntimeError\)/, output) + frame_traces = output.split("\n").select { |line| line.strip.match?(/from /) }.map(&:strip) + + expected_traces = if RUBY_VERSION >= "3.3.0" + [ + /from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/, + /from .*\/irbtest-.*.rb\(irb\):1:in [`']
'/, + /from :\d+:in (`|'Kernel#)loop'/, + /from :\d+:in (`|'Binding#)irb'/, + /from .*\/irbtest-.*.rb:9:in [`']
'/ + ] + else + [ + /from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/, + /from .*\/irbtest-.*.rb\(irb\):1:in [`']
'/, + /from :\d+:in (`|'Binding#)irb'/, + /from .*\/irbtest-.*.rb:9:in [`']
'/ + ] + end + + expected_traces.reverse! if RUBY_VERSION < "3.0.0" + + expected_traces.each_with_index do |expected_trace, index| + assert_match(expected_trace, frame_traces[index]) + end + end + + def test_backtrace_filtering_with_backtrace_filter + write_rc <<~'RUBY' + class TestBacktraceFilter + def self.call(backtrace) + backtrace.reject { |line| line.include?("internal") } + end + end + + IRB.conf[:BACKTRACE_FILTER] = TestBacktraceFilter + RUBY + + write_ruby <<~'RUBY' + def foo + raise "error" + end + + def bar + foo + end + + binding.irb + RUBY + + output = run_ruby_file do + type "bar" + type "exit" + end + + assert_match(/irbtest-.*\.rb:2:in (`|'Object#)foo': error \(RuntimeError\)/, output) + frame_traces = output.split("\n").select { |line| line.strip.match?(/from /) }.map(&:strip) + + expected_traces = [ + /from .*\/irbtest-.*.rb:6:in (`|'Object#)bar'/, + /from .*\/irbtest-.*.rb\(irb\):1:in [`']
'/, + /from .*\/irbtest-.*.rb:9:in [`']
'/ + ] + + expected_traces.reverse! if RUBY_VERSION < "3.0.0" + + expected_traces.each_with_index do |expected_trace, index| + assert_match(expected_trace, frame_traces[index]) + end + end + end end