require "test/unit" require "error_highlight" require "tempfile" class ErrorHighlightTest < Test::Unit::TestCase class DummyFormatter def self.message_for(corrections) "" end end def setup if defined?(DidYouMean) @did_you_mean_old_formatter = DidYouMean.formatter DidYouMean.formatter = DummyFormatter end end def teardown if defined?(DidYouMean) DidYouMean.formatter = @did_you_mean_old_formatter end end if Exception.method_defined?(:detailed_message) def assert_error_message(klass, expected_msg, &blk) omit unless klass < ErrorHighlight::CoreExt err = assert_raise(klass, &blk) assert_equal(expected_msg.chomp, err.detailed_message(highlight: false).sub(/ \((?:NoMethod|Name)Error\)/, "")) end else def assert_error_message(klass, expected_msg, &blk) omit unless klass < ErrorHighlight::CoreExt err = assert_raise(klass, &blk) assert_equal(expected_msg.chomp, err.message) end end if begin; 1.time; rescue; $!.message.end_with?("an instance of Integer"); end # new message format NEW_MESSAGE_FORMAT = true NIL_RECV_MESSAGE = "nil" ONE_RECV_MESSAGE = "an instance of Integer" else NEW_MESSAGE_FORMAT = false NIL_RECV_MESSAGE = "nil:NilClass" ONE_RECV_MESSAGE = "1:Integer" end def test_CALL_noarg_1 assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } nil.foo + 1 ^^^^ END nil.foo + 1 end end def test_CALL_noarg_2 assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } .foo + 1 ^^^^ END nil .foo + 1 end end def test_CALL_noarg_3 assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } foo + 1 ^^^ END nil. foo + 1 end end def test_CALL_noarg_4 assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } (nil).foo + 1 ^^^^ END (nil).foo + 1 end end def test_CALL_arg_1 assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } nil.foo (42) ^^^^ END nil.foo (42) end end def test_CALL_arg_2 assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } .foo ( ^^^^ END nil .foo ( 42 ) end end def test_CALL_arg_3 assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } foo ( ^^^ END nil. foo ( 42 ) end end def test_CALL_arg_4 assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } nil.foo(42) ^^^^ END nil.foo(42) end end def test_CALL_arg_5 assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } .foo( ^^^^ END nil .foo( 42 ) end end def test_CALL_arg_6 assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } foo( ^^^ END nil. foo( 42 ) end end def test_QCALL_1 assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ ONE_RECV_MESSAGE } 1&.foo ^^^^^ END 1&.foo end end def test_QCALL_2 assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ ONE_RECV_MESSAGE } 1&.foo(42) ^^^^^ END 1&.foo(42) end end def test_CALL_aref_1 assert_error_message(NoMethodError, <<~END) do undefined method `[]' for #{ NIL_RECV_MESSAGE } nil [ ] ^^^ END nil [ ] end end def test_CALL_aref_2 assert_error_message(NoMethodError, <<~END) do undefined method `[]' for #{ NIL_RECV_MESSAGE } nil [0] ^^^ END nil [0] end end def test_CALL_aref_3 assert_error_message(NoMethodError, <<~END) do undefined method `[]' for #{ NIL_RECV_MESSAGE } END nil [ 0 ] end end def test_CALL_aref_4 v = Object.new recv = NEW_MESSAGE_FORMAT ? "an instance of Object" : v.inspect assert_error_message(NoMethodError, <<~END) do undefined method `[]' for #{ recv } v &.[](0) ^^^^ END v &.[](0) end end def test_CALL_aref_5 assert_error_message(NoMethodError, <<~END) do undefined method `[]' for #{ NIL_RECV_MESSAGE } (nil)[ ] ^^^ END (nil)[ ] end end def test_CALL_aset assert_error_message(NoMethodError, <<~END) do undefined method `[]=' for #{ NIL_RECV_MESSAGE } nil.[]= ^^^^ END nil.[]= end end def test_CALL_op_asgn v = nil assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ NIL_RECV_MESSAGE } v += 42 ^ END v += 42 end end def test_CALL_special_call_1 assert_error_message(NoMethodError, <<~END) do undefined method `call' for #{ NIL_RECV_MESSAGE } END nil.() end end def test_CALL_special_call_2 assert_error_message(NoMethodError, <<~END) do undefined method `call' for #{ NIL_RECV_MESSAGE } END nil.(42) end end def test_CALL_send assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } nil.send(:foo, 42) ^^^^^ END nil.send(:foo, 42) end end def test_ATTRASGN_1 assert_error_message(NoMethodError, <<~END) do undefined method `[]=' for #{ NIL_RECV_MESSAGE } nil [ ] = 42 ^^^^^ END nil [ ] = 42 end end def test_ATTRASGN_2 assert_error_message(NoMethodError, <<~END) do undefined method `[]=' for #{ NIL_RECV_MESSAGE } nil [0] = 42 ^^^^^ END nil [0] = 42 end end def test_ATTRASGN_3 assert_error_message(NoMethodError, <<~END) do undefined method `foo=' for #{ NIL_RECV_MESSAGE } nil.foo = 42 ^^^^^^ END nil.foo = 42 end end def test_ATTRASGN_4 assert_error_message(NoMethodError, <<~END) do undefined method `[]=' for #{ NIL_RECV_MESSAGE } (nil)[0] = 42 ^^^^^ END (nil)[0] = 42 end end def test_ATTRASGN_5 assert_error_message(NoMethodError, <<~END) do undefined method `foo=' for #{ NIL_RECV_MESSAGE } (nil).foo = 42 ^^^^^^ END (nil).foo = 42 end end def test_OPCALL_binary_1 assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ NIL_RECV_MESSAGE } nil + 42 ^ END nil + 42 end end def test_OPCALL_binary_2 assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ NIL_RECV_MESSAGE } nil + # comment ^ END nil + # comment 42 end end def test_OPCALL_binary_3 assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ NIL_RECV_MESSAGE } (nil) + 42 ^ END (nil) + 42 end end def test_OPCALL_unary_1 assert_error_message(NoMethodError, <<~END) do undefined method `+@' for #{ NIL_RECV_MESSAGE } + nil ^ END + nil end end def test_OPCALL_unary_2 assert_error_message(NoMethodError, <<~END) do undefined method `+@' for #{ NIL_RECV_MESSAGE } +(nil) ^ END +(nil) end end def test_FCALL_1 assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } nil.instance_eval { foo() } ^^^ END nil.instance_eval { foo() } end end def test_FCALL_2 assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } nil.instance_eval { foo(42) } ^^^ END nil.instance_eval { foo(42) } end end def test_VCALL_2 assert_error_message(NameError, <<~END) do undefined local variable or method `foo' for #{ NIL_RECV_MESSAGE } nil.instance_eval { foo } ^^^ END nil.instance_eval { foo } end end def test_OP_ASGN1_aref_1 v = nil assert_error_message(NoMethodError, <<~END) do undefined method `[]' for #{ NIL_RECV_MESSAGE } v [0] += 42 ^^^ END v [0] += 42 end end def test_OP_ASGN1_aref_2 v = nil assert_error_message(NoMethodError, <<~END) do undefined method `[]' for #{ NIL_RECV_MESSAGE } v [0] += # comment ^^^ END v [0] += # comment 42 end end def test_OP_ASGN1_aref_3 v = nil assert_error_message(NoMethodError, <<~END) do undefined method `[]' for #{ NIL_RECV_MESSAGE } END v [ 0 ] += # comment 42 end end def test_OP_ASGN1_aref_4 v = nil assert_error_message(NoMethodError, <<~END) do undefined method `[]' for #{ NIL_RECV_MESSAGE } (v)[0] += 42 ^^^ END (v)[0] += 42 end end def test_OP_ASGN1_op_1 v = Object.new def v.[](x); nil; end assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ NIL_RECV_MESSAGE } v [0] += 42 ^ END v [0] += 42 end end def test_OP_ASGN1_op_2 v = Object.new def v.[](x); nil; end assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ NIL_RECV_MESSAGE } v [0 ] += # comment ^ END v [0 ] += # comment 42 end end def test_OP_ASGN1_op_3 v = Object.new def v.[](x); nil; end assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ NIL_RECV_MESSAGE } END v [ 0 ] += 42 end end def test_OP_ASGN1_op_4 v = Object.new def v.[](x); nil; end assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ NIL_RECV_MESSAGE } (v)[0] += 42 ^ END (v)[0] += 42 end end def test_OP_ASGN1_aset_1 v = Object.new def v.[](x); 1; end assert_error_message(NoMethodError, <<~END) do undefined method `[]=' for #{ v.inspect } v [0] += 42 ^^^^^^ END v [0] += 42 end end def test_OP_ASGN1_aset_2 v = Object.new def v.[](x); 1; end assert_error_message(NoMethodError, <<~END) do undefined method `[]=' for #{ v.inspect } v [0] += # comment ^^^^^^ END v [0] += # comment 42 end end def test_OP_ASGN1_aset_3 v = Object.new def v.[](x); 1; end assert_error_message(NoMethodError, <<~END) do undefined method `[]=' for #{ v.inspect } END v [ 0 ] += 42 end end def test_OP_ASGN1_aset_4 v = Object.new def v.[](x); 1; end assert_error_message(NoMethodError, <<~END) do undefined method `[]=' for #{ v.inspect } (v)[0] += 42 ^^^^^^ END (v)[0] += 42 end end def test_OP_ASGN2_read_1 v = nil assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } v.foo += 42 ^^^^ END v.foo += 42 end end def test_OP_ASGN2_read_2 v = nil assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } v.foo += # comment ^^^^ END v.foo += # comment 42 end end def test_OP_ASGN2_read_3 v = nil assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } (v).foo += 42 ^^^^ END (v).foo += 42 end end def test_OP_ASGN2_op_1 v = Object.new def v.foo; nil; end assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ NIL_RECV_MESSAGE } v.foo += 42 ^ END v.foo += 42 end end def test_OP_ASGN2_op_2 v = Object.new def v.foo; nil; end assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ NIL_RECV_MESSAGE } v.foo += # comment ^ END v.foo += # comment 42 end end def test_OP_ASGN2_op_3 v = Object.new def v.foo; nil; end assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ NIL_RECV_MESSAGE } (v).foo += 42 ^ END (v).foo += 42 end end def test_OP_ASGN2_write_1 v = Object.new def v.foo; 1; end assert_error_message(NoMethodError, <<~END) do undefined method `foo=' for #{ v.inspect } v.foo += 42 ^^^^^^^ END v.foo += 42 end end def test_OP_ASGN2_write_2 v = Object.new def v.foo; 1; end assert_error_message(NoMethodError, <<~END) do undefined method `foo=' for #{ v.inspect } v.foo += # comment ^^^^^^^ END v.foo += # comment 42 end end def test_OP_ASGN2_write_3 v = Object.new def v.foo; 1; end assert_error_message(NoMethodError, <<~END) do undefined method `foo=' for #{ v.inspect } (v).foo += 42 ^^^^^^^ END (v).foo += 42 end end def test_CONST assert_error_message(NameError, <<~END) do uninitialized constant ErrorHighlightTest::NotDefined 1 + NotDefined + 1 ^^^^^^^^^^ END 1 + NotDefined + 1 end end def test_COLON2_1 assert_error_message(NameError, <<~END) do uninitialized constant ErrorHighlightTest::NotDefined ErrorHighlightTest::NotDefined ^^^^^^^^^^^^ END ErrorHighlightTest::NotDefined end end def test_COLON2_2 assert_error_message(NameError, <<~END) do uninitialized constant ErrorHighlightTest::NotDefined NotDefined ^^^^^^^^^^ END ErrorHighlightTest:: NotDefined end end def test_COLON2_3 assert_error_message(NameError, <<~END) do uninitialized constant ErrorHighlightTest::NotDefined ErrorHighlightTest::NotDefined::Foo ^^^^^^^^^^^^ END ErrorHighlightTest::NotDefined::Foo end end def test_COLON2_4 assert_error_message(NameError, <<~END) do uninitialized constant ErrorHighlightTest::NotDefined ::ErrorHighlightTest::NotDefined::Foo ^^^^^^^^^^^^ END ::ErrorHighlightTest::NotDefined::Foo end end if ErrorHighlight.const_get(:Spotter).const_get(:OPT_GETCONSTANT_PATH) def test_COLON2_5 # Unfortunately, we cannot identify which `NotDefined` caused the NameError assert_error_message(NameError, <<~END) do uninitialized constant ErrorHighlightTest::NotDefined END ErrorHighlightTest::NotDefined::NotDefined end end else def test_COLON2_5 assert_error_message(NameError, <<~END) do uninitialized constant ErrorHighlightTest::NotDefined ErrorHighlightTest::NotDefined::NotDefined ^^^^^^^^^^^^ END ErrorHighlightTest::NotDefined::NotDefined end end end def test_COLON3 assert_error_message(NameError, <<~END) do uninitialized constant NotDefined ::NotDefined ^^^^^^^^^^^^ END ::NotDefined end end module OP_CDECL_TEST Nil = nil end def test_OP_CDECL_read_1 assert_error_message(NameError, <<~END) do uninitialized constant ErrorHighlightTest::OP_CDECL_TEST::NotDefined OP_CDECL_TEST::NotDefined += 1 ^^^^^^^^^^^^ END OP_CDECL_TEST::NotDefined += 1 end end def test_OP_CDECL_read_2 assert_error_message(NameError, <<~END) do uninitialized constant ErrorHighlightTest::OP_CDECL_TEST::NotDefined OP_CDECL_TEST::NotDefined += # comment ^^^^^^^^^^^^ END OP_CDECL_TEST::NotDefined += # comment 1 end end def test_OP_CDECL_read_3 assert_error_message(NameError, <<~END) do uninitialized constant ErrorHighlightTest::OP_CDECL_TEST::NotDefined END OP_CDECL_TEST:: NotDefined += 1 end end def test_OP_CDECL_op_1 assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ NIL_RECV_MESSAGE } OP_CDECL_TEST::Nil += 1 ^ END OP_CDECL_TEST::Nil += 1 end end def test_OP_CDECL_op_2 assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ NIL_RECV_MESSAGE } OP_CDECL_TEST::Nil += # comment ^ END OP_CDECL_TEST::Nil += # comment 1 end end def test_OP_CDECL_op_3 assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ NIL_RECV_MESSAGE } Nil += 1 ^ END OP_CDECL_TEST:: Nil += 1 end end def test_OP_CDECL_toplevel_1 assert_error_message(NameError, <<~END) do uninitialized constant NotDefined ::NotDefined += 1 ^^^^^^^^^^^^ END ::NotDefined += 1 end end def test_OP_CDECL_toplevel_2 recv = NEW_MESSAGE_FORMAT ? "class ErrorHighlightTest" : "ErrorHighlightTest:Class" assert_error_message(NoMethodError, <<~END) do undefined method `+' for #{ recv } ::ErrorHighlightTest += 1 ^ END ::ErrorHighlightTest += 1 end end def test_explicit_raise_name_error assert_error_message(NameError, <<~END) do NameError raise NameError ^^^^^ END raise NameError end end def test_explicit_raise_no_method_error assert_error_message(NoMethodError, <<~END) do NoMethodError raise NoMethodError ^^^^^ END raise NoMethodError end end def test_const_get assert_error_message(NameError, <<~END) do uninitialized constant ErrorHighlightTest::NotDefined ErrorHighlightTest.const_get(:NotDefined) ^^^^^^^^^^ END ErrorHighlightTest.const_get(:NotDefined) end end def test_local_variable_get b = binding assert_error_message(NameError, <<~END) do local variable `foo' is not defined for #{ b.inspect } b.local_variable_get(:foo) ^^^^^^^^^^^^^^^^^^^ END b.local_variable_get(:foo) end end def test_multibyte assert_error_message(NoMethodError, <<~END) do undefined method `あいうえお' for #{ NIL_RECV_MESSAGE } END nil.あいうえお end end def test_args_CALL_1 assert_error_message(TypeError, <<~END) do nil can't be coerced into Integer (TypeError) 1.+(nil) ^^^ END 1.+(nil) end end def test_args_CALL_2 v = [] assert_error_message(TypeError, <<~END) do no implicit conversion from nil to integer (TypeError) v[nil] ^^^ END v[nil] end end def test_args_ATTRASGN_1 v = [] assert_error_message(ArgumentError, <<~END) do wrong number of arguments (given 1, expected 2..3) (ArgumentError) v [ ] = 1 ^^^^^^ END v [ ] = 1 end end def test_args_ATTRASGN_2 v = [] assert_error_message(TypeError, <<~END) do no implicit conversion from nil to integer (TypeError) v [nil] = 1 ^^^^^^^^ END v [nil] = 1 end end def test_args_ATTRASGN_3 assert_error_message(TypeError, <<~END) do no implicit conversion of String into Integer (TypeError) $stdin.lineno = "str" ^^^^^ END $stdin.lineno = "str" end end def test_args_OPCALL assert_error_message(TypeError, <<~END) do nil can't be coerced into Integer (TypeError) 1 + nil ^^^ END 1 + nil end end def test_args_FCALL_1 assert_error_message(TypeError, <<~END) do no implicit conversion of Symbol into String (TypeError) "str".instance_eval { gsub("foo", :sym) } ^^^^^^^^^^^ END "str".instance_eval { gsub("foo", :sym) } end end def test_args_FCALL_2 assert_error_message(TypeError, <<~END) do no implicit conversion of Symbol into String (TypeError) "str".instance_eval { gsub "foo", :sym } ^^^^^^^^^^^ END "str".instance_eval { gsub "foo", :sym } end end def test_args_OP_ASGN1_aref_1 v = [] assert_error_message(TypeError, <<~END) do no implicit conversion from nil to integer (TypeError) v [nil] += 42 ^^^^^^^^^^ END v [nil] += 42 end end def test_args_OP_ASGN1_aref_2 v = [] assert_error_message(ArgumentError, <<~END) do wrong number of arguments (given 0, expected 1..2) (ArgumentError) v [ ] += 42 ^^^^^^^^ END v [ ] += 42 end end def test_args_OP_ASGN1_op v = [1] assert_error_message(TypeError, <<~END) do nil can't be coerced into Integer (TypeError) v [0] += nil ^^^^^^^^^ END v [0] += nil end end def test_args_OP_ASGN2 v = Object.new def v.foo; 1; end assert_error_message(TypeError, <<~END) do nil can't be coerced into Integer (TypeError) v.foo += nil ^^^ END v.foo += nil end end def test_custom_formatter custom_formatter = Object.new def custom_formatter.message_for(spot) "\n\n" + spot.except(:script_lines).inspect end original_formatter, ErrorHighlight.formatter = ErrorHighlight.formatter, custom_formatter assert_error_message(NoMethodError, <<~END) do undefined method `time' for #{ ONE_RECV_MESSAGE } {:first_lineno=>#{ __LINE__ + 3 }, :first_column=>7, :last_lineno=>#{ __LINE__ + 3 }, :last_column=>12, :snippet=>" 1.time {}\\n"} END 1.time {} end ensure ErrorHighlight.formatter = original_formatter end def test_hard_tabs Tempfile.create(["error_highlight_test", ".rb"], binmode: true) do |tmp| tmp << "\t \t1.time {}\n" tmp.close assert_error_message(NoMethodError, <<~END.gsub("_", "\t")) do undefined method `time' for #{ ONE_RECV_MESSAGE } _ _1.time {} _ _ ^^^^^ END load tmp.path end end end def test_no_final_newline Tempfile.create(["error_highlight_test", ".rb"], binmode: true) do |tmp| tmp << "1.time {}" tmp.close assert_error_message(NoMethodError, <<~END) do undefined method `time' for #{ ONE_RECV_MESSAGE } 1.time {} ^^^^^ END load tmp.path end end end def test_simulate_funcallv_from_embedded_ruby assert_error_message(NoMethodError, <<~END) do undefined method `foo' for #{ NIL_RECV_MESSAGE } END nil.foo + 1 rescue NoMethodError => exc def exc.backtrace_locations = [] raise end end def test_spoofed_filename Tempfile.create(["error_highlight_test", ".rb"], binmode: true) do |tmp| tmp << "module Dummy\nend\n" tmp.close recv = NEW_MESSAGE_FORMAT ? "an instance of String" : '"dummy":String' assert_error_message(NameError, <<~END) do undefined local variable or method `foo' for #{ recv } END "dummy".instance_eval do eval <<-END, nil, tmp.path foo END end end end end def raise_name_error 1.time end def test_spot_with_backtrace_location lineno = __LINE__ begin raise_name_error rescue NameError => exc end spot = ErrorHighlight.spot(exc).except(:script_lines) assert_equal(lineno - 4, spot[:first_lineno]) assert_equal(lineno - 4, spot[:last_lineno]) assert_equal(5, spot[:first_column]) assert_equal(10, spot[:last_column]) assert_equal(" 1.time\n", spot[:snippet]) spot = ErrorHighlight.spot(exc, backtrace_location: exc.backtrace_locations[1]).except(:script_lines) assert_equal(lineno + 2, spot[:first_lineno]) assert_equal(lineno + 2, spot[:last_lineno]) assert_equal(6, spot[:first_column]) assert_equal(22, spot[:last_column]) assert_equal(" raise_name_error\n", spot[:snippet]) end def test_spot_with_node omit unless RubyVM::AbstractSyntaxTree.respond_to?(:node_id_for_backtrace_location) begin raise_name_error rescue NameError => exc end bl = exc.backtrace_locations.first expected_spot = ErrorHighlight.spot(exc, backtrace_location: bl) ast = RubyVM::AbstractSyntaxTree.parse_file(__FILE__, keep_script_lines: true) node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(bl) node = find_node_by_id(ast, node_id) actual_spot = ErrorHighlight.spot(node) assert_equal expected_spot, actual_spot end private def find_node_by_id(node, node_id) return node if node.node_id == node_id node.children.each do |child| next unless child.is_a?(RubyVM::AbstractSyntaxTree::Node) found = find_node_by_id(child, node_id) return found if found end return false end end