diff --git a/gem_prelude.rb b/gem_prelude.rb index c4debb6509..f5616e6b68 100644 --- a/gem_prelude.rb +++ b/gem_prelude.rb @@ -4,6 +4,12 @@ rescue LoadError warn "`RubyGems' were not loaded." end if defined?(Gem) +begin + require 'error_squiggle' +rescue LoadError + warn "`error_squiggle' was not loaded." +end if defined?(ErrorSquiggle) + begin require 'did_you_mean' rescue LoadError diff --git a/lib/error_squiggle.rb b/lib/error_squiggle.rb new file mode 100644 index 0000000000..02b01932c3 --- /dev/null +++ b/lib/error_squiggle.rb @@ -0,0 +1,2 @@ +require_relative "error_squiggle/base" +require_relative "error_squiggle/core_ext" diff --git a/lib/error_squiggle/base.rb b/lib/error_squiggle/base.rb new file mode 100644 index 0000000000..1ff2db9797 --- /dev/null +++ b/lib/error_squiggle/base.rb @@ -0,0 +1,446 @@ +require_relative "version" + +module ErrorSquiggle + # Identify the code fragment that seems associated with a given error + # + # Arguments: + # node: RubyVM::AbstractSyntaxTree::Node + # point: :name | :args + # name: The name associated with the NameError/NoMethodError + # fetch: A block to fetch a specified code line (or lines) + # + # Returns: + # { + # first_lineno: Integer, + # first_column: Integer, + # last_lineno: Integer, + # last_column: Integer, + # line: String, + # } | nil + def self.spot(...) + Spotter.new(...).spot + end + + class Spotter + def initialize(node, point, name: nil, &fetch) + @node = node + @point = point + @name = name + + # Not-implemented-yet options + @arg = nil # Specify the index or keyword at which argument caused the TypeError/ArgumentError + @multiline = false # Allow multiline spot + + @fetch = fetch + end + + def spot + return nil unless @node + + case @node.type + + when :CALL, :QCALL + case @point + when :name + spot_call_for_name + when :args + spot_call_for_args + end + + when :ATTRASGN + case @point + when :name + spot_attrasgn_for_name + when :args + spot_attrasgn_for_args + end + + when :OPCALL + case @point + when :name + spot_opcall_for_name + when :args + spot_opcall_for_args + end + + when :FCALL + case @point + when :name + spot_fcall_for_name + when :args + spot_fcall_for_args + end + + when :VCALL + spot_vcall + + when :OP_ASGN1 + case @point + when :name + spot_op_asgn1_for_name + when :args + spot_op_asgn1_for_args + end + + when :OP_ASGN2 + case @point + when :name + spot_op_asgn2_for_name + when :args + spot_op_asgn2_for_args + end + + when :CONST + spot_vcall + + when :COLON2 + spot_colon2 + + when :COLON3 + spot_vcall + + when :OP_CDECL + spot_op_cdecl + end + + if @line && @beg_column && @end_column && @beg_column < @end_column + return { + first_lineno: @beg_lineno, + first_column: @beg_column, + last_lineno: @end_lineno, + last_column: @end_column, + line: @line, + } + else + return nil + end + end + + private + + # Example: + # x.foo + # ^^^^ + # x.foo(42) + # ^^^^ + # x&.foo + # ^^^^^ + # x[42] + # ^^^^ + # x += 1 + # ^ + def spot_call_for_name + nd_recv, mid, nd_args = @node.children + lineno = nd_recv.last_lineno + lines = @fetch[lineno, @node.last_lineno] + if mid == :[] && lines.match(/\G\s*(\[(?:\s*\])?)/, nd_recv.last_column) + @beg_column = $~.begin(1) + @line = lines[/.*\n/] + @beg_lineno = @end_lineno = lineno + if nd_args + if nd_recv.last_lineno == nd_args.last_lineno && @line.match(/\s*\]/, nd_args.last_column) + @end_column = $~.end(0) + end + else + if lines.match(/\G\s*?\[\s*\]/, nd_recv.last_column) + @end_column = $~.end(0) + end + end + elsif lines.match(/\G\s*?(\&?\.)(\s*?)(#{ Regexp.quote(mid) }).*\n/, nd_recv.last_column) + lines = $` + $& + @beg_column = $~.begin($2.include?("\n") ? 3 : 1) + @end_column = $~.end(3) + if i = lines[..@beg_column].rindex("\n") + @beg_lineno = @end_lineno = lineno + lines[..@beg_column].count("\n") + @line = lines[i + 1..] + @beg_column -= i + 1 + @end_column -= i + 1 + else + @line = lines + @beg_lineno = @end_lineno = lineno + end + elsif mid.to_s =~ /\A\W+\z/ && lines.match(/\G\s*(#{ Regexp.quote(mid) })=.*\n/, nd_recv.last_column) + @line = $` + $& + @beg_column = $~.begin(1) + @end_column = $~.end(1) + end + end + + # Example: + # x.foo(42) + # ^^ + # x[42] + # ^^ + # x += 1 + # ^ + def spot_call_for_args + _nd_recv, _mid, nd_args = @node.children + if nd_args && nd_args.first_lineno == nd_args.last_lineno + fetch_line(nd_args.first_lineno) + @beg_column = nd_args.first_column + @end_column = nd_args.last_column + end + # TODO: support @arg + end + + # Example: + # x.foo = 1 + # ^^^^^^ + # x[42] = 1 + # ^^^^^^ + def spot_attrasgn_for_name + nd_recv, mid, nd_args = @node.children + *nd_args, _nd_last_arg, _nil = nd_args.children + fetch_line(nd_recv.last_lineno) + if mid == :[]= && @line.match(/\G\s*(\[)/, nd_recv.last_column) + @beg_column = $~.begin(1) + args_last_column = $~.end(0) + if nd_args.last && nd_recv.last_lineno == nd_args.last.last_lineno + args_last_column = nd_args.last.last_column + end + if @line.match(/\s*\]\s*=/, args_last_column) + @end_column = $~.end(0) + end + elsif @line.match(/\G\s*(\.\s*#{ Regexp.quote(mid.to_s.sub(/=\z/, "")) }\s*=)/, nd_recv.last_column) + @beg_column = $~.begin(1) + @end_column = $~.end(1) + end + end + + # Example: + # x.foo = 1 + # ^ + # x[42] = 1 + # ^^^^^^^ + # x[] = 1 + # ^^^^^ + def spot_attrasgn_for_args + nd_recv, mid, nd_args = @node.children + fetch_line(nd_recv.last_lineno) + if mid == :[]= && @line.match(/\G\s*\[/, nd_recv.last_column) + @beg_column = $~.end(0) + if nd_recv.last_lineno == nd_args.last_lineno + @end_column = nd_args.last_column + end + elsif nd_args && nd_args.first_lineno == nd_args.last_lineno + @beg_column = nd_args.first_column + @end_column = nd_args.last_column + end + # TODO: support @arg + end + + # Example: + # x + 1 + # ^ + # +x + # ^ + def spot_opcall_for_name + nd_recv, op, nd_arg = @node.children + fetch_line(nd_recv.last_lineno) + if nd_arg + # binary operator + if @line.match(/\G\s*(#{ Regexp.quote(op) })/, nd_recv.last_column) + @beg_column = $~.begin(1) + @end_column = $~.end(1) + end + else + # unary operator + if @line[...nd_recv.first_column].match(/(#{ Regexp.quote(op.to_s.sub(/@\z/, "")) })\s*\(?\s*\z/) + @beg_column = $~.begin(1) + @end_column = $~.end(1) + end + end + end + + # Example: + # x + 1 + # ^ + def spot_opcall_for_args + _nd_recv, _op, nd_arg = @node.children + if nd_arg && nd_arg.first_lineno == nd_arg.last_lineno + # binary operator + fetch_line(nd_arg.first_lineno) + @beg_column = nd_arg.first_column + @end_column = nd_arg.last_column + end + end + + # Example: + # foo(42) + # ^^^ + # foo 42 + # ^^^ + def spot_fcall_for_name + mid, _nd_args = @node.children + fetch_line(@node.first_lineno) + if @line.match(/(#{ Regexp.quote(mid) })/, @node.first_column) + @beg_column = $~.begin(1) + @end_column = $~.end(1) + end + end + + # Example: + # foo(42) + # ^^ + # foo 42 + # ^^ + def spot_fcall_for_args + _mid, nd_args = @node.children + if nd_args && nd_args.first_lineno == nd_args.last_lineno + # binary operator + fetch_line(nd_args.first_lineno) + @beg_column = nd_args.first_column + @end_column = nd_args.last_column + end + end + + # Example: + # foo + # ^^^ + def spot_vcall + if @node.first_lineno == @node.last_lineno + fetch_line(@node.last_lineno) + @beg_column = @node.first_column + @end_column = @node.last_column + end + end + + # Example: + # x[1] += 42 + # ^^^ (for []) + # x[1] += 42 + # ^ (for +) + # x[1] += 42 + # ^^^^^^ (for []=) + def spot_op_asgn1_for_name + nd_recv, op, nd_args, _nd_rhs = @node.children + fetch_line(nd_recv.last_lineno) + if @line.match(/\G\s*(\[)/, nd_recv.last_column) + bracket_beg_column = $~.begin(1) + args_last_column = $~.end(0) + if nd_args && nd_recv.last_lineno == nd_args.last_lineno + args_last_column = nd_args.last_column + end + if @line.match(/\s*\](\s*)(#{ Regexp.quote(op) })=()/, args_last_column) + case @name + when :[], :[]= + @beg_column = bracket_beg_column + @end_column = $~.begin(@name == :[] ? 1 : 3) + when op + @beg_column = $~.begin(2) + @end_column = $~.end(2) + end + end + end + end + + # Example: + # x[1] += 42 + # ^^^^^^^^ + def spot_op_asgn1_for_args + nd_recv, mid, nd_args, nd_rhs = @node.children + fetch_line(nd_recv.last_lineno) + if mid == :[]= && @line.match(/\G\s*\[/, nd_recv.last_column) + @beg_column = $~.end(0) + if nd_recv.last_lineno == nd_rhs.last_lineno + @end_column = nd_rhs.last_column + end + elsif nd_args && nd_args.first_lineno == nd_rhs.last_lineno + @beg_column = nd_args.first_column + @end_column = nd_rhs.last_column + end + # TODO: support @arg + end + + # Example: + # x.foo += 42 + # ^^^ (for foo) + # x.foo += 42 + # ^ (for +) + # x.foo += 42 + # ^^^^^^^ (for foo=) + def spot_op_asgn2_for_name + nd_recv, _qcall, attr, op, _nd_rhs = @node.children + fetch_line(nd_recv.last_lineno) + if @line.match(/\G\s*(\.)\s*#{ Regexp.quote(attr) }()\s*(#{ Regexp.quote(op) })(=)/, nd_recv.last_column) + case @name + when attr + @beg_column = $~.begin(1) + @end_column = $~.begin(2) + when op + @beg_column = $~.begin(3) + @end_column = $~.end(3) + when :"#{ attr }=" + @beg_column = $~.begin(1) + @end_column = $~.end(4) + end + end + end + + # Example: + # x.foo += 42 + # ^^ + def spot_op_asgn2_for_args + _nd_recv, _qcall, _attr, _op, nd_rhs = @node.children + if nd_rhs.first_lineno == nd_rhs.last_lineno + fetch_line(nd_rhs.first_lineno) + @beg_column = nd_rhs.first_column + @end_column = nd_rhs.last_column + end + end + + # Example: + # Foo::Bar + # ^^^^^ + def spot_colon2 + nd_parent, const = @node.children + if nd_parent.last_lineno == @node.last_lineno + fetch_line(nd_parent.last_lineno) + @beg_column = nd_parent.last_column + @end_column = @node.last_column + else + @line = @fetch[@node.last_lineno] + if @line[...@node.last_column].match(/#{ Regexp.quote(const) }\z/) + @beg_column = $~.begin(0) + @end_column = $~.end(0) + end + end + end + + # Example: + # Foo::Bar += 1 + # ^^^^^^^^ + def spot_op_cdecl + nd_lhs, op, _nd_rhs = @node.children + *nd_parent_lhs, _const = nd_lhs.children + if @name == op + @line = @fetch[nd_lhs.last_lineno] + if @line.match(/\G\s*(#{ Regexp.quote(op) })=/, nd_lhs.last_column) + @beg_column = $~.begin(1) + @end_column = $~.end(1) + end + else + # constant access error + @end_column = nd_lhs.last_column + if nd_parent_lhs.empty? # example: ::C += 1 + if nd_lhs.first_lineno == nd_lhs.last_lineno + @line = @fetch[nd_lhs.last_lineno] + @beg_column = nd_lhs.first_column + end + else # example: Foo::Bar::C += 1 + if nd_parent_lhs.last.last_lineno == nd_lhs.last_lineno + @line = @fetch[nd_lhs.last_lineno] + @beg_column = nd_parent_lhs.last.last_column + end + end + end + end + + def fetch_line(lineno) + @beg_lineno = @end_lineno = lineno + @line = @fetch[lineno] + end + end + + private_constant :Spotter +end diff --git a/lib/error_squiggle/core_ext.rb b/lib/error_squiggle/core_ext.rb new file mode 100644 index 0000000000..6274eecb77 --- /dev/null +++ b/lib/error_squiggle/core_ext.rb @@ -0,0 +1,48 @@ +module ErrorSquiggle + module CoreExt + SKIP_TO_S_FOR_SUPER_LOOKUP = true + private_constant :SKIP_TO_S_FOR_SUPER_LOOKUP + + def to_s + msg = super.dup + + locs = backtrace_locations + return msg unless locs + + loc = locs.first + begin + node = RubyVM::AbstractSyntaxTree.of(loc, save_script_lines: true) + opts = {} + + case self + when NoMethodError, NameError + point = :name + opts[:name] = name + when TypeError, ArgumentError + point = :args + end + + spot = ErrorSquiggle.spot(node, point, **opts) do |lineno, last_lineno| + last_lineno ||= lineno + node.script_lines[lineno - 1 .. last_lineno - 1].join("") + end + + rescue Errno::ENOENT + end + + if spot + marker = " " * spot[:first_column] + "^" * (spot[:last_column] - spot[:first_column]) + points = "\n\n#{ spot[:line] }#{ marker }" + msg << points if !msg.include?(points) + end + + msg + end + end + + NameError.prepend(CoreExt) + + # temporarily disabled + #TypeError.prepend(CoreExt) + #ArgumentError.prepend(CoreExt) +end diff --git a/lib/error_squiggle/version.rb b/lib/error_squiggle/version.rb new file mode 100644 index 0000000000..4d88f3a9cf --- /dev/null +++ b/lib/error_squiggle/version.rb @@ -0,0 +1,3 @@ +module ErrorSquiggle + VERSION = "0.1.0" +end diff --git a/ruby.c b/ruby.c index baa2f251c2..c8b3964c39 100644 --- a/ruby.c +++ b/ruby.c @@ -94,6 +94,8 @@ void rb_warning_category_update(unsigned int mask, unsigned int bits); #define EACH_FEATURES(X, SEP) \ X(gems) \ SEP \ + X(error_squiggle) \ + SEP \ X(did_you_mean) \ SEP \ X(rubyopt) \ @@ -320,6 +322,7 @@ usage(const char *name, int help, int highlight, int columns) }; static const struct message features[] = { M("gems", "", "rubygems (only for debugging, default: "DEFAULT_RUBYGEMS_ENABLED")"), + M("error_squiggle", "", "error_squiggle (default: "DEFAULT_RUBYGEMS_ENABLED")"), M("did_you_mean", "", "did_you_mean (default: "DEFAULT_RUBYGEMS_ENABLED")"), M("rubyopt", "", "RUBYOPT environment variable (default: enabled)"), M("frozen-string-literal", "", "freeze all string literals (default: disabled)"), @@ -1508,6 +1511,9 @@ ruby_opt_init(ruby_cmdline_options_t *opt) if (opt->features.set & FEATURE_BIT(gems)) { rb_define_module("Gem"); + if (opt->features.set & FEATURE_BIT(error_squiggle)) { + rb_define_module("ErrorSquiggle"); + } if (opt->features.set & FEATURE_BIT(did_you_mean)) { rb_define_module("DidYouMean"); } diff --git a/spec/ruby/core/exception/no_method_error_spec.rb b/spec/ruby/core/exception/no_method_error_spec.rb index 570ffc47b1..a93ef2187e 100644 --- a/spec/ruby/core/exception/no_method_error_spec.rb +++ b/spec/ruby/core/exception/no_method_error_spec.rb @@ -110,14 +110,14 @@ describe "NoMethodError#message" do begin klass.foo rescue NoMethodError => error - error.message.lines.first.should == "undefined method `foo' for MyClass:Class" + error.message.lines.first.chomp.should == "undefined method `foo' for MyClass:Class" end mod = Module.new { def self.name; "MyModule"; end } begin mod.foo rescue NoMethodError => error - error.message.lines.first.should == "undefined method `foo' for MyModule:Module" + error.message.lines.first.chomp.should == "undefined method `foo' for MyModule:Module" end end end diff --git a/test/error_squiggle/test_error_squiggle.rb b/test/error_squiggle/test_error_squiggle.rb new file mode 100644 index 0000000000..3b44daf74c --- /dev/null +++ b/test/error_squiggle/test_error_squiggle.rb @@ -0,0 +1,984 @@ +require "test/unit" + +require "error_squiggle" + +class ErrorSquiggleTest < Test::Unit::TestCase + class DummyFormatter + def 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 + + def assert_error_message(klass, expected_msg, &blk) + err = assert_raise(klass, &blk) + assert_equal(expected_msg.chomp, err.message) + end + + def test_CALL_noarg_1 + assert_error_message(NoMethodError, <<~END) do +undefined method `foo' for nil:NilClass + + 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:NilClass + + .foo + 1 + ^^^^ + END + + nil + .foo + 1 + end + end + + def test_CALL_noarg_3 + assert_error_message(NoMethodError, <<~END) do +undefined method `foo' for nil:NilClass + + foo + 1 + ^^^ + END + + nil. + foo + 1 + end + end + + def test_CALL_arg_1 + assert_error_message(NoMethodError, <<~END) do +undefined method `foo' for nil:NilClass + + 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:NilClass + + .foo ( + ^^^^ + END + + nil + .foo ( + 42 + ) + end + end + + def test_CALL_arg_3 + assert_error_message(NoMethodError, <<~END) do +undefined method `foo' for nil:NilClass + + foo ( + ^^^ + END + + nil. + foo ( + 42 + ) + end + end + + def test_CALL_arg_4 + assert_error_message(NoMethodError, <<~END) do +undefined method `foo' for nil:NilClass + + 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:NilClass + + .foo( + ^^^^ + END + + nil + .foo( + 42 + ) + end + end + + def test_CALL_arg_6 + assert_error_message(NoMethodError, <<~END) do +undefined method `foo' for nil:NilClass + + foo( + ^^^ + END + + nil. + foo( + 42 + ) + end + end + + def test_QCALL_1 + assert_error_message(NoMethodError, <<~END) do +undefined method `foo' for 1:Integer + + 1&.foo + ^^^^^ + END + + 1&.foo + end + end + + def test_QCALL_2 + assert_error_message(NoMethodError, <<~END) do +undefined method `foo' for 1:Integer + + 1&.foo(42) + ^^^^^ + END + + 1&.foo(42) + end + end + + def test_CALL_aref_1 + assert_error_message(NoMethodError, <<~END) do +undefined method `[]' for nil:NilClass + + nil [ ] + ^^^ + END + + nil [ ] + end + end + + def test_CALL_aref_2 + assert_error_message(NoMethodError, <<~END) do +undefined method `[]' for nil:NilClass + + nil [0] + ^^^ + END + + nil [0] + end + end + + def test_CALL_aref_3 + assert_error_message(NoMethodError, <<~END) do +undefined method `[]' for nil:NilClass + END + + nil [ + 0 + ] + end + end + + def test_CALL_aref_4 + v = Object.new + assert_error_message(NoMethodError, <<~END) do +undefined method `[]' for #{ v.inspect } + + v &.[](0) + ^^^^ + END + + v &.[](0) + end + end + + def test_CALL_aset + assert_error_message(NoMethodError, <<~END) do +undefined method `[]=' for nil:NilClass + + nil.[]= + ^^^^ + END + + nil.[]= + end + end + + def test_CALL_op_asgn + v = nil + assert_error_message(NoMethodError, <<~END) do +undefined method `+' for nil:NilClass + + v += 42 + ^ + END + + v += 42 + end + end + + def test_CALL_special_call_1 + assert_error_message(NoMethodError, <<~END) do +undefined method `call' for nil:NilClass + END + + nil.() + end + end + + def test_CALL_special_call_2 + assert_error_message(NoMethodError, <<~END) do +undefined method `call' for nil:NilClass + END + + nil.(42) + end + end + + def test_CALL_send + assert_error_message(NoMethodError, <<~END) do +undefined method `foo' for nil:NilClass + + 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:NilClass + + nil [ ] = 42 + ^^^^^ + END + + nil [ ] = 42 + end + end + + def test_ATTRASGN_2 + assert_error_message(NoMethodError, <<~END) do +undefined method `[]=' for nil:NilClass + + nil [0] = 42 + ^^^^^ + END + + nil [0] = 42 + end + end + + def test_ATTRASGN_3 + assert_error_message(NoMethodError, <<~END) do +undefined method `foo=' for nil:NilClass + + nil.foo = 42 + ^^^^^^ + END + + nil.foo = 42 + end + end + + def test_OPCALL_binary_1 + assert_error_message(NoMethodError, <<~END) do +undefined method `+' for nil:NilClass + + nil + 42 + ^ + END + + nil + 42 + end + end + + def test_OPCALL_binary_2 + assert_error_message(NoMethodError, <<~END) do +undefined method `+' for nil:NilClass + + nil + # comment + ^ + END + + nil + # comment + 42 + end + end + + def test_OPCALL_unary + assert_error_message(NoMethodError, <<~END) do +undefined method `+@' for nil:NilClass + + + nil + ^ + END + + + nil + end + end + + def test_FCALL_1 + assert_error_message(NoMethodError, <<~END) do +undefined method `foo' for nil:NilClass + + 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:NilClass + + 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:NilClass + + 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:NilClass + + 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:NilClass + + 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:NilClass + END + + v [ + 0 + ] += # comment + 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:NilClass + + 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:NilClass + + 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:NilClass + 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_ASGN2_read_1 + v = nil + + assert_error_message(NoMethodError, <<~END) do +undefined method `foo' for nil:NilClass + + 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:NilClass + + v.foo += # comment + ^^^^ + END + + v.foo += # comment + 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:NilClass + + 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:NilClass + + v.foo += # comment + ^ + END + + v.foo += # comment + 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_CONST + assert_error_message(NameError, <<~END) do +uninitialized constant ErrorSquiggleTest::NotDefined + + 1 + NotDefined + 1 + ^^^^^^^^^^ + END + + 1 + NotDefined + 1 + end + end + + def test_COLON2_1 + assert_error_message(NameError, <<~END) do +uninitialized constant ErrorSquiggleTest::NotDefined + + ErrorSquiggleTest::NotDefined + ^^^^^^^^^^^^ + END + + ErrorSquiggleTest::NotDefined + end + end + + def test_COLON2_2 + assert_error_message(NameError, <<~END) do +uninitialized constant ErrorSquiggleTest::NotDefined + + NotDefined + ^^^^^^^^^^ + END + + ErrorSquiggleTest:: + NotDefined + 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 ErrorSquiggleTest::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 ErrorSquiggleTest::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 ErrorSquiggleTest::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:NilClass + + 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:NilClass + + 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:NilClass + + 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 + assert_error_message(NoMethodError, <<~END) do +undefined method `+' for ErrorSquiggleTest:Class + + ::ErrorSquiggleTest += 1 + ^ + END + + ::ErrorSquiggleTest += 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 ErrorSquiggleTest::NotDefined + + ErrorSquiggleTest.const_get(:NotDefined) + ^^^^^^^^^^ + END + + ErrorSquiggleTest.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:NilClass + + nil.あいうえお + ^^^^^^ + END + + nil.あいうえお + end + end + + if false + + def test_args_CALL_1 + assert_error_message(TypeError, <<~END) do +nil can't be coerced into Integer + + 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 + + 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) + + 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 + + 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 + + $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 + + 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 + + "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 + + "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 + + 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) + + 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 + + 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 + + v.foo += nil + ^^^ + END + + v.foo += nil + end + end + + end +end diff --git a/test/ruby/marshaltestlib.rb b/test/ruby/marshaltestlib.rb index 5c48a8d853..7f100b7873 100644 --- a/test/ruby/marshaltestlib.rb +++ b/test/ruby/marshaltestlib.rb @@ -112,7 +112,7 @@ module MarshalTestLib marshal_equal(Exception.new('foo')) {|o| o.message} obj = Object.new e = assert_raise(NoMethodError) {obj.no_such_method()} - marshal_equal(e) {|o| o.message} + marshal_equal(e) {|o| o.message.lines.first.chomp} end def test_exception_subclass diff --git a/test/ruby/test_marshal.rb b/test/ruby/test_marshal.rb index d2b4ec169f..5e6891a6a0 100644 --- a/test/ruby/test_marshal.rb +++ b/test/ruby/test_marshal.rb @@ -817,7 +817,7 @@ class TestMarshal < Test::Unit::TestCase nameerror_test rescue NameError => e e2 = Marshal.load(Marshal.dump(e)) - assert_equal(e.message, e2.message) + assert_equal(e.message.lines.first.chomp, e2.message.lines.first) assert_equal(e.name, e2.name) assert_equal(e.backtrace, e2.backtrace) assert_nil(e2.backtrace_locations) # temporal diff --git a/test/ruby/test_module.rb b/test/ruby/test_module.rb index 84e74693aa..3411c3d701 100644 --- a/test/ruby/test_module.rb +++ b/test/ruby/test_module.rb @@ -267,7 +267,7 @@ class TestModule < Test::Unit::TestCase ].each do |name, msg| expected = "wrong constant name %s" % name msg = "#{msg}#{': ' if msg}wrong constant name #{name.dump}" - assert_raise_with_message(NameError, expected, "#{msg} to #{m}") do + assert_raise_with_message(NameError, Regexp.compile(Regexp.quote(expected)), "#{msg} to #{m}") do yield name end end diff --git a/test/ruby/test_name_error.rb b/test/ruby/test_name_error.rb index 813a976e96..8fcc2dcb26 100644 --- a/test/ruby/test_name_error.rb +++ b/test/ruby/test_name_error.rb @@ -151,6 +151,6 @@ class TestNameError < Test::Unit::TestCase error = assert_raise(NameError) do receiver::FOO end - assert_equal "uninitialized constant #{'A' * 120}::FOO", error.message + assert_match /\Auninitialized constant #{'A' * 120}::FOO$/, error.message end end diff --git a/test/ruby/test_nomethod_error.rb b/test/ruby/test_nomethod_error.rb index 8b81052905..321b7ccab2 100644 --- a/test/ruby/test_nomethod_error.rb +++ b/test/ruby/test_nomethod_error.rb @@ -86,7 +86,7 @@ class TestNoMethodError < Test::Unit::TestCase str = "\u2600" id = :"\u2604" msg = "undefined method `#{id}' for \"#{str}\":String" - assert_raise_with_message(NoMethodError, msg, bug3237) do + assert_raise_with_message(NoMethodError, Regexp.compile(Regexp.quote(msg)), bug3237) do str.__send__(id) end end diff --git a/test/ruby/test_object.rb b/test/ruby/test_object.rb index 774b707742..83208bbcdb 100644 --- a/test/ruby/test_object.rb +++ b/test/ruby/test_object.rb @@ -777,7 +777,7 @@ class TestObject < Test::Unit::TestCase e = assert_raise(NoMethodError) { o.never_defined_test_no_superclass_method } - assert_equal(m1, e.message, bug2312) + assert_equal(m1.lines.first, e.message.lines.first, bug2312) end def test_superclass_method