diff options
Diffstat (limited to 'spec/ruby/language')
57 files changed, 5114 insertions, 2909 deletions
diff --git a/spec/ruby/language/END_spec.rb b/spec/ruby/language/END_spec.rb index 762a8db0c0..c84f0cc9ac 100644 --- a/spec/ruby/language/END_spec.rb +++ b/spec/ruby/language/END_spec.rb @@ -1,15 +1,33 @@ require_relative '../spec_helper' +require_relative '../shared/kernel/at_exit' describe "The END keyword" do + it_behaves_like :kernel_at_exit, :END + it "runs only once for multiple calls" do ruby_exe("10.times { END { puts 'foo' }; } ").should == "foo\n" end - it "runs last in a given code unit" do - ruby_exe("END { puts 'bar' }; puts'foo'; ").should == "foo\nbar\n" + it "is affected by the toplevel assignment" do + ruby_exe("foo = 'foo'; END { puts foo }").should == "foo\n" + end + + it "warns when END is used in a method" do + ruby_exe(<<~ruby, args: "2>&1").should =~ /warning: END in method; use at_exit/ + def foo + END { } + end + ruby end - it "runs multiple ends in LIFO order" do - ruby_exe("END { puts 'foo' }; END { puts 'bar' }").should == "bar\nfoo\n" + context "END blocks and at_exit callbacks are mixed" do + it "runs them all in reverse order of registration" do + ruby_exe(<<~ruby).should == "at_exit#2\nEND#2\nat_exit#1\nEND#1\n" + END { puts 'END#1' } + at_exit { puts 'at_exit#1' } + END { puts 'END#2' } + at_exit { puts 'at_exit#2' } + ruby + end end end diff --git a/spec/ruby/language/alias_spec.rb b/spec/ruby/language/alias_spec.rb index d1d06e3fac..61fddb0184 100644 --- a/spec/ruby/language/alias_spec.rb +++ b/spec/ruby/language/alias_spec.rb @@ -52,6 +52,15 @@ describe "The alias keyword" do @obj.a.should == 5 end + it "works with an interpolated symbol with non-literal embedded expression on the left-hand side" do + @meta.class_eval do + eval %Q{ + alias :"#{'a' + ''.to_s}" value + } + end + @obj.a.should == 5 + end + it "works with a simple symbol on the right-hand side" do @meta.class_eval do alias a :value @@ -80,6 +89,15 @@ describe "The alias keyword" do @obj.a.should == 5 end + it "works with an interpolated symbol with non-literal embedded expression on the right-hand side" do + @meta.class_eval do + eval %Q{ + alias a :"#{'value' + ''.to_s}" + } + end + @obj.a.should == 5 + end + it "adds the new method to the list of methods" do original_methods = @obj.methods @meta.class_eval do @@ -234,7 +252,7 @@ describe "The alias keyword" do it "on top level defines the alias on Object" do # because it defines on the default definee / current module - ruby_exe("def foo; end; alias bla foo; print method(:bla).owner", escape: true).should == "Object" + ruby_exe("def foo; end; alias bla foo; print method(:bla).owner").should == "Object" end it "raises a NameError when passed a missing name" do @@ -243,6 +261,19 @@ describe "The alias keyword" do e.class.should == NameError } end + + it "defines the method on the aliased class when the original method is from a parent class" do + parent = Class.new do + def parent_method + end + end + child = Class.new(parent) do + alias parent_method_alias parent_method + end + + child.instance_method(:parent_method_alias).owner.should == child + child.instance_methods(false).should include(:parent_method_alias) + end end describe "The alias keyword" do diff --git a/spec/ruby/language/assignments_spec.rb b/spec/ruby/language/assignments_spec.rb new file mode 100644 index 0000000000..2773508d8d --- /dev/null +++ b/spec/ruby/language/assignments_spec.rb @@ -0,0 +1,529 @@ +require_relative '../spec_helper' + +# Should be synchronized with spec/ruby/language/optional_assignments_spec.rb +# Some specs for assignments are located in language/variables_spec.rb +describe 'Assignments' do + describe 'using =' do + describe 'evaluation order' do + it 'evaluates expressions left to right when assignment with an accessor' do + object = Object.new + def object.a=(value) end + ScratchPad.record [] + + (ScratchPad << :receiver; object).a = (ScratchPad << :rhs; :value) + ScratchPad.recorded.should == [:receiver, :rhs] + end + + it 'evaluates expressions left to right when assignment with a #[]=' do + object = Object.new + def object.[]=(_, _) end + ScratchPad.record [] + + (ScratchPad << :receiver; object)[(ScratchPad << :argument; :a)] = (ScratchPad << :rhs; :value) + ScratchPad.recorded.should == [:receiver, :argument, :rhs] + end + + # similar tests for evaluation order are located in language/constants_spec.rb + ruby_version_is ''...'3.2' do + it 'evaluates expressions right to left when assignment with compounded constant' do + m = Module.new + ScratchPad.record [] + + (ScratchPad << :module; m)::A = (ScratchPad << :rhs; :value) + ScratchPad.recorded.should == [:rhs, :module] + end + end + + ruby_version_is '3.2' do + it 'evaluates expressions left to right when assignment with compounded constant' do + m = Module.new + ScratchPad.record [] + + (ScratchPad << :module; m)::A = (ScratchPad << :rhs; :value) + ScratchPad.recorded.should == [:module, :rhs] + end + end + + it 'raises TypeError after evaluation of right-hand-side when compounded constant module is not a module' do + ScratchPad.record [] + + -> { + (:not_a_module)::A = (ScratchPad << :rhs; :value) + }.should raise_error(TypeError) + + ScratchPad.recorded.should == [:rhs] + end + end + end + + describe 'using +=' do + describe 'using an accessor' do + before do + klass = Class.new { attr_accessor :b } + @a = klass.new + end + + it 'does evaluate receiver only once when assigns' do + ScratchPad.record [] + @a.b = 1 + + (ScratchPad << :evaluated; @a).b += 2 + + ScratchPad.recorded.should == [:evaluated] + @a.b.should == 3 + end + + it 'ignores method visibility when receiver is self' do + klass_with_private_methods = Class.new do + def initialize(n) @a = n end + def public_method(n); self.a += n end + private + def a; @a end + def a=(n) @a = n; 42 end + end + + a = klass_with_private_methods.new(0) + a.public_method(2).should == 2 + end + end + + describe 'using a #[]' do + before do + klass = Class.new do + def [](k) + @hash ||= {} + @hash[k] + end + + def []=(k, v) + @hash ||= {} + @hash[k] = v + 7 + end + end + @b = klass.new + end + + it 'evaluates receiver only once when assigns' do + ScratchPad.record [] + a = {k: 1} + + (ScratchPad << :evaluated; a)[:k] += 2 + + ScratchPad.recorded.should == [:evaluated] + a[:k].should == 3 + end + + it 'ignores method visibility when receiver is self' do + klass_with_private_methods = Class.new do + def initialize(h) @a = h end + def public_method(k, n); self[k] += n end + private + def [](k) @a[k] end + def []=(k, v) @a[k] = v; 42 end + end + + a = klass_with_private_methods.new(k: 0) + a.public_method(:k, 2).should == 2 + end + + context 'splatted argument' do + it 'correctly handles it' do + @b[:m] = 10 + (@b[*[:m]] += 10).should == 20 + @b[:m].should == 20 + + @b[:n] = 10 + (@b[*(1; [:n])] += 10).should == 20 + @b[:n].should == 20 + + @b[:k] = 10 + (@b[*begin 1; [:k] end] += 10).should == 20 + @b[:k].should == 20 + end + + it 'calls #to_a only once' do + k = Object.new + def k.to_a + ScratchPad << :to_a + [:k] + end + + ScratchPad.record [] + @b[:k] = 10 + (@b[*k] += 10).should == 20 + @b[:k].should == 20 + ScratchPad.recorded.should == [:to_a] + end + + it 'correctly handles a nested splatted argument' do + @b[:k] = 10 + (@b[*[*[:k]]] += 10).should == 20 + @b[:k].should == 20 + end + + it 'correctly handles multiple nested splatted arguments' do + klass_with_multiple_parameters = Class.new do + def [](k1, k2, k3) + @hash ||= {} + @hash[:"#{k1}#{k2}#{k3}"] + end + + def []=(k1, k2, k3, v) + @hash ||= {} + @hash[:"#{k1}#{k2}#{k3}"] = v + 7 + end + end + a = klass_with_multiple_parameters.new + + a[:a, :b, :c] = 10 + (a[*[:a], *[:b], *[:c]] += 10).should == 20 + a[:a, :b, :c].should == 20 + end + end + end + + describe 'using compounded constants' do + it 'causes side-effects of the module part to be applied only once (when assigns)' do + module ConstantSpecs + OpAssignTrue = 1 + end + + suppress_warning do # already initialized constant + x = 0 + (x += 1; ConstantSpecs)::OpAssignTrue += 2 + x.should == 1 + ConstantSpecs::OpAssignTrue.should == 3 + end + + ConstantSpecs.send :remove_const, :OpAssignTrue + end + end + end +end + +# generic cases +describe 'Multiple assignments' do + it 'assigns multiple targets when assignment with an accessor' do + object = Object.new + class << object + attr_accessor :a, :b + end + + object.a, object.b = :a, :b + + object.a.should == :a + object.b.should == :b + end + + it 'assigns multiple targets when assignment with a nested accessor' do + object = Object.new + class << object + attr_accessor :a, :b + end + + (object.a, object.b), c = [:a, :b], nil + + object.a.should == :a + object.b.should == :b + end + + it 'assigns multiple targets when assignment with a #[]=' do + object = Object.new + class << object + def []=(k, v) (@h ||= {})[k] = v; end + def [](k) (@h ||= {})[k]; end + end + + object[:a], object[:b] = :a, :b + + object[:a].should == :a + object[:b].should == :b + end + + it 'assigns multiple targets when assignment with a nested #[]=' do + object = Object.new + class << object + def []=(k, v) (@h ||= {})[k] = v; end + def [](k) (@h ||= {})[k]; end + end + + (object[:a], object[:b]), c = [:v1, :v2], nil + + object[:a].should == :v1 + object[:b].should == :v2 + end + + it 'assigns multiple targets when assignment with compounded constant' do + m = Module.new + + m::A, m::B = :a, :b + + m::A.should == :a + m::B.should == :b + end + + it 'assigns multiple targets when assignment with a nested compounded constant' do + m = Module.new + + (m::A, m::B), c = [:a, :b], nil + + m::A.should == :a + m::B.should == :b + end +end + +describe 'Multiple assignments' do + describe 'evaluation order' do + ruby_version_is ''...'3.1' do + it 'evaluates expressions right to left when assignment with an accessor' do + object = Object.new + def object.a=(value) end + ScratchPad.record [] + + (ScratchPad << :a; object).a, (ScratchPad << :b; object).a = (ScratchPad << :c; :c), (ScratchPad << :d; :d) + ScratchPad.recorded.should == [:c, :d, :a, :b] + end + + it 'evaluates expressions right to left when assignment with a nested accessor' do + object = Object.new + def object.a=(value) end + ScratchPad.record [] + + ((ScratchPad << :a; object).a, foo), bar = [(ScratchPad << :b; :b)] + ScratchPad.recorded.should == [:b, :a] + end + end + + ruby_version_is '3.1' do + it 'evaluates expressions left to right when assignment with an accessor' do + object = Object.new + def object.a=(value) end + ScratchPad.record [] + + (ScratchPad << :a; object).a, (ScratchPad << :b; object).a = (ScratchPad << :c; :c), (ScratchPad << :d; :d) + ScratchPad.recorded.should == [:a, :b, :c, :d] + end + + it 'evaluates expressions left to right when assignment with a nested accessor' do + object = Object.new + def object.a=(value) end + ScratchPad.record [] + + ((ScratchPad << :a; object).a, foo), bar = [(ScratchPad << :b; :b)] + ScratchPad.recorded.should == [:a, :b] + end + + it 'evaluates expressions left to right when assignment with a deeply nested accessor' do + o = Object.new + def o.a=(value) end + def o.b=(value) end + def o.c=(value) end + def o.d=(value) end + def o.e=(value) end + def o.f=(value) end + ScratchPad.record [] + + (ScratchPad << :a; o).a, + ((ScratchPad << :b; o).b, + ((ScratchPad << :c; o).c, (ScratchPad << :d; o).d), + (ScratchPad << :e; o).e), + (ScratchPad << :f; o).f = (ScratchPad << :value; :value) + + ScratchPad.recorded.should == [:a, :b, :c, :d, :e, :f, :value] + end + end + + ruby_version_is ''...'3.1' do + it 'evaluates expressions right to left when assignment with a #[]=' do + object = Object.new + def object.[]=(_, _) end + ScratchPad.record [] + + (ScratchPad << :a; object)[(ScratchPad << :b; :b)], (ScratchPad << :c; object)[(ScratchPad << :d; :d)] = (ScratchPad << :e; :e), (ScratchPad << :f; :f) + ScratchPad.recorded.should == [:e, :f, :a, :b, :c, :d] + end + + it 'evaluates expressions right to left when assignment with a nested #[]=' do + object = Object.new + def object.[]=(_, _) end + ScratchPad.record [] + + ((ScratchPad << :a; object)[(ScratchPad << :b; :b)], foo), bar = [(ScratchPad << :c; :c)] + ScratchPad.recorded.should == [:c, :a, :b] + end + end + + ruby_version_is '3.1' do + it 'evaluates expressions left to right when assignment with a #[]=' do + object = Object.new + def object.[]=(_, _) end + ScratchPad.record [] + + (ScratchPad << :a; object)[(ScratchPad << :b; :b)], (ScratchPad << :c; object)[(ScratchPad << :d; :d)] = (ScratchPad << :e; :e), (ScratchPad << :f; :f) + ScratchPad.recorded.should == [:a, :b, :c, :d, :e, :f] + end + + it 'evaluates expressions left to right when assignment with a nested #[]=' do + object = Object.new + def object.[]=(_, _) end + ScratchPad.record [] + + ((ScratchPad << :a; object)[(ScratchPad << :b; :b)], foo), bar = [(ScratchPad << :c; :c)] + ScratchPad.recorded.should == [:a, :b, :c] + end + + it 'evaluates expressions left to right when assignment with a deeply nested #[]=' do + o = Object.new + def o.[]=(_, _) end + ScratchPad.record [] + + (ScratchPad << :ra; o)[(ScratchPad << :aa; :aa)], + ((ScratchPad << :rb; o)[(ScratchPad << :ab; :ab)], + ((ScratchPad << :rc; o)[(ScratchPad << :ac; :ac)], (ScratchPad << :rd; o)[(ScratchPad << :ad; :ad)]), + (ScratchPad << :re; o)[(ScratchPad << :ae; :ae)]), + (ScratchPad << :rf; o)[(ScratchPad << :af; :af)] = (ScratchPad << :value; :value) + + ScratchPad.recorded.should == [:ra, :aa, :rb, :ab, :rc, :ac, :rd, :ad, :re, :ae, :rf, :af, :value] + end + end + + ruby_version_is ''...'3.2' do + it 'evaluates expressions right to left when assignment with compounded constant' do + m = Module.new + ScratchPad.record [] + + (ScratchPad << :a; m)::A, (ScratchPad << :b; m)::B = (ScratchPad << :c; :c), (ScratchPad << :d; :d) + ScratchPad.recorded.should == [:c, :d, :a, :b] + end + end + + ruby_version_is '3.2' do + it 'evaluates expressions left to right when assignment with compounded constant' do + m = Module.new + ScratchPad.record [] + + (ScratchPad << :a; m)::A, (ScratchPad << :b; m)::B = (ScratchPad << :c; :c), (ScratchPad << :d; :d) + ScratchPad.recorded.should == [:a, :b, :c, :d] + end + + it 'evaluates expressions left to right when assignment with a nested compounded constant' do + m = Module.new + ScratchPad.record [] + + ((ScratchPad << :a; m)::A, foo), bar = [(ScratchPad << :b; :b)] + ScratchPad.recorded.should == [:a, :b] + end + + it 'evaluates expressions left to right when assignment with deeply nested compounded constants' do + m = Module.new + ScratchPad.record [] + + (ScratchPad << :a; m)::A, + ((ScratchPad << :b; m)::B, + ((ScratchPad << :c; m)::C, (ScratchPad << :d; m)::D), + (ScratchPad << :e; m)::E), + (ScratchPad << :f; m)::F = (ScratchPad << :value; :value) + + ScratchPad.recorded.should == [:a, :b, :c, :d, :e, :f, :value] + end + end + end + + context 'when assignment with method call and receiver is self' do + it 'assigns values correctly when assignment with accessor' do + object = Object.new + class << object + attr_accessor :a, :b + + def assign(v1, v2) + self.a, self.b = v1, v2 + end + end + + object.assign :v1, :v2 + object.a.should == :v1 + object.b.should == :v2 + end + + it 'evaluates expressions right to left when assignment with a nested accessor' do + object = Object.new + class << object + attr_accessor :a, :b + + def assign(v1, v2) + (self.a, self.b), c = [v1, v2], nil + end + end + + object.assign :v1, :v2 + object.a.should == :v1 + object.b.should == :v2 + end + + it 'assigns values correctly when assignment with a #[]=' do + object = Object.new + class << object + def []=(key, v) + @h ||= {} + @h[key] = v + end + + def [](key) + (@h || {})[key] + end + + def assign(k1, v1, k2, v2) + self[k1], self[k2] = v1, v2 + end + end + + object.assign :k1, :v1, :k2, :v2 + object[:k1].should == :v1 + object[:k2].should == :v2 + end + + it 'assigns values correctly when assignment with a nested #[]=' do + object = Object.new + class << object + def []=(key, v) + @h ||= {} + @h[key] = v + end + + def [](key) + (@h || {})[key] + end + + def assign(k1, v1, k2, v2) + (self[k1], self[k2]), c = [v1, v2], nil + end + end + + object.assign :k1, :v1, :k2, :v2 + object[:k1].should == :v1 + object[:k2].should == :v2 + end + + it 'assigns values correctly when assignment with compounded constant' do + m = Module.new + m.module_exec do + self::A, self::B = :v1, :v2 + end + + m::A.should == :v1 + m::B.should == :v2 + end + + it 'assigns values correctly when assignment with a nested compounded constant' do + m = Module.new + m.module_exec do + (self::A, self::B), c = [:v1, :v2], nil + end + + m::A.should == :v1 + m::B.should == :v2 + end + end +end diff --git a/spec/ruby/language/block_spec.rb b/spec/ruby/language/block_spec.rb index b2a3cc84c4..578d9cb3b0 100644 --- a/spec/ruby/language/block_spec.rb +++ b/spec/ruby/language/block_spec.rb @@ -40,80 +40,73 @@ describe "A block yielded a single" do m([1, 2]) { |a=5, b, c, d| [a, b, c, d] }.should == [5, 1, 2, nil] end - it "assigns elements to required arguments when a keyword rest argument is present" do - m([1, 2]) { |a, **k| [a, k] }.should == [1, {}] + it "assigns elements to pre arguments" do + m([1, 2]) { |a, b, c, d=5| [a, b, c, d] }.should == [1, 2, nil, 5] end - ruby_version_is ''..."3.0" do - it "assigns elements to mixed argument types" do - suppress_keyword_warning do - result = m([1, 2, 3, {x: 9}]) { |a, b=5, *c, d, e: 2, **k| [a, b, c, d, e, k] } - result.should == [1, 2, [], 3, 2, {x: 9}] - end - end + it "assigns elements to pre and post arguments" do + m([1 ]) { |a, b=5, c=6, d, e| [a, b, c, d, e] }.should == [1, 5, 6, nil, nil] + m([1, 2 ]) { |a, b=5, c=6, d, e| [a, b, c, d, e] }.should == [1, 5, 6, 2, nil] + m([1, 2, 3 ]) { |a, b=5, c=6, d, e| [a, b, c, d, e] }.should == [1, 5, 6, 2, 3] + m([1, 2, 3, 4 ]) { |a, b=5, c=6, d, e| [a, b, c, d, e] }.should == [1, 2, 6, 3, 4] + m([1, 2, 3, 4, 5 ]) { |a, b=5, c=6, d, e| [a, b, c, d, e] }.should == [1, 2, 3, 4, 5] + m([1, 2, 3, 4, 5, 6]) { |a, b=5, c=6, d, e| [a, b, c, d, e] }.should == [1, 2, 3, 4, 5] + end - it "assigns symbol keys from a Hash to keyword arguments" do - suppress_keyword_warning do - result = m(["a" => 1, a: 10]) { |a=nil, **b| [a, b] } - result.should == [{"a" => 1}, a: 10] - end + it "assigns elements to pre and post arguments when *rest is present" do + m([1 ]) { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.should == [1, 5, 6, [], nil, nil] + m([1, 2 ]) { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.should == [1, 5, 6, [], 2, nil] + m([1, 2, 3 ]) { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.should == [1, 5, 6, [], 2, 3] + m([1, 2, 3, 4 ]) { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.should == [1, 2, 6, [], 3, 4] + m([1, 2, 3, 4, 5 ]) { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.should == [1, 2, 3, [], 4, 5] + m([1, 2, 3, 4, 5, 6]) { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.should == [1, 2, 3, [4], 5, 6] + end + + ruby_version_is "3.2" do + it "does not autosplat single argument to required arguments when a keyword rest argument is present" do + m([1, 2]) { |a, **k| [a, k] }.should == [[1, 2], {}] end - it "assigns symbol keys from a Hash returned by #to_hash to keyword arguments" do - suppress_keyword_warning do - obj = mock("coerce block keyword arguments") - obj.should_receive(:to_hash).and_return({"a" => 1, b: 2}) + it "does not autosplat single argument to required arguments when keyword arguments are present" do + m([1, 2]) { |a, b: :b, c: :c| [a, b, c] }.should == [[1, 2], :b, :c] + end - result = m([obj]) { |a=nil, **b| [a, b] } - result.should == [{"a" => 1}, b: 2] - end + it "raises error when required keyword arguments are present" do + -> { + m([1, 2]) { |a, b:, c:| [a, b, c] } + }.should raise_error(ArgumentError, "missing keywords: :b, :c") end end - ruby_version_is "3.0" do - it "assigns elements to mixed argument types" do - result = m([1, 2, 3, {x: 9}]) { |a, b=5, *c, d, e: 2, **k| [a, b, c, d, e, k] } - result.should == [1, 2, [3], {x: 9}, 2, {}] + ruby_version_is ''..."3.2" do + # https://bugs.ruby-lang.org/issues/18633 + it "autosplats single argument to required arguments when a keyword rest argument is present" do + m([1, 2]) { |a, **k| [a, k] }.should == [1, {}] end - it "does not treat final Hash as keyword arguments and does not autosplat" do - result = m(["a" => 1, a: 10]) { |a=nil, **b| [a, b] } - result.should == [[{"a" => 1, a: 10}], {}] + it "autosplats single argument to required arguments when optional keyword arguments are present" do + m([1, 2]) { |a, b: :b, c: :c| [a, b, c] }.should == [1, :b, :c] end - it "does not call #to_hash on final argument to get keyword arguments and does not autosplat" do - suppress_keyword_warning do - obj = mock("coerce block keyword arguments") - obj.should_not_receive(:to_hash) - - result = m([obj]) { |a=nil, **b| [a, b] } - result.should == [[obj], {}] - end + it "raises error when required keyword arguments are present" do + -> { + m([1, 2]) { |a, b:, c:| [a, b, c] } + }.should raise_error(ArgumentError, "missing keywords: :b, :c") end end - ruby_version_is ""..."2.7" do - it "calls #to_hash on the argument and uses resulting hash as first argument when optional argument and keyword argument accepted" do - obj = mock("coerce block keyword arguments") - obj.should_receive(:to_hash).and_return({"a" => 1, "b" => 2}) - - result = m([obj]) { |a=nil, **b| [a, b] } - result.should == [{"a" => 1, "b" => 2}, {}] - end + it "assigns elements to mixed argument types" do + result = m([1, 2, 3, {x: 9}]) { |a, b=5, *c, d, e: 2, **k| [a, b, c, d, e, k] } + result.should == [1, 2, [3], {x: 9}, 2, {}] end - ruby_version_is "2.7"...'3.0' do - it "calls #to_hash on the argument but ignores result when optional argument and keyword argument accepted" do - obj = mock("coerce block keyword arguments") - obj.should_receive(:to_hash).and_return({"a" => 1, "b" => 2}) - - result = m([obj]) { |a=nil, **b| [a, b] } - result.should == [obj, {}] - end + it "does not treat final Hash as keyword arguments and does not autosplat" do + result = m(["a" => 1, a: 10]) { |a=nil, **b| [a, b] } + result.should == [[{"a" => 1, a: 10}], {}] end - ruby_version_is "3.0" do - it "does not call #to_hash on the argument when optional argument and keyword argument accepted and does not autosplat" do + it "does not call #to_hash on final argument to get keyword arguments and does not autosplat" do + suppress_keyword_warning do obj = mock("coerce block keyword arguments") obj.should_not_receive(:to_hash) @@ -122,102 +115,42 @@ describe "A block yielded a single" do end end - describe "when non-symbol keys are in a keyword arguments Hash" do - ruby_version_is ""..."3.0" do - it "separates non-symbol keys and symbol keys" do - suppress_keyword_warning do - result = m(["a" => 10, b: 2]) { |a=nil, **b| [a, b] } - result.should == [{"a" => 10}, {b: 2}] - end - end - end - ruby_version_is "3.0" do - it "does not separate non-symbol keys and symbol keys and does not autosplat" do - suppress_keyword_warning do - result = m(["a" => 10, b: 2]) { |a=nil, **b| [a, b] } - result.should == [[{"a" => 10, b: 2}], {}] - end - end - end - end - - ruby_version_is ""..."3.0" do - it "does not treat hashes with string keys as keyword arguments" do - result = m(["a" => 10]) { |a = nil, **b| [a, b] } - result.should == [{"a" => 10}, {}] - end - end + it "does not call #to_hash on the argument when optional argument and keyword argument accepted and does not autosplat" do + obj = mock("coerce block keyword arguments") + obj.should_not_receive(:to_hash) - ruby_version_is "3.0" do - it "does not treat hashes with string keys as keyword arguments and does not autosplat" do - result = m(["a" => 10]) { |a = nil, **b| [a, b] } - result.should == [[{"a" => 10}], {}] - end + result = m([obj]) { |a=nil, **b| [a, b] } + result.should == [[obj], {}] end - ruby_version_is ''...'3.0' do - it "calls #to_hash on the last element if keyword arguments are present" do - suppress_keyword_warning do - obj = mock("destructure block keyword arguments") - obj.should_receive(:to_hash).and_return({x: 9}) - - result = m([1, 2, 3, obj]) { |a, *b, c, **k| [a, b, c, k] } - result.should == [1, [2], 3, {x: 9}] - end - end - - it "assigns the last element to a non-keyword argument if #to_hash returns nil" do - suppress_keyword_warning do - obj = mock("destructure block keyword arguments") - obj.should_receive(:to_hash).and_return(nil) - - result = m([1, 2, 3, obj]) { |a, *b, c, **k| [a, b, c, k] } - result.should == [1, [2, 3], obj, {}] - end - end - - it "calls #to_hash on the last element when there are more arguments than parameters" do + describe "when non-symbol keys are in a keyword arguments Hash" do + it "does not separate non-symbol keys and symbol keys and does not autosplat" do suppress_keyword_warning do - x = mock("destructure matching block keyword argument") - x.should_receive(:to_hash).and_return({x: 9}) - - result = m([1, 2, 3, {y: 9}, 4, 5, x]) { |a, b=5, c, **k| [a, b, c, k] } - result.should == [1, 2, 3, {x: 9}] + result = m(["a" => 10, b: 2]) { |a=nil, **b| [a, b] } + result.should == [[{"a" => 10, b: 2}], {}] end end + end - it "raises a TypeError if #to_hash does not return a Hash" do - obj = mock("destructure block keyword arguments") - obj.should_receive(:to_hash).and_return(1) - - -> { m([1, 2, 3, obj]) { |a, *b, c, **k| } }.should raise_error(TypeError) - end - - it "raises the error raised inside #to_hash" do - obj = mock("destructure block keyword arguments") - error = RuntimeError.new("error while converting to a hash") - obj.should_receive(:to_hash).and_raise(error) - - -> { m([1, 2, 3, obj]) { |a, *b, c, **k| } }.should raise_error(error) - end + it "does not treat hashes with string keys as keyword arguments and does not autosplat" do + result = m(["a" => 10]) { |a = nil, **b| [a, b] } + result.should == [[{"a" => 10}], {}] end - ruby_version_is '3.0' do - it "does not call #to_hash on the last element if keyword arguments are present" do - obj = mock("destructure block keyword arguments") - obj.should_not_receive(:to_hash) + it "does not call #to_hash on the last element if keyword arguments are present" do + obj = mock("destructure block keyword arguments") + obj.should_not_receive(:to_hash) - result = m([1, 2, 3, obj]) { |a, *b, c, **k| [a, b, c, k] } - result.should == [1, [2, 3], obj, {}] - end + result = m([1, 2, 3, obj]) { |a, *b, c, **k| [a, b, c, k] } + result.should == [1, [2, 3], obj, {}] + end - it "does not call #to_hash on the last element when there are more arguments than parameters" do - x = mock("destructure matching block keyword argument") - x.should_not_receive(:to_hash) + it "does not call #to_hash on the last element when there are more arguments than parameters" do + x = mock("destructure matching block keyword argument") + x.should_not_receive(:to_hash) - result = m([1, 2, 3, {y: 9}, 4, 5, x]) { |a, b=5, c, **k| [a, b, c, k] } - result.should == [1, 2, 3, {}] - end + result = m([1, 2, 3, {y: 9}, 4, 5, x]) { |a, b=5, c, **k| [a, b, c, k] } + result.should == [1, 2, 3, {}] end it "does not call #to_ary on the Array" do @@ -264,12 +197,55 @@ describe "A block yielded a single" do m(obj) { |a, b, c| [a, b, c] }.should == [obj, nil, nil] end + it "receives the object if it does not respond to #to_ary" do + obj = Object.new + + m(obj) { |a, b, c| [a, b, c] }.should == [obj, nil, nil] + end + + it "calls #respond_to? to check if object has method #to_ary" do + obj = mock("destructure block arguments") + obj.should_receive(:respond_to?).with(:to_ary, true).and_return(true) + obj.should_receive(:to_ary).and_return([1, 2]) + + m(obj) { |a, b, c| [a, b, c] }.should == [1, 2, nil] + end + + it "receives the object if it does not respond to #respond_to?" do + obj = BasicObject.new + + m(obj) { |a, b, c| [a, b, c] }.should == [obj, nil, nil] + end + + it "calls #to_ary on the object when it is defined dynamically" do + obj = Object.new + def obj.method_missing(name, *args, &block) + if name == :to_ary + [1, 2] + else + super + end + end + def obj.respond_to_missing?(name, include_private) + name == :to_ary + end + + m(obj) { |a, b, c| [a, b, c] }.should == [1, 2, nil] + end + it "raises a TypeError if #to_ary does not return an Array" do obj = mock("destructure block arguments") obj.should_receive(:to_ary).and_return(1) -> { m(obj) { |a, b| } }.should raise_error(TypeError) end + + it "raises error transparently if #to_ary raises error on its own" do + obj = Object.new + def obj.to_ary; raise "Exception raised in #to_ary" end + + -> { m(obj) { |a, b| } }.should raise_error(RuntimeError, "Exception raised in #to_ary") + end end end @@ -434,7 +410,6 @@ describe "A block" do -> { @y.s(obj) { |a, b| } }.should raise_error(ZeroDivisionError) end - end describe "taking |a, *b| arguments" do @@ -767,6 +742,42 @@ describe "A block" do eval("Proc.new { |_,_| }").should be_an_instance_of(Proc) end end + + describe 'pre and post parameters' do + it "assigns nil to unassigned required arguments" do + proc { |a, *b, c, d| [a, b, c, d] }.call(1, 2).should == [1, [], 2, nil] + end + + it "assigns elements to optional arguments" do + proc { |a=5, b=4, c=3| [a, b, c] }.call(1, 2).should == [1, 2, 3] + end + + it "assigns elements to post arguments" do + proc { |a=5, b, c, d| [a, b, c, d] }.call(1, 2).should == [5, 1, 2, nil] + end + + it "assigns elements to pre arguments" do + proc { |a, b, c, d=5| [a, b, c, d] }.call(1, 2).should == [1, 2, nil, 5] + end + + it "assigns elements to pre and post arguments" do + proc { |a, b=5, c=6, d, e| [a, b, c, d, e] }.call(1 ).should == [1, 5, 6, nil, nil] + proc { |a, b=5, c=6, d, e| [a, b, c, d, e] }.call(1, 2 ).should == [1, 5, 6, 2, nil] + proc { |a, b=5, c=6, d, e| [a, b, c, d, e] }.call(1, 2, 3 ).should == [1, 5, 6, 2, 3] + proc { |a, b=5, c=6, d, e| [a, b, c, d, e] }.call(1, 2, 3, 4 ).should == [1, 2, 6, 3, 4] + proc { |a, b=5, c=6, d, e| [a, b, c, d, e] }.call(1, 2, 3, 4, 5 ).should == [1, 2, 3, 4, 5] + proc { |a, b=5, c=6, d, e| [a, b, c, d, e] }.call(1, 2, 3, 4, 5, 6).should == [1, 2, 3, 4, 5] + end + + it "assigns elements to pre and post arguments when *rest is present" do + proc { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.call(1 ).should == [1, 5, 6, [], nil, nil] + proc { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.call(1, 2 ).should == [1, 5, 6, [], 2, nil] + proc { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.call(1, 2, 3 ).should == [1, 5, 6, [], 2, 3] + proc { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.call(1, 2, 3, 4 ).should == [1, 2, 6, [], 3, 4] + proc { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.call(1, 2, 3, 4, 5 ).should == [1, 2, 3, [], 4, 5] + proc { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.call(1, 2, 3, 4, 5, 6).should == [1, 2, 3, [4], 5, 6] + end + end end describe "Block-local variables" do @@ -949,38 +960,18 @@ describe "Post-args" do end describe "with a circular argument reference" do - ruby_version_is ''...'2.7' do - it "warns and uses a nil value when there is an existing local variable with same name" do - a = 1 - -> { - @proc = eval "proc { |a=a| a }" - }.should complain(/circular argument reference/) - @proc.call.should == nil - end - - it "warns and uses a nil value when there is an existing method with same name" do - def a; 1; end - -> { - @proc = eval "proc { |a=a| a }" - }.should complain(/circular argument reference/) - @proc.call.should == nil - end + it "raises a SyntaxError if using an existing local with the same name as the argument" do + a = 1 + -> { + @proc = eval "proc { |a=a| a }" + }.should raise_error(SyntaxError) end - ruby_version_is '2.7' do - it "raises a SyntaxError if using an existing local with the same name as the argument" do - a = 1 - -> { - @proc = eval "proc { |a=a| a }" - }.should raise_error(SyntaxError) - end - - it "raises a SyntaxError if there is an existing method with the same name as the argument" do - def a; 1; end - -> { - @proc = eval "proc { |a=a| a }" - }.should raise_error(SyntaxError) - end + it "raises a SyntaxError if there is an existing method with the same name as the argument" do + def a; 1; end + -> { + @proc = eval "proc { |a=a| a }" + }.should raise_error(SyntaxError) end it "calls an existing method with the same name as the argument if explicitly using ()" do @@ -1004,3 +995,77 @@ describe "Post-args" do end end end + +describe "Anonymous block forwarding" do + ruby_version_is "3.1" do + it "forwards blocks to other method that formally declares anonymous block" do + eval <<-EOF + def b(&); c(&) end + def c(&); yield :non_null end + EOF + + b { |c| c }.should == :non_null + end + + it "requires the anonymous block parameter to be declared if directly passing a block" do + -> { eval "def a; b(&); end; def b; end" }.should raise_error(SyntaxError) + end + + it "works when it's the only declared parameter" do + eval <<-EOF + def inner; yield end + def block_only(&); inner(&) end + EOF + + block_only { 1 }.should == 1 + end + + it "works alongside positional parameters" do + eval <<-EOF + def inner; yield end + def pos(arg1, &); inner(&) end + EOF + + pos(:a) { 1 }.should == 1 + end + + it "works alongside positional arguments and splatted keyword arguments" do + eval <<-EOF + def inner; yield end + def pos_kwrest(arg1, **kw, &); inner(&) end + EOF + + pos_kwrest(:a, arg: 3) { 1 }.should == 1 + end + + it "works alongside positional arguments and disallowed keyword arguments" do + eval <<-EOF + def inner; yield end + def no_kw(arg1, **nil, &); inner(&) end + EOF + + no_kw(:a) { 1 }.should == 1 + end + end + + ruby_version_is "3.2" do + it "works alongside explicit keyword arguments" do + eval <<-EOF + def inner; yield end + def rest_kw(*a, kwarg: 1, &); inner(&) end + def kw(kwarg: 1, &); inner(&) end + def pos_kw_kwrest(arg1, kwarg: 1, **kw, &); inner(&) end + def pos_rkw(arg1, kwarg1:, &); inner(&) end + def all(arg1, arg2, *rest, post1, post2, kw1: 1, kw2: 2, okw1:, okw2:, &); inner(&) end + def all_kwrest(arg1, arg2, *rest, post1, post2, kw1: 1, kw2: 2, okw1:, okw2:, **kw, &); inner(&) end + EOF + + rest_kw { 1 }.should == 1 + kw { 1 }.should == 1 + pos_kw_kwrest(:a) { 1 }.should == 1 + pos_rkw(:a, kwarg1: 3) { 1 }.should == 1 + all(:a, :b, :c, :d, :e, okw1: 'x', okw2: 'y') { 1 }.should == 1 + all_kwrest(:a, :b, :c, :d, :e, okw1: 'x', okw2: 'y') { 1 }.should == 1 + end + end +end diff --git a/spec/ruby/language/break_spec.rb b/spec/ruby/language/break_spec.rb index 627cb4a071..e725e77e80 100644 --- a/spec/ruby/language/break_spec.rb +++ b/spec/ruby/language/break_spec.rb @@ -372,7 +372,7 @@ describe "Executing break from within a block" do end.should_not raise_error end - it "raises LocalJumpError when converted into a proc during a a super call" do + it "raises LocalJumpError when converted into a proc during a super call" do cls1 = Class.new { def foo(&b); b; end } cls2 = Class.new(cls1) { def foo; super { break 1 }.call; end } diff --git a/spec/ruby/language/case_spec.rb b/spec/ruby/language/case_spec.rb index 410cb9afb1..3262f09dd5 100644 --- a/spec/ruby/language/case_spec.rb +++ b/spec/ruby/language/case_spec.rb @@ -103,7 +103,7 @@ describe "The 'case'-construct" do $1.should == "42" end - it "tests with a regexp interpolated within another regexp" do + it "tests with a string interpolated in a regexp" do digits = '\d+' case "foo44" when /oo(#{digits})/ @@ -116,7 +116,7 @@ describe "The 'case'-construct" do $1.should == "44" end - it "tests with a string interpolated in a regexp" do + it "tests with a regexp interpolated within another regexp" do digits_regexp = /\d+/ case "foo43" when /oo(#{digits_regexp})/ @@ -156,6 +156,15 @@ describe "The 'case'-construct" do end.should == "foo" end + it "tests an empty array" do + case [] + when [] + 'foo' + else + 'bar' + end.should == 'foo' + end + it "expands arrays to lists of values" do case 'z' when *['a', 'b', 'c', 'd'] @@ -320,49 +329,6 @@ describe "The 'case'-construct" do 100 end.should == 100 end -end - -describe "The 'case'-construct with no target expression" do - it "evaluates the body of the first clause when at least one of its condition expressions is true" do - case - when true, false; 'foo' - end.should == 'foo' - end - - it "evaluates the body of the first when clause that is not false/nil" do - case - when false; 'foo' - when 2; 'bar' - when 1 == 1; 'baz' - end.should == 'bar' - - case - when false; 'foo' - when nil; 'foo' - when 1 == 1; 'bar' - end.should == 'bar' - end - - it "evaluates the body of the else clause if all when clauses are false/nil" do - case - when false; 'foo' - when nil; 'foo' - when 1 == 2; 'bar' - else 'baz' - end.should == 'baz' - end - - it "evaluates multiple conditional expressions as a boolean disjunction" do - case - when true, false; 'foo' - else 'bar' - end.should == 'foo' - - case - when false, true; 'foo' - else 'bar' - end.should == 'foo' - end it "evaluates true as only 'true' when true is the first clause" do case 1 @@ -425,6 +391,87 @@ describe "The 'case'-construct with no target expression" do end.should == :called end + it "only matches last value in complex expressions within ()" do + case 'a' + when ('a'; 'b') + :wrong_called + when ('b'; 'a') + :called + end.should == :called + end + + it "supports declaring variables in the case target expression" do + def test(v) + case new_variable_in_expression = v + when true + # This extra block is a test that `new_variable_in_expression` is declared outside of it and not inside + self.then { new_variable_in_expression } + else + # Same + self.then { new_variable_in_expression.casecmp?("foo") } + end + end + + self.test("bar").should == false + self.test(true).should == true + end + + it "warns if there are identical when clauses" do + -> { + eval <<~RUBY + case 1 + when 2 + :foo + when 2 + :bar + end + RUBY + }.should complain(/warning: duplicated .when' clause with line \d+ is ignored/, verbose: true) + end +end + +describe "The 'case'-construct with no target expression" do + it "evaluates the body of the first clause when at least one of its condition expressions is true" do + case + when true, false; 'foo' + end.should == 'foo' + end + + it "evaluates the body of the first when clause that is not false/nil" do + case + when false; 'foo' + when 2; 'bar' + when 1 == 1; 'baz' + end.should == 'bar' + + case + when false; 'foo' + when nil; 'foo' + when 1 == 1; 'bar' + end.should == 'bar' + end + + it "evaluates the body of the else clause if all when clauses are false/nil" do + case + when false; 'foo' + when nil; 'foo' + when 1 == 2; 'bar' + else 'baz' + end.should == 'baz' + end + + it "evaluates multiple conditional expressions as a boolean disjunction" do + case + when true, false; 'foo' + else 'bar' + end.should == 'foo' + + case + when false, true; 'foo' + else 'bar' + end.should == 'foo' + end + # Homogeneous cases are often optimized to avoid === using a jump table, and should be tested separately. # See https://github.com/jruby/jruby/issues/6440 it "handles homogeneous cases" do @@ -433,4 +480,13 @@ describe "The 'case'-construct with no target expression" do when 2; 'bar' end.should == 'foo' end + + it "expands arrays to lists of values" do + case + when *[false] + "foo" + when *[true] + "bar" + end.should == "bar" + end end diff --git a/spec/ruby/language/class_spec.rb b/spec/ruby/language/class_spec.rb index 83db164e1a..eab3cd0651 100644 --- a/spec/ruby/language/class_spec.rb +++ b/spec/ruby/language/class_spec.rb @@ -17,6 +17,19 @@ describe "The class keyword" do eval "class ClassSpecsKeywordWithoutSemicolon end" ClassSpecsKeywordWithoutSemicolon.should be_an_instance_of(Class) end + + it "can redefine a class when called from a block" do + ClassSpecs::DEFINE_CLASS.call + A.should be_an_instance_of(Class) + + Object.send(:remove_const, :A) + defined?(A).should be_nil + + ClassSpecs::DEFINE_CLASS.call + A.should be_an_instance_of(Class) + ensure + Object.send(:remove_const, :A) if defined?(::A) + end end describe "A class definition" do @@ -295,20 +308,10 @@ describe "A class definition extending an object (sclass)" do -> { class TestClass < BasicObject.new; end }.should raise_error(TypeError, error_msg) end - ruby_version_is ""..."3.0" do - it "allows accessing the block of the original scope" do - suppress_warning do - ClassSpecs.sclass_with_block { 123 }.should == 123 - end - end - end - - ruby_version_is "3.0" do - it "does not allow accessing the block of the original scope" do - -> { - ClassSpecs.sclass_with_block { 123 } - }.should raise_error(SyntaxError) - end + it "does not allow accessing the block of the original scope" do + -> { + ClassSpecs.sclass_with_block { 123 } + }.should raise_error(SyntaxError) end it "can use return to cause the enclosing method to return" do diff --git a/spec/ruby/language/class_variable_spec.rb b/spec/ruby/language/class_variable_spec.rb index dffab47a6b..a26a3fb8de 100644 --- a/spec/ruby/language/class_variable_spec.rb +++ b/spec/ruby/language/class_variable_spec.rb @@ -82,3 +82,33 @@ describe 'A class variable definition' do c.class_variable_get(:@@cv).should == :next end end + +describe 'Accessing a class variable' do + it "raises a RuntimeError when accessed from the toplevel scope (not in some module or class)" do + -> { + eval "@@cvar_toplevel1" + }.should raise_error(RuntimeError, 'class variable access from toplevel') + -> { + eval "@@cvar_toplevel2 = 2" + }.should raise_error(RuntimeError, 'class variable access from toplevel') + end + + it "does not raise an error when checking if defined from the toplevel scope" do + -> { + eval "defined?(@@cvar_toplevel1)" + }.should_not raise_error + end + + it "raises a RuntimeError when a class variable is overtaken in an ancestor class" do + parent = Class.new() + subclass = Class.new(parent) + subclass.class_variable_set(:@@cvar_overtaken, :subclass) + parent.class_variable_set(:@@cvar_overtaken, :parent) + + -> { + subclass.class_variable_get(:@@cvar_overtaken) + }.should raise_error(RuntimeError, /class variable @@cvar_overtaken of .+ is overtaken by .+/) + + parent.class_variable_get(:@@cvar_overtaken).should == :parent + end +end diff --git a/spec/ruby/language/comment_spec.rb b/spec/ruby/language/comment_spec.rb index 690e844dc0..dd788e681c 100644 --- a/spec/ruby/language/comment_spec.rb +++ b/spec/ruby/language/comment_spec.rb @@ -1,15 +1,13 @@ require_relative '../spec_helper' describe "The comment" do - ruby_version_is "2.7" do - it "can be placed between fluent dot now" do - code = <<~CODE - 10 - # some comment - .to_s - CODE + it "can be placed between fluent dot now" do + code = <<~CODE + 10 + # some comment + .to_s + CODE - eval(code).should == '10' - end + eval(code).should == '10' end end diff --git a/spec/ruby/language/constants_spec.rb b/spec/ruby/language/constants_spec.rb index 760b9d4a24..08c534487e 100644 --- a/spec/ruby/language/constants_spec.rb +++ b/spec/ruby/language/constants_spec.rb @@ -135,18 +135,34 @@ describe "Literal (A::X) constant resolution" do ConstantSpecs::ClassB::CS_CONST109.should == :const109_2 end - it "evaluates the right hand side before evaluating a constant path" do - mod = Module.new + ruby_version_is "3.2" do + it "evaluates left-to-right" do + mod = Module.new - mod.module_eval <<-EOC - ConstantSpecsRHS::B = begin - module ConstantSpecsRHS; end + mod.module_eval <<-EOC + order = [] + ConstantSpecsRHS = Module.new + (order << :lhs; ConstantSpecsRHS)::B = (order << :rhs) + EOC - "hello" - end - EOC + mod::ConstantSpecsRHS::B.should == [:lhs, :rhs] + end + end + + ruby_version_is ""..."3.2" do + it "evaluates the right hand side before evaluating a constant path" do + mod = Module.new - mod::ConstantSpecsRHS::B.should == 'hello' + mod.module_eval <<-EOC + ConstantSpecsRHS::B = begin + module ConstantSpecsRHS; end + + "hello" + end + EOC + + mod::ConstantSpecsRHS::B.should == 'hello' + end end end @@ -154,34 +170,32 @@ describe "Literal (A::X) constant resolution" do -> { ConstantSpecs::ParentA::CS_CONSTX }.should raise_error(NameError) end - ruby_version_is "3.0" do - it "uses the module or class #name to craft the error message" do - mod = Module.new do - def self.name - "ModuleName" - end - - def self.inspect - "<unusable info>" - end + it "uses the module or class #name to craft the error message" do + mod = Module.new do + def self.name + "ModuleName" end - -> { mod::DOES_NOT_EXIST }.should raise_error(NameError, /uninitialized constant ModuleName::DOES_NOT_EXIST/) + def self.inspect + "<unusable info>" + end end - it "uses the module or class #inspect to craft the error message if they are anonymous" do - mod = Module.new do - def self.name - nil - end + -> { mod::DOES_NOT_EXIST }.should raise_error(NameError, /uninitialized constant ModuleName::DOES_NOT_EXIST/) + end - def self.inspect - "<unusable info>" - end + it "uses the module or class #inspect to craft the error message if they are anonymous" do + mod = Module.new do + def self.name + nil end - -> { mod::DOES_NOT_EXIST }.should raise_error(NameError, /uninitialized constant <unusable info>::DOES_NOT_EXIST/) + def self.inspect + "<unusable info>" + end end + + -> { mod::DOES_NOT_EXIST }.should raise_error(NameError, /uninitialized constant <unusable info>::DOES_NOT_EXIST/) end it "sends #const_missing to the original class or module scope" do @@ -432,17 +446,15 @@ describe "Module#private_constant marked constants" do -> {mod::Foo}.should raise_error(NameError) end - ruby_version_is "2.6" do - it "sends #const_missing to the original class or module" do - mod = Module.new - mod.const_set :Foo, true - mod.send :private_constant, :Foo - def mod.const_missing(name) - name == :Foo ? name : super - end - - mod::Foo.should == :Foo + it "sends #const_missing to the original class or module" do + mod = Module.new + mod.const_set :Foo, true + mod.send :private_constant, :Foo + def mod.const_missing(name) + name == :Foo ? name : super end + + mod::Foo.should == :Foo end describe "in a module" do @@ -713,20 +725,24 @@ describe 'Allowed characters' do end.should raise_error(NameError, /wrong constant name/) end - ruby_version_is ""..."2.6" do - it 'does not allow not ASCII upcased characters at the beginning' do - -> do - Module.new.const_set("ἍBB", 1) - end.should raise_error(NameError, /wrong constant name/) - end - end + it 'allows not ASCII upcased characters at the beginning' do + mod = Module.new + mod.const_set("ἍBB", 1) - ruby_version_is "2.6" do - it 'allows not ASCII upcased characters at the beginning' do - mod = Module.new - mod.const_set("ἍBB", 1) + eval("mod::ἍBB").should == 1 + end +end - eval("mod::ἍBB").should == 1 +describe 'Assignment' do + context 'dynamic assignment' do + it 'raises SyntaxError' do + -> do + eval <<-CODE + def test + B = 1 + end + CODE + end.should raise_error(SyntaxError, /dynamic constant assignment/) end end end diff --git a/spec/ruby/language/def_spec.rb b/spec/ruby/language/def_spec.rb index 6b0be19d90..42e721c68c 100644 --- a/spec/ruby/language/def_spec.rb +++ b/spec/ruby/language/def_spec.rb @@ -197,32 +197,15 @@ describe "An instance method with a default argument" do foo(2,3,3).should == [2,3,[3]] end - ruby_version_is ''...'2.7' do - it "warns and uses a nil value when there is an existing local method with same name" do - def bar - 1 - end - -> { - eval "def foo(bar = bar) - bar - end" - }.should complain(/circular argument reference/) - foo.should == nil - foo(2).should == 2 - end - end - - ruby_version_is '2.7' do - it "raises a syntaxError an existing method with the same name as the local variable" do - def bar - 1 - end - -> { - eval "def foo(bar = bar) - bar - end" - }.should raise_error(SyntaxError) + it "raises a SyntaxError when there is an existing method with the same name as the local variable" do + def bar + 1 end + -> { + eval "def foo(bar = bar) + bar + end" + }.should raise_error(SyntaxError) end it "calls a method with the same name as the local when explicitly using ()" do @@ -255,7 +238,7 @@ describe "A singleton method definition" do end it "can be declared for a global variable" do - $__a__ = "hi" + $__a__ = +"hi" def $__a__.foo 7 end diff --git a/spec/ruby/language/defined_spec.rb b/spec/ruby/language/defined_spec.rb index f2da8a9293..34408c0190 100644 --- a/spec/ruby/language/defined_spec.rb +++ b/spec/ruby/language/defined_spec.rb @@ -5,21 +5,25 @@ describe "The defined? keyword for literals" do it "returns 'self' for self" do ret = defined?(self) ret.should == "self" + ret.frozen?.should == true end it "returns 'nil' for nil" do ret = defined?(nil) ret.should == "nil" + ret.frozen?.should == true end it "returns 'true' for true" do ret = defined?(true) ret.should == "true" + ret.frozen?.should == true end it "returns 'false' for false" do ret = defined?(false) ret.should == "false" + ret.frozen?.should == true end describe "for a literal Array" do @@ -27,6 +31,7 @@ describe "The defined? keyword for literals" do it "returns 'expression' if each element is defined" do ret = defined?([Object, Array]) ret.should == "expression" + ret.frozen?.should == true end it "returns nil if one element is not defined" do @@ -43,9 +48,25 @@ describe "The defined? keyword for literals" do end describe "The defined? keyword when called with a method name" do + before :each do + ScratchPad.clear + end + + it "does not call the method" do + defined?(DefinedSpecs.side_effects).should == "method" + ScratchPad.recorded.should != :defined_specs_side_effects + end + + it "does not execute the arguments" do + defined?(DefinedSpecs.any_args(DefinedSpecs.side_effects)).should == "method" + ScratchPad.recorded.should != :defined_specs_side_effects + end + describe "without a receiver" do it "returns 'method' if the method is defined" do - defined?(puts).should == "method" + ret = defined?(puts) + ret.should == "method" + ret.frozen?.should == true end it "returns nil if the method is not defined" do @@ -95,6 +116,11 @@ describe "The defined? keyword when called with a method name" do defined?(obj.a_defined_method).should == "method" end + it "returns 'method' for []=" do + a = [] + defined?(a[0] = 1).should == "method" + end + it "returns nil if the method is not defined" do obj = DefinedSpecs::Basic.new defined?(obj.an_undefined_method).should be_nil @@ -159,6 +185,32 @@ describe "The defined? keyword when called with a method name" do ScratchPad.recorded.should == :defined_specs_fixnum_method end end + + describe "having a throw in the receiver" do + it "escapes defined? and performs the throw semantics as normal" do + defined_returned = false + catch(:out) { + # NOTE: defined? behaves differently if it is called in a void context, see below + defined?(throw(:out, 42).foo).should == :unreachable + defined_returned = true + }.should == 42 + defined_returned.should == false + end + end + + describe "in a void context" do + it "does not execute the receiver" do + ScratchPad.record :not_executed + defined?(DefinedSpecs.side_effects / 2) + ScratchPad.recorded.should == :not_executed + end + + it "warns about the void context when parsing it" do + -> { + eval "defined?(DefinedSpecs.side_effects / 2); 42" + }.should complain(/warning: possibly useless use of defined\? in void context/, verbose: true) + end + end end describe "The defined? keyword for an expression" do @@ -167,7 +219,9 @@ describe "The defined? keyword for an expression" do end it "returns 'assignment' for assigning a local variable" do - defined?(x = 2).should == "assignment" + ret = defined?(x = 2) + ret.should == "assignment" + ret.frozen?.should == true end it "returns 'assignment' for assigning an instance variable" do @@ -182,6 +236,14 @@ describe "The defined? keyword for an expression" do defined?(@@defined_specs_x = 2).should == "assignment" end + it "returns 'assignment' for assigning a constant" do + defined?(A = 2).should == "assignment" + end + + it "returns 'assignment' for assigning a fully qualified constant" do + defined?(Object::A = 2).should == "assignment" + end + it "returns 'assignment' for assigning multiple variables" do defined?((a, b = 1, 2)).should == "assignment" end @@ -199,7 +261,27 @@ describe "The defined? keyword for an expression" do end it "returns 'assignment' for an expression with '+='" do - defined?(x += 2).should == "assignment" + defined?(a += 1).should == "assignment" + defined?(@a += 1).should == "assignment" + defined?(@@a += 1).should == "assignment" + defined?($a += 1).should == "assignment" + defined?(A += 1).should == "assignment" + # fully qualified constant check is moved out into a separate test case + defined?(a.b += 1).should == "assignment" + defined?(a[:b] += 1).should == "assignment" + end + + # https://bugs.ruby-lang.org/issues/20111 + ruby_version_is ""..."3.4" do + it "returns 'expression' for an assigning a fully qualified constant with '+='" do + defined?(Object::A += 1).should == "expression" + end + end + + ruby_version_is "3.4" do + it "returns 'assignment' for an assigning a fully qualified constant with '+='" do + defined?(Object::A += 1).should == "assignment" + end end it "returns 'assignment' for an expression with '*='" do @@ -230,12 +312,90 @@ describe "The defined? keyword for an expression" do defined?(x >>= 2).should == "assignment" end - it "returns 'assignment' for an expression with '||='" do - defined?(x ||= 2).should == "assignment" + context "||=" do + it "returns 'assignment' for assigning a local variable with '||='" do + defined?(a ||= true).should == "assignment" + end + + it "returns 'assignment' for assigning an instance variable with '||='" do + defined?(@a ||= true).should == "assignment" + end + + it "returns 'assignment' for assigning a class variable with '||='" do + defined?(@@a ||= true).should == "assignment" + end + + it "returns 'assignment' for assigning a global variable with '||='" do + defined?($a ||= true).should == "assignment" + end + + it "returns 'assignment' for assigning a constant with '||='" do + defined?(A ||= true).should == "assignment" + end + + # https://bugs.ruby-lang.org/issues/20111 + ruby_version_is ""..."3.4" do + it "returns 'expression' for assigning a fully qualified constant with '||='" do + defined?(Object::A ||= true).should == "expression" + end + end + + ruby_version_is "3.4" do + it "returns 'assignment' for assigning a fully qualified constant with '||='" do + defined?(Object::A ||= true).should == "assignment" + end + end + + it "returns 'assignment' for assigning an attribute with '||='" do + defined?(a.b ||= true).should == "assignment" + end + + it "returns 'assignment' for assigning a referenced element with '||='" do + defined?(a[:b] ||= true).should == "assignment" + end end - it "returns 'assignment' for an expression with '&&='" do - defined?(x &&= 2).should == "assignment" + context "&&=" do + it "returns 'assignment' for assigning a local variable with '&&='" do + defined?(a &&= true).should == "assignment" + end + + it "returns 'assignment' for assigning an instance variable with '&&='" do + defined?(@a &&= true).should == "assignment" + end + + it "returns 'assignment' for assigning a class variable with '&&='" do + defined?(@@a &&= true).should == "assignment" + end + + it "returns 'assignment' for assigning a global variable with '&&='" do + defined?($a &&= true).should == "assignment" + end + + it "returns 'assignment' for assigning a constant with '&&='" do + defined?(A &&= true).should == "assignment" + end + + # https://bugs.ruby-lang.org/issues/20111 + ruby_version_is ""..."3.4" do + it "returns 'expression' for assigning a fully qualified constant with '&&='" do + defined?(Object::A &&= true).should == "expression" + end + end + + ruby_version_is "3.4" do + it "returns 'assignment' for assigning a fully qualified constant with '&&='" do + defined?(Object::A &&= true).should == "assignment" + end + end + + it "returns 'assignment' for assigning an attribute with '&&='" do + defined?(a.b &&= true).should == "assignment" + end + + it "returns 'assignment' for assigning a referenced element with '&&='" do + defined?(a[:b] &&= true).should == "assignment" + end end it "returns 'assignment' for an expression with '**='" do @@ -470,7 +630,9 @@ end describe "The defined? keyword for variables" do it "returns 'local-variable' when called with the name of a local variable" do - DefinedSpecs::Basic.new.local_variable_defined.should == "local-variable" + ret = DefinedSpecs::Basic.new.local_variable_defined + ret.should == "local-variable" + ret.frozen?.should == true end it "returns 'local-variable' when called with the name of a local variable assigned to nil" do @@ -486,7 +648,9 @@ describe "The defined? keyword for variables" do end it "returns 'instance-variable' for an instance variable that has been assigned" do - DefinedSpecs::Basic.new.instance_variable_defined.should == "instance-variable" + ret = DefinedSpecs::Basic.new.instance_variable_defined + ret.should == "instance-variable" + ret.frozen?.should == true end it "returns 'instance-variable' for an instance variable that has been assigned to nil" do @@ -502,7 +666,9 @@ describe "The defined? keyword for variables" do end it "returns 'global-variable' for a global variable that has been assigned nil" do - DefinedSpecs::Basic.new.global_variable_defined_as_nil.should == "global-variable" + ret = DefinedSpecs::Basic.new.global_variable_defined_as_nil + ret.should == "global-variable" + ret.frozen?.should == true end # MRI appears to special case defined? for $! and $~ in that it returns @@ -674,7 +840,9 @@ describe "The defined? keyword for variables" do # get to the defined? call so it really has nothing to do with 'defined?'. it "returns 'class variable' when called with the name of a class variable" do - DefinedSpecs::Basic.new.class_variable_defined.should == "class variable" + ret = DefinedSpecs::Basic.new.class_variable_defined + ret.should == "class variable" + ret.frozen?.should == true end it "returns 'local-variable' when called with the name of a block local" do @@ -685,7 +853,9 @@ end describe "The defined? keyword for a simple constant" do it "returns 'constant' when the constant is defined" do - defined?(DefinedSpecs).should == "constant" + ret = defined?(DefinedSpecs) + ret.should == "constant" + ret.frozen?.should == true end it "returns nil when the constant is not defined" do @@ -941,12 +1111,26 @@ describe "The defined? keyword for yield" do end it "returns 'yield' if a block is passed to a method not taking a block parameter" do - DefinedSpecs::Basic.new.yield_block.should == "yield" + ret = DefinedSpecs::Basic.new.yield_block + ret.should == "yield" + ret.frozen?.should == true end it "returns 'yield' if a block is passed to a method taking a block parameter" do DefinedSpecs::Basic.new.yield_block_parameter.should == "yield" end + + it "returns 'yield' when called within a block" do + def yielder + yield + end + + def call_defined + yielder { defined?(yield) } + end + + call_defined() { }.should == "yield" + end end describe "The defined? keyword for super" do @@ -972,7 +1156,9 @@ describe "The defined? keyword for super" do end it "returns 'super' when a superclass method exists" do - DefinedSpecs::Super.new.method_no_args.should == "super" + ret = DefinedSpecs::Super.new.method_no_args + ret.should == "super" + ret.frozen?.should == true end it "returns 'super' from a block when a superclass method exists" do diff --git a/spec/ruby/language/delegation_spec.rb b/spec/ruby/language/delegation_spec.rb index ac7b511f65..d780506421 100644 --- a/spec/ruby/language/delegation_spec.rb +++ b/spec/ruby/language/delegation_spec.rb @@ -1,41 +1,93 @@ require_relative '../spec_helper' require_relative 'fixtures/delegation' -ruby_version_is "2.7" do - describe "delegation with def(...)" do - it "delegates rest and kwargs" do - a = Class.new(DelegationSpecs::Target) +describe "delegation with def(...)" do + it "delegates rest and kwargs" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate(...) + target(...) + end + RUBY + + a.new.delegate(1, b: 2).should == [[1], {b: 2}] + end + + it "delegates block" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate_block(...) + target_block(...) + end + RUBY + + a.new.delegate_block(1, b: 2) { |x| x }.should == [{b: 2}, [1]] + end + + it "parses as open endless Range when brackets are omitted" do + a = Class.new(DelegationSpecs::Target) + suppress_warning do a.class_eval(<<-RUBY) def delegate(...) - target(...) + target ... end RUBY - - a.new.delegate(1, b: 2).should == [[1], {b: 2}] end - it "delegates block" do + a.new.delegate(1, b: 2).should == Range.new([[], {}], nil, true) + end +end + +describe "delegation with def(x, ...)" do + it "delegates rest and kwargs" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate(x, ...) + target(...) + end + RUBY + + a.new.delegate(0, 1, b: 2).should == [[1], {b: 2}] + end + + it "delegates block" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate_block(x, ...) + target_block(...) + end + RUBY + + a.new.delegate_block(0, 1, b: 2) { |x| x }.should == [{b: 2}, [1]] + end +end + +ruby_version_is "3.2" do + describe "delegation with def(*)" do + it "delegates rest" do a = Class.new(DelegationSpecs::Target) a.class_eval(<<-RUBY) - def delegate_block(...) - target_block(...) - end - RUBY + def delegate(*) + target(*) + end + RUBY - a.new.delegate_block(1, b: 2) { |x| x }.should == [{b: 2}, [1]] + a.new.delegate(0, 1).should == [[0, 1], {}] end + end +end - it "parses as open endless Range when brackets are omitted" do +ruby_version_is "3.2" do + describe "delegation with def(**)" do + it "delegates kwargs" do a = Class.new(DelegationSpecs::Target) - suppress_warning do - a.class_eval(<<-RUBY) - def delegate(...) - target ... - end - RUBY - end - - a.new.delegate(1, b: 2).should == Range.new([[], {}], nil, true) + a.class_eval(<<-RUBY) + def delegate(**) + target(**) + end + RUBY + + a.new.delegate(a: 1) { |x| x }.should == [[], {a: 1}] end end end diff --git a/spec/ruby/language/encoding_spec.rb b/spec/ruby/language/encoding_spec.rb index 5430c9cb98..e761a53cb6 100644 --- a/spec/ruby/language/encoding_spec.rb +++ b/spec/ruby/language/encoding_spec.rb @@ -13,15 +13,15 @@ describe "The __ENCODING__ pseudo-variable" do end it "is the evaluated strings's one inside an eval" do - eval("__ENCODING__".force_encoding("US-ASCII")).should == Encoding::US_ASCII - eval("__ENCODING__".force_encoding("BINARY")).should == Encoding::BINARY + eval("__ENCODING__".dup.force_encoding("US-ASCII")).should == Encoding::US_ASCII + eval("__ENCODING__".dup.force_encoding("BINARY")).should == Encoding::BINARY end it "is the encoding specified by a magic comment inside an eval" do - code = "# encoding: BINARY\n__ENCODING__".force_encoding("US-ASCII") + code = "# encoding: BINARY\n__ENCODING__".dup.force_encoding("US-ASCII") eval(code).should == Encoding::BINARY - code = "# encoding: us-ascii\n__ENCODING__".force_encoding("BINARY") + code = "# encoding: us-ascii\n__ENCODING__".dup.force_encoding("BINARY") eval(code).should == Encoding::US_ASCII end diff --git a/spec/ruby/language/ensure_spec.rb b/spec/ruby/language/ensure_spec.rb index e893904bcb..16e626b4d0 100644 --- a/spec/ruby/language/ensure_spec.rb +++ b/spec/ruby/language/ensure_spec.rb @@ -328,4 +328,21 @@ describe "An ensure block inside 'do end' block" do result.should == :begin end + + ruby_version_is "3.4" do + it "does not introduce extra backtrace entries" do + def foo + begin + raise "oops" + ensure + return caller(0, 2) # rubocop:disable Lint/EnsureReturn + end + end + line = __LINE__ + foo.should == [ + "#{__FILE__}:#{line-3}:in 'foo'", + "#{__FILE__}:#{line+1}:in 'block (3 levels) in <top (required)>'" + ] + end + end end diff --git a/spec/ruby/language/execution_spec.rb b/spec/ruby/language/execution_spec.rb index 4e0310946d..ef1de38899 100644 --- a/spec/ruby/language/execution_spec.rb +++ b/spec/ruby/language/execution_spec.rb @@ -5,6 +5,45 @@ describe "``" do ip = 'world' `echo disc #{ip}`.should == "disc world\n" end + + it "can be redefined and receive a frozen string as argument" do + called = false + runner = Object.new + + runner.singleton_class.define_method(:`) do |str| + called = true + + str.should == "test command" + str.frozen?.should == true + end + + runner.instance_exec do + `test command` + end + + called.should == true + end + + it "the argument isn't frozen if it contains interpolation" do + called = false + runner = Object.new + + runner.singleton_class.define_method(:`) do |str| + called = true + + str.should == "test command" + str.frozen?.should == false + str << "mutated" + end + + 2.times do + runner.instance_exec do + `test #{:command}` + end + end + + called.should == true + end end describe "%x" do @@ -12,4 +51,43 @@ describe "%x" do ip = 'world' %x(echo disc #{ip}).should == "disc world\n" end + + it "can be redefined and receive a frozen string as argument" do + called = false + runner = Object.new + + runner.singleton_class.define_method(:`) do |str| + called = true + + str.should == "test command" + str.frozen?.should == true + end + + runner.instance_exec do + %x{test command} + end + + called.should == true + end + + it "the argument isn't frozen if it contains interpolation" do + called = false + runner = Object.new + + runner.singleton_class.define_method(:`) do |str| + called = true + + str.should == "test command" + str.frozen?.should == false + str << "mutated" + end + + 2.times do + runner.instance_exec do + %x{test #{:command}} + end + end + + called.should == true + end end diff --git a/spec/ruby/language/file_spec.rb b/spec/ruby/language/file_spec.rb index 729dee1008..59563d9642 100644 --- a/spec/ruby/language/file_spec.rb +++ b/spec/ruby/language/file_spec.rb @@ -7,23 +7,23 @@ describe "The __FILE__ pseudo-variable" do -> { eval("__FILE__ = 1") }.should raise_error(SyntaxError) end - it "equals (eval) inside an eval" do - eval("__FILE__").should == "(eval)" + ruby_version_is ""..."3.3" do + it "equals (eval) inside an eval" do + eval("__FILE__").should == "(eval)" + end end -end -describe "The __FILE__ pseudo-variable" do - it_behaves_like :language___FILE__, :require, CodeLoadingSpecs::Method.new + ruby_version_is "3.3" do + it "equals (eval at __FILE__:__LINE__) inside an eval" do + eval("__FILE__").should == "(eval at #{__FILE__}:#{__LINE__})" + end + end end -describe "The __FILE__ pseudo-variable" do +describe "The __FILE__ pseudo-variable with require" do it_behaves_like :language___FILE__, :require, Kernel end -describe "The __FILE__ pseudo-variable" do - it_behaves_like :language___FILE__, :load, CodeLoadingSpecs::Method.new -end - -describe "The __FILE__ pseudo-variable" do +describe "The __FILE__ pseudo-variable with load" do it_behaves_like :language___FILE__, :load, Kernel end diff --git a/spec/ruby/language/fixtures/defined.rb b/spec/ruby/language/fixtures/defined.rb index 8b6004c19f..a9552619bf 100644 --- a/spec/ruby/language/fixtures/defined.rb +++ b/spec/ruby/language/fixtures/defined.rb @@ -19,6 +19,9 @@ module DefinedSpecs DefinedSpecs end + def self.any_args(*) + end + class Basic A = 42 diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb b/spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb Binary files differindex d0558a2251..f72a32e879 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_two_literals.rb b/spec/ruby/language/fixtures/freeze_magic_comment_two_literals.rb index a4d655ad02..cccc5969bd 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_two_literals.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_two_literals.rb @@ -1,3 +1,3 @@ # frozen_string_literal: true -p "abc".object_id == "abc".object_id +p "abc".equal?("abc") diff --git a/spec/ruby/language/fixtures/rescue/top_level.rb b/spec/ruby/language/fixtures/rescue/top_level.rb new file mode 100644 index 0000000000..59e78ef1d6 --- /dev/null +++ b/spec/ruby/language/fixtures/rescue/top_level.rb @@ -0,0 +1,7 @@ +# capturing in local variable at top-level + +begin + raise "message" +rescue => e + ScratchPad << e.message +end diff --git a/spec/ruby/language/fixtures/super.rb b/spec/ruby/language/fixtures/super.rb index 218f1e4970..c5bdcf0e40 100644 --- a/spec/ruby/language/fixtures/super.rb +++ b/spec/ruby/language/fixtures/super.rb @@ -539,6 +539,30 @@ module SuperSpecs args end + def m3(*args) + args + end + + def m4(*args) + args + end + + def m_default(*args) + args + end + + def m_rest(*args) + args + end + + def m_pre_default_rest_post(*args) + args + end + + def m_kwrest(**kw) + kw + end + def m_modified(*args) args end @@ -549,6 +573,30 @@ module SuperSpecs super end + def m3(_, _, _) + super + end + + def m4(_, _, _, _) + super + end + + def m_default(_ = 0) + super + end + + def m_rest(*_) + super + end + + def m_pre_default_rest_post(_, _, _=:a, _=:b, *_, _, _) + super + end + + def m_kwrest(**_) + super + end + def m_modified(_, _) _ = 14 super @@ -556,6 +604,20 @@ module SuperSpecs end end + module ZSuperInBlock + class A + def m(arg:) + arg + end + end + + class B < A + def m(arg:) + proc { super }.call + end + end + end + module Keywords class Arguments def foo(**args) diff --git a/spec/ruby/language/fixtures/variables.rb b/spec/ruby/language/fixtures/variables.rb index 07265dbb2b..527caa7a78 100644 --- a/spec/ruby/language/fixtures/variables.rb +++ b/spec/ruby/language/fixtures/variables.rb @@ -82,4 +82,76 @@ module VariablesSpecs def self.false false end + + class EvalOrder + attr_reader :order + + def initialize + @order = [] + end + + def reset + @order = [] + end + + def foo + self << "foo" + FooClass.new(self) + end + + def bar + self << "bar" + BarClass.new(self) + end + + def a + self << "a" + end + + def b + self << "b" + end + + def node + self << "node" + + node = Node.new + node.left = Node.new + node.left.right = Node.new + + node + end + + def <<(value) + order << value + end + + class FooClass + attr_reader :evaluator + + def initialize(evaluator) + @evaluator = evaluator + end + + def []=(_index, _value) + evaluator << "foo[]=" + end + end + + class BarClass + attr_reader :evaluator + + def initialize(evaluator) + @evaluator = evaluator + end + + def baz=(_value) + evaluator << "bar.baz=" + end + end + + class Node + attr_accessor :left, :right + end + end end diff --git a/spec/ruby/language/hash_spec.rb b/spec/ruby/language/hash_spec.rb index 9b2e5a2dc7..a7631fb0d6 100644 --- a/spec/ruby/language/hash_spec.rb +++ b/spec/ruby/language/hash_spec.rb @@ -33,7 +33,7 @@ describe "Hash literal" do end it "freezes string keys on initialization" do - key = "foo" + key = +"foo" h = {key => "bar"} key.reverse! h["foo"].should == "bar" @@ -58,11 +58,16 @@ describe "Hash literal" do }.should complain(/key 1000 is duplicated|duplicated key/) @h.keys.size.should == 1 @h.should == {1000 => :foo} - -> { - @h = eval "{1.0 => :bar, 1.0 => :foo}" - }.should complain(/key 1.0 is duplicated|duplicated key/) - @h.keys.size.should == 1 - @h.should == {1.0 => :foo} + end + + ruby_version_is "3.1" do + it "checks duplicated float keys on initialization" do + -> { + @h = eval "{1.0 => :bar, 1.0 => :foo}" + }.should complain(/key 1.0 is duplicated|duplicated key/) + @h.keys.size.should == 1 + @h.should == {1.0 => :foo} + end end it "accepts a hanging comma" do @@ -122,11 +127,24 @@ describe "Hash literal" do {a: 1, **h, c: 4}.should == {a: 1, b: 2, c: 4} end - it "expands an '**{}' element with the last key/value pair taking precedence" do + it "expands an '**{}' or '**obj' element with the last key/value pair taking precedence" do -> { @h = eval "{a: 1, **{a: 2, b: 3, c: 1}, c: 3}" }.should complain(/key :a is duplicated|duplicated key/) @h.should == {a: 2, b: 3, c: 3} + + -> { + h = {a: 2, b: 3, c: 1} + @h = eval "{a: 1, **h, c: 3}" + }.should_not complain + @h.should == {a: 2, b: 3, c: 3} + end + + it "expands an '**{}' and warns when finding an additional duplicate key afterwards" do + -> { + @h = eval "{d: 1, **{a: 2, b: 3, c: 1}, c: 3}" + }.should complain(/key :c is duplicated|duplicated key/) + @h.should == {a: 2, b: 3, c: 3, d: 1} end it "merges multiple nested '**obj' in Hash literals" do @@ -143,18 +161,9 @@ describe "Hash literal" do {a: 1, **obj, c: 3}.should == {a:1, b: 2, c: 3, d: 4} end - ruby_version_is ""..."2.7" do - it "raises a TypeError if any splatted elements keys are not symbols" do - h = {1 => 2, b: 3} - -> { {a: 1, **h} }.should raise_error(TypeError) - end - end - - ruby_version_is "2.7" do - it "allows splatted elements keys that are not symbols" do - h = {1 => 2, b: 3} - {a: 1, **h}.should == {a: 1, 1 => 2, b: 3} - end + it "allows splatted elements keys that are not symbols" do + h = {1 => 2, b: 3} + {a: 1, **h}.should == {a: 1, 1 => 2, b: 3} end it "raises a TypeError if #to_hash does not return a Hash" do @@ -181,6 +190,24 @@ describe "Hash literal" do utf8_hash.keys.first.should == usascii_hash.keys.first usascii_hash.keys.first.encoding.should == Encoding::US_ASCII end + + ruby_bug "#20280", ""..."3.4" do + it "raises a SyntaxError at parse time when Symbol key with invalid bytes" do + ScratchPad.record [] + -> { + eval 'ScratchPad << 1; {:"\xC3" => 1}' + }.should raise_error(SyntaxError, /invalid symbol/) + ScratchPad.recorded.should == [] + end + + it "raises a SyntaxError at parse time when Symbol key with invalid bytes and 'key: value' syntax used" do + ScratchPad.record [] + -> { + eval 'ScratchPad << 1; {"\xC3": 1}' + }.should raise_error(SyntaxError, /invalid symbol/) + ScratchPad.recorded.should == [] + end + end end describe "The ** operator" do @@ -195,8 +222,8 @@ describe "The ** operator" do h.should == { one: 1, two: 2 } end - ruby_version_is ""..."3.0" do - it "makes a caller-side copy when calling a method taking a positional Hash" do + ruby_bug "#20012", ""..."3.3" do + it "makes a copy when calling a method taking a positional Hash" do def m(h) h.delete(:one); h end @@ -208,16 +235,42 @@ describe "The ** operator" do end end - ruby_version_is "3.0" do - it "does not copy when calling a method taking a positional Hash" do - def m(h) - h.delete(:one); h + ruby_version_is "3.1" do + describe "hash with omitted value" do + it "accepts short notation 'key' for 'key: value' syntax" do + a, b, c = 1, 2, 3 + h = eval('{a:}') + {a: 1}.should == h + h = eval('{a:, b:, c:}') + {a: 1, b: 2, c: 3}.should == h end - h = { one: 1, two: 2 } - m(**h).should == { two: 2 } - m(**h).should.equal?(h) - h.should == { two: 2 } + it "ignores hanging comma on short notation" do + a, b, c = 1, 2, 3 + h = eval('{a:, b:, c:,}') + {a: 1, b: 2, c: 3}.should == h + end + + it "accepts mixed syntax" do + a, e = 1, 5 + h = eval('{a:, b: 2, "c" => 3, :d => 4, e:}') + eval('{a: 1, :b => 2, "c" => 3, "d": 4, e: 5}').should == h + end + + it "works with methods and local vars" do + a = Class.new + a.class_eval(<<-RUBY) + def bar + "baz" + end + + def foo(val) + {bar:, val:} + end + RUBY + + a.new.foo(1).should == {bar: "baz", val: 1} + end end end end diff --git a/spec/ruby/language/heredoc_spec.rb b/spec/ruby/language/heredoc_spec.rb index 61a27ad8e0..b3b160fd11 100644 --- a/spec/ruby/language/heredoc_spec.rb +++ b/spec/ruby/language/heredoc_spec.rb @@ -59,20 +59,10 @@ HERE s.encoding.should == Encoding::US_ASCII end - ruby_version_is "2.7" do - it 'raises SyntaxError if quoted HEREDOC identifier is ending not on same line' do - -> { - eval %{<<"HERE\n"\nraises syntax error\nHERE} - }.should raise_error(SyntaxError) - end - end - - ruby_version_is ""..."2.7" do - it 'prints a warning if quoted HEREDOC identifier is ending not on same line' do - -> { - eval %{<<"HERE\n"\nit warns\nHERE} - }.should complain(/here document identifier ends with a newline/) - end + it 'raises SyntaxError if quoted HEREDOC identifier is ending not on same line' do + -> { + eval %{<<"HERE\n"\nraises syntax error\nHERE} + }.should raise_error(SyntaxError) end it "allows HEREDOC with <<~'identifier', allowing to indent identifier and content" do diff --git a/spec/ruby/language/if_spec.rb b/spec/ruby/language/if_spec.rb index d1d95c1607..2d1a89f081 100644 --- a/spec/ruby/language/if_spec.rb +++ b/spec/ruby/language/if_spec.rb @@ -305,6 +305,59 @@ describe "The if expression" do 6.times(&b) ScratchPad.recorded.should == [4, 5, 4, 5] end + + it "warns when Integer literals are used instead of predicates" do + -> { + eval <<~RUBY + $. = 0 + 10.times { |i| ScratchPad << i if 4..5 } + RUBY + }.should complain(/warning: integer literal in flip-flop/, verbose: true) + ScratchPad.recorded.should == [] + end + end + + describe "when a branch syntactically does not return a value" do + it "raises SyntaxError if both do not return a value" do + -> { + eval <<~RUBY + def m + a = if rand + return + else + return + end + a + end + RUBY + }.should raise_error(SyntaxError, /void value expression/) + end + + it "does not raise SyntaxError if one branch returns a value" do + eval(<<~RUBY).should == 1 + def m + a = if false # using false to make it clear that's not checked for + 42 + else + return 1 + end + a + end + m + RUBY + + eval(<<~RUBY).should == 1 + def m + a = if true # using true to make it clear that's not checked for + return 1 + else + 42 + end + a + end + m + RUBY + end end end diff --git a/spec/ruby/language/keyword_arguments_spec.rb b/spec/ruby/language/keyword_arguments_spec.rb new file mode 100644 index 0000000000..ffb5b1fab0 --- /dev/null +++ b/spec/ruby/language/keyword_arguments_spec.rb @@ -0,0 +1,398 @@ +require_relative '../spec_helper' + +describe "Keyword arguments" do + def target(*args, **kwargs) + [args, kwargs] + end + + it "are separated from positional arguments" do + def m(*args, **kwargs) + [args, kwargs] + end + + empty = {} + m(**empty).should == [[], {}] + m(empty).should == [[{}], {}] + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + end + + it "when the receiving method has not keyword parameters it treats kwargs as positional" do + def m(*a) + a + end + + m(a: 1).should == [{a: 1}] + m({a: 1}).should == [{a: 1}] + end + + context "empty kwargs are treated as if they were not passed" do + it "when calling a method" do + def m(*a) + a + end + + empty = {} + m(**empty).should == [] + m(empty).should == [{}] + end + + it "when yielding to a block" do + def y(*args, **kwargs) + yield(*args, **kwargs) + end + + empty = {} + y(**empty) { |*a| a }.should == [] + y(empty) { |*a| a }.should == [{}] + end + end + + it "extra keywords are not allowed without **kwrest" do + def m(*a, kw:) + a + end + + m(kw: 1).should == [] + -> { m(kw: 1, kw2: 2) }.should raise_error(ArgumentError, 'unknown keyword: :kw2') + -> { m(kw: 1, true => false) }.should raise_error(ArgumentError, 'unknown keyword: true') + -> { m(kw: 1, a: 1, b: 2, c: 3) }.should raise_error(ArgumentError, 'unknown keywords: :a, :b, :c') + end + + it "raises ArgumentError exception when required keyword argument is not passed" do + def m(a:, b:, c:) + [a, b, c] + end + + -> { m(a: 1, b: 2) }.should raise_error(ArgumentError, /missing keyword: :c/) + -> { m() }.should raise_error(ArgumentError, /missing keywords: :a, :b, :c/) + end + + it "raises ArgumentError for missing keyword arguments even if there are extra ones" do + def m(a:) + a + end + + -> { m(b: 1) }.should raise_error(ArgumentError, /missing keyword: :a/) + end + + it "handle * and ** at the same call site" do + def m(*a) + a + end + + m(*[], **{}).should == [] + m(*[], 42, **{}).should == [42] + end + + context "**" do + ruby_version_is "3.3" do + it "copies a non-empty Hash for a method taking (*args)" do + def m(*args) + args[0] + end + + h = {a: 1} + m(**h).should_not.equal?(h) + h.should == {a: 1} + end + end + + it "copies the given Hash for a method taking (**kwargs)" do + def m(**kw) + kw + end + + empty = {} + m(**empty).should == empty + m(**empty).should_not.equal?(empty) + + h = {a: 1} + m(**h).should == h + m(**h).should_not.equal?(h) + end + end + + context "delegation" do + it "works with (*args, **kwargs)" do + def m(*args, **kwargs) + target(*args, **kwargs) + end + + empty = {} + m(**empty).should == [[], {}] + m(empty).should == [[{}], {}] + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + end + + it "works with proc { |*args, **kwargs| }" do + m = proc do |*args, **kwargs| + target(*args, **kwargs) + end + + empty = {} + m.(**empty).should == [[], {}] + m.(empty).should == [[{}], {}] + + m.(a: 1).should == [[], {a: 1}] + m.({a: 1}).should == [[{a: 1}], {}] + + # no autosplatting for |*args, **kwargs| + m.([1, 2]).should == [[[1, 2]], {}] + end + + it "works with -> (*args, **kwargs) {}" do + m = -> *args, **kwargs do + target(*args, **kwargs) + end + + empty = {} + m.(**empty).should == [[], {}] + m.(empty).should == [[{}], {}] + + m.(a: 1).should == [[], {a: 1}] + m.({a: 1}).should == [[{a: 1}], {}] + end + + it "works with (...)" do + instance_eval <<~DEF + def m(...) + target(...) + end + DEF + + empty = {} + m(**empty).should == [[], {}] + m(empty).should == [[{}], {}] + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + end + + it "works with call(*ruby2_keyword_args)" do + class << self + ruby2_keywords def m(*args) + target(*args) + end + end + + empty = {} + m(**empty).should == [[], {}] + Hash.ruby2_keywords_hash?(empty).should == false + m(empty).should == [[{}], {}] + Hash.ruby2_keywords_hash?(empty).should == false + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + + kw = {a: 1} + + m(**kw).should == [[], {a: 1}] + m(**kw)[1].should == kw + m(**kw)[1].should_not.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + Hash.ruby2_keywords_hash?(m(**kw)[1]).should == false + + m(kw).should == [[{a: 1}], {}] + m(kw)[0][0].should.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + end + + it "works with super(*ruby2_keyword_args)" do + parent = Class.new do + def m(*args, **kwargs) + [args, kwargs] + end + end + + child = Class.new(parent) do + ruby2_keywords def m(*args) + super(*args) + end + end + + obj = child.new + + empty = {} + obj.m(**empty).should == [[], {}] + Hash.ruby2_keywords_hash?(empty).should == false + obj.m(empty).should == [[{}], {}] + Hash.ruby2_keywords_hash?(empty).should == false + + obj.m(a: 1).should == [[], {a: 1}] + obj.m({a: 1}).should == [[{a: 1}], {}] + + kw = {a: 1} + + obj.m(**kw).should == [[], {a: 1}] + obj.m(**kw)[1].should == kw + obj.m(**kw)[1].should_not.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + Hash.ruby2_keywords_hash?(obj.m(**kw)[1]).should == false + + obj.m(kw).should == [[{a: 1}], {}] + obj.m(kw)[0][0].should.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + end + + it "works with zsuper" do + parent = Class.new do + def m(*args, **kwargs) + [args, kwargs] + end + end + + child = Class.new(parent) do + ruby2_keywords def m(*args) + super + end + end + + obj = child.new + + empty = {} + obj.m(**empty).should == [[], {}] + Hash.ruby2_keywords_hash?(empty).should == false + obj.m(empty).should == [[{}], {}] + Hash.ruby2_keywords_hash?(empty).should == false + + obj.m(a: 1).should == [[], {a: 1}] + obj.m({a: 1}).should == [[{a: 1}], {}] + + kw = {a: 1} + + obj.m(**kw).should == [[], {a: 1}] + obj.m(**kw)[1].should == kw + obj.m(**kw)[1].should_not.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + Hash.ruby2_keywords_hash?(obj.m(**kw)[1]).should == false + + obj.m(kw).should == [[{a: 1}], {}] + obj.m(kw)[0][0].should.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + end + + it "works with yield(*ruby2_keyword_args)" do + class << self + def y(args) + yield(*args) + end + + ruby2_keywords def m(*outer_args) + y(outer_args, &-> *args, **kwargs { target(*args, **kwargs) }) + end + end + + empty = {} + m(**empty).should == [[], {}] + Hash.ruby2_keywords_hash?(empty).should == false + m(empty).should == [[{}], {}] + Hash.ruby2_keywords_hash?(empty).should == false + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + + kw = {a: 1} + + m(**kw).should == [[], {a: 1}] + m(**kw)[1].should == kw + m(**kw)[1].should_not.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + Hash.ruby2_keywords_hash?(m(**kw)[1]).should == false + + m(kw).should == [[{a: 1}], {}] + m(kw)[0][0].should.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + end + + it "does not work with (*args)" do + class << self + def m(*args) + target(*args) + end + end + + empty = {} + m(**empty).should == [[], {}] + m(empty).should == [[{}], {}] + + m(a: 1).should == [[{a: 1}], {}] + m({a: 1}).should == [[{a: 1}], {}] + end + + ruby_version_is "3.1" do + describe "omitted values" do + it "accepts short notation 'key' for 'key: value' syntax" do + def m(a:, b:) + [a, b] + end + + a = 1 + b = 2 + + eval('m(a:, b:).should == [1, 2]') + end + end + end + + ruby_version_is "3.2" do + it "does not work with call(*ruby2_keyword_args) with missing ruby2_keywords in between" do + class << self + def n(*args) # Note the missing ruby2_keywords here + target(*args) + end + + ruby2_keywords def m(*args) + n(*args) + end + end + + empty = {} + m(**empty).should == [[], {}] + m(empty).should == [[{}], {}] + + m(a: 1).should == [[{a: 1}], {}] + m({a: 1}).should == [[{a: 1}], {}] + end + end + + ruby_version_is ""..."3.2" do + # https://bugs.ruby-lang.org/issues/18625 + it "works with call(*ruby2_keyword_args) with missing ruby2_keywords in between due to CRuby bug #18625" do + class << self + def n(*args) # Note the missing ruby2_keywords here + target(*args) + end + + ruby2_keywords def m(*args) + n(*args) + end + end + + empty = {} + m(**empty).should == [[], {}] + Hash.ruby2_keywords_hash?(empty).should == false + m(empty).should == [[{}], {}] + Hash.ruby2_keywords_hash?(empty).should == false + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + + kw = {a: 1} + + m(**kw).should == [[], {a: 1}] + m(**kw)[1].should == kw + m(**kw)[1].should_not.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + Hash.ruby2_keywords_hash?(m(**kw)[1]).should == false + + m(kw).should == [[{a: 1}], {}] + m(kw)[0][0].should.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + end + end + end +end diff --git a/spec/ruby/language/lambda_spec.rb b/spec/ruby/language/lambda_spec.rb index 630817c909..3ab3569ebe 100644 --- a/spec/ruby/language/lambda_spec.rb +++ b/spec/ruby/language/lambda_spec.rb @@ -22,14 +22,12 @@ describe "A lambda literal -> () { }" do -> { }.lambda?.should be_true end - ruby_version_is "2.6" do - it "may include a rescue clause" do - eval('-> do raise ArgumentError; rescue ArgumentError; 7; end').should be_an_instance_of(Proc) - end + it "may include a rescue clause" do + eval('-> do raise ArgumentError; rescue ArgumentError; 7; end').should be_an_instance_of(Proc) + end - it "may include a ensure clause" do - eval('-> do 1; ensure; 2; end').should be_an_instance_of(Proc) - end + it "may include a ensure clause" do + eval('-> do 1; ensure; 2; end').should be_an_instance_of(Proc) end it "has its own scope for local variables" do @@ -179,34 +177,16 @@ describe "A lambda literal -> () { }" do result.should == [1, 2, 3, [4, 5], 6, [7, 8], 9, 10, 11, 12] end - ruby_version_is ''...'3.0' do - evaluate <<-ruby do - @a = -> (*, **k) { k } - ruby - - @a.().should == {} - @a.(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} - - suppress_keyword_warning do - h = mock("keyword splat") - h.should_receive(:to_hash).and_return({a: 1}) - @a.(h).should == {a: 1} - end - end - end - - ruby_version_is '3.0' do - evaluate <<-ruby do - @a = -> (*, **k) { k } - ruby + evaluate <<-ruby do + @a = -> (*, **k) { k } + ruby - @a.().should == {} - @a.(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} + @a.().should == {} + @a.(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} - h = mock("keyword splat") - h.should_not_receive(:to_hash) - @a.(h).should == {} - end + h = mock("keyword splat") + h.should_not_receive(:to_hash) + @a.(h).should == {} end evaluate <<-ruby do @@ -283,38 +263,18 @@ describe "A lambda literal -> () { }" do end describe "with circular optional argument reference" do - ruby_version_is ''...'2.7' do - it "warns and uses a nil value when there is an existing local variable with same name" do - a = 1 - -> { - @proc = eval "-> (a=a) { a }" - }.should complain(/circular argument reference/) - @proc.call.should == nil - end - - it "warns and uses a nil value when there is an existing method with same name" do - def a; 1; end - -> { - @proc = eval "-> (a=a) { a }" - }.should complain(/circular argument reference/) - @proc.call.should == nil - end + it "raises a SyntaxError if using an existing local with the same name as the argument" do + a = 1 + -> { + @proc = eval "-> (a=a) { a }" + }.should raise_error(SyntaxError) end - ruby_version_is '2.7' do - it "raises a SyntaxError if using an existing local with the same name as the argument" do - a = 1 - -> { - @proc = eval "-> (a=a) { a }" - }.should raise_error(SyntaxError) - end - - it "raises a SyntaxError if there is an existing method with the same name as the argument" do - def a; 1; end - -> { - @proc = eval "-> (a=a) { a }" - }.should raise_error(SyntaxError) - end + it "raises a SyntaxError if there is an existing method with the same name as the argument" do + def a; 1; end + -> { + @proc = eval "-> (a=a) { a }" + }.should raise_error(SyntaxError) end it "calls an existing method with the same name as the argument if explicitly using ()" do @@ -362,26 +322,12 @@ describe "A lambda expression 'lambda { ... }'" do def meth; lambda; end end - ruby_version_is ""..."2.7" do - it "can be created" do - implicit_lambda = nil + it "raises ArgumentError" do + implicit_lambda = nil + suppress_warning do -> { - implicit_lambda = meth { 1 } - }.should complain(/tried to create Proc object without a block/) - - implicit_lambda.lambda?.should be_true - implicit_lambda.call.should == 1 - end - end - - ruby_version_is "2.7" do - it "raises ArgumentError" do - implicit_lambda = nil - suppress_warning do - -> { - meth { 1 } - }.should raise_error(ArgumentError, /tried to create Proc object without a block/) - end + meth { 1 } + }.should raise_error(ArgumentError, /tried to create Proc object without a block/) end end end @@ -550,34 +496,16 @@ describe "A lambda expression 'lambda { ... }'" do result.should == [1, 2, 3, [4, 5], 6, [7, 8], 9, 10, 11, 12] end - ruby_version_is ''...'3.0' do - evaluate <<-ruby do - @a = lambda { |*, **k| k } - ruby - - @a.().should == {} - @a.(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} - - suppress_keyword_warning do - h = mock("keyword splat") - h.should_receive(:to_hash).and_return({a: 1}) - @a.(h).should == {a: 1} - end - end - end - - ruby_version_is '3.0' do - evaluate <<-ruby do - @a = lambda { |*, **k| k } - ruby + evaluate <<-ruby do + @a = lambda { |*, **k| k } + ruby - @a.().should == {} - @a.(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} + @a.().should == {} + @a.(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} - h = mock("keyword splat") - h.should_not_receive(:to_hash) - @a.(h).should == {} - end + h = mock("keyword splat") + h.should_not_receive(:to_hash) + @a.(h).should == {} end evaluate <<-ruby do diff --git a/spec/ruby/language/method_spec.rb b/spec/ruby/language/method_spec.rb index 449d5e6f1b..9abe4cde20 100644 --- a/spec/ruby/language/method_spec.rb +++ b/spec/ruby/language/method_spec.rb @@ -572,6 +572,13 @@ describe "A method" do end evaluate <<-ruby do + def m(a:, **kw) [a, kw] end + ruby + + -> { m(b: 1) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do def m(a: 1) a end ruby @@ -602,13 +609,11 @@ describe "A method" do -> { m(2) }.should raise_error(ArgumentError) end - ruby_version_is "2.7" do - evaluate <<-ruby do - def m(**k); k end; - ruby + evaluate <<-ruby do + def m(**k); k end; + ruby - m("a" => 1).should == { "a" => 1 } - end + m("a" => 1).should == { "a" => 1 } end evaluate <<-ruby do @@ -744,68 +749,31 @@ describe "A method" do end end - ruby_version_is ""..."3.0" do - evaluate <<-ruby do - def m(a, b: 1) [a, b] end - ruby - - m(2).should == [2, 1] - m(1, b: 2).should == [1, 2] - suppress_keyword_warning do - m("a" => 1, b: 2).should == [{"a" => 1, b: 2}, 1] - end - end - - evaluate <<-ruby do - def m(a, **) a end - ruby - - m(1).should == 1 - m(1, a: 2, b: 3).should == 1 - suppress_keyword_warning do - m("a" => 1, b: 2).should == {"a" => 1, b: 2} - end - end - - evaluate <<-ruby do - def m(a, **k) [a, k] end - ruby + evaluate <<-ruby do + def m(a, b: 1) [a, b] end + ruby - m(1).should == [1, {}] - m(1, a: 2, b: 3).should == [1, {a: 2, b: 3}] - suppress_keyword_warning do - m("a" => 1, b: 2).should == [{"a" => 1, b: 2}, {}] - end - end + m(2).should == [2, 1] + m(1, b: 2).should == [1, 2] + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) end - ruby_version_is "3.0" do - evaluate <<-ruby do - def m(a, b: 1) [a, b] end - ruby - - m(2).should == [2, 1] - m(1, b: 2).should == [1, 2] - -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) - end - - evaluate <<-ruby do - def m(a, **) a end - ruby + evaluate <<-ruby do + def m(a, **) a end + ruby - m(1).should == 1 - m(1, a: 2, b: 3).should == 1 - -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) - end + m(1).should == 1 + m(1, a: 2, b: 3).should == 1 + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) + end - evaluate <<-ruby do - def m(a, **k) [a, k] end - ruby + evaluate <<-ruby do + def m(a, **k) [a, k] end + ruby - m(1).should == [1, {}] - m(1, a: 2, b: 3).should == [1, {a: 2, b: 3}] - -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) - end + m(1).should == [1, {}] + m(1, a: 2, b: 3).should == [1, {a: 2, b: 3}] + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) end evaluate <<-ruby do @@ -916,72 +884,32 @@ describe "A method" do result.should == [[1, 2, 3], 4, [5, 6], 7, [], 8] end - ruby_version_is ""..."3.0" do - evaluate <<-ruby do - def m(a=1, b:) [a, b] end - ruby - - m(b: 2).should == [1, 2] - m(2, b: 1).should == [2, 1] - suppress_keyword_warning do - m("a" => 1, b: 2).should == [{"a" => 1}, 2] - end - end - - evaluate <<-ruby do - def m(a=1, b: 2) [a, b] end - ruby - - m().should == [1, 2] - m(2).should == [2, 2] - m(b: 3).should == [1, 3] - suppress_keyword_warning do - m("a" => 1, b: 2).should == [{"a" => 1}, 2] - end - end - end - - ruby_version_is "3.0" do - evaluate <<-ruby do - def m(a=1, b:) [a, b] end - ruby - - m(b: 2).should == [1, 2] - m(2, b: 1).should == [2, 1] - -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) - end - - evaluate <<-ruby do - def m(a=1, b: 2) [a, b] end - ruby + evaluate <<-ruby do + def m(a=1, b:) [a, b] end + ruby - m().should == [1, 2] - m(2).should == [2, 2] - m(b: 3).should == [1, 3] - -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) - end + m(b: 2).should == [1, 2] + m(2, b: 1).should == [2, 1] + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) end - ruby_version_is ""..."2.7" do - evaluate <<-ruby do - def m(a=1, **) a end - ruby + evaluate <<-ruby do + def m(a=1, b: 2) [a, b] end + ruby - m().should == 1 - m(2, a: 1, b: 0).should == 2 - m("a" => 1, a: 2).should == {"a" => 1} - end + m().should == [1, 2] + m(2).should == [2, 2] + m(b: 3).should == [1, 3] + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) end - ruby_version_is "2.7" do - evaluate <<-ruby do - def m(a=1, **) a end - ruby + evaluate <<-ruby do + def m(a=1, **) a end + ruby - m().should == 1 - m(2, a: 1, b: 0).should == 2 - m("a" => 1, a: 2).should == 1 - end + m().should == 1 + m(2, a: 1, b: 0).should == 2 + m("a" => 1, a: 2).should == 1 end evaluate <<-ruby do @@ -1021,449 +949,6 @@ describe "A method" do m(1, 2, 3).should == [[1, 2], 3] end - ruby_version_is ""..."2.7" do - evaluate <<-ruby do - def m(*, a:) a end - ruby - - m(a: 1).should == 1 - m(1, 2, a: 3).should == 3 - suppress_keyword_warning do - m("a" => 1, a: 2).should == 2 - end - end - - evaluate <<-ruby do - def m(*a, b:) [a, b] end - ruby - - m(b: 1).should == [[], 1] - m(1, 2, b: 3).should == [[1, 2], 3] - suppress_keyword_warning do - m("a" => 1, b: 2).should == [[{"a" => 1}], 2] - end - end - - evaluate <<-ruby do - def m(*, a: 1) a end - ruby - - m().should == 1 - m(1, 2).should == 1 - m(a: 2).should == 2 - m(1, a: 2).should == 2 - suppress_keyword_warning do - m("a" => 1, a: 2).should == 2 - end - end - - evaluate <<-ruby do - def m(*a, b: 1) [a, b] end - ruby - - m().should == [[], 1] - m(1, 2, 3, b: 4).should == [[1, 2, 3], 4] - suppress_keyword_warning do - m("a" => 1, b: 2).should == [[{"a" => 1}], 2] - end - - a = mock("splat") - a.should_not_receive(:to_ary) - m(*a).should == [[a], 1] - end - - evaluate <<-ruby do - def m(*, **) end - ruby - - m().should be_nil - m(a: 1, b: 2).should be_nil - m(1, 2, 3, a: 4, b: 5).should be_nil - - h = mock("keyword splat") - h.should_receive(:to_hash).and_return({a: 1}) - suppress_keyword_warning do - m(h).should be_nil - end - - h = mock("keyword splat") - error = RuntimeError.new("error while converting to a hash") - h.should_receive(:to_hash).and_raise(error) - -> { m(h) }.should raise_error(error) - end - - evaluate <<-ruby do - def m(*a, **) a end - ruby - - m().should == [] - m(1, 2, 3, a: 4, b: 5).should == [1, 2, 3] - m("a" => 1, a: 1).should == [{"a" => 1}] - m(1, **{a: 2}).should == [1] - - h = mock("keyword splat") - h.should_receive(:to_hash) - -> { m(**h) }.should raise_error(TypeError) - end - - evaluate <<-ruby do - def m(*, **k) k end - ruby - - m().should == {} - m(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} - m("a" => 1, a: 1).should == {a: 1} - - h = mock("keyword splat") - h.should_receive(:to_hash).and_return({a: 1}) - m(h).should == {a: 1} - end - - evaluate <<-ruby do - def m(a = nil, **k) [a, k] end - ruby - - m().should == [nil, {}] - m("a" => 1).should == [{"a" => 1}, {}] - m(a: 1).should == [nil, {a: 1}] - m("a" => 1, a: 1).should == [{"a" => 1}, {a: 1}] - m({ "a" => 1 }, a: 1).should == [{"a" => 1}, {a: 1}] - m({a: 1}, {}).should == [{a: 1}, {}] - - h = {"a" => 1, b: 2} - m(h).should == [{"a" => 1}, {b: 2}] - h.should == {"a" => 1, b: 2} - - h = {"a" => 1} - m(h).first.should == h - - h = {} - r = m(h) - r.first.should be_nil - r.last.should == {} - - hh = {} - h = mock("keyword splat empty hash") - h.should_receive(:to_hash).and_return(hh) - r = m(h) - r.first.should be_nil - r.last.should == {} - - h = mock("keyword splat") - h.should_receive(:to_hash).and_return({"a" => 1, a: 2}) - m(h).should == [{"a" => 1}, {a: 2}] - end - - evaluate <<-ruby do - def m(*a, **k) [a, k] end - ruby - - m().should == [[], {}] - m(1).should == [[1], {}] - m(a: 1, b: 2).should == [[], {a: 1, b: 2}] - m(1, 2, 3, a: 2).should == [[1, 2, 3], {a: 2}] - - m("a" => 1).should == [[{"a" => 1}], {}] - m(a: 1).should == [[], {a: 1}] - m("a" => 1, a: 1).should == [[{"a" => 1}], {a: 1}] - m({ "a" => 1 }, a: 1).should == [[{"a" => 1}], {a: 1}] - m({a: 1}, {}).should == [[{a: 1}], {}] - m({a: 1}, {"a" => 1}).should == [[{a: 1}, {"a" => 1}], {}] - - bo = BasicObject.new - def bo.to_a; [1, 2, 3]; end - def bo.to_hash; {:b => 2, :c => 3}; end - - m(*bo, **bo).should == [[1, 2, 3], {:b => 2, :c => 3}] - end - end - - ruby_version_is "2.7"...'3.0' do - evaluate <<-ruby do - def m(*, a:) a end - ruby - - m(a: 1).should == 1 - m(1, 2, a: 3).should == 3 - suppress_keyword_warning do - m("a" => 1, a: 2).should == 2 - end - end - - evaluate <<-ruby do - def m(*a, b:) [a, b] end - ruby - - m(b: 1).should == [[], 1] - m(1, 2, b: 3).should == [[1, 2], 3] - suppress_keyword_warning do - m("a" => 1, b: 2).should == [[{"a" => 1}], 2] - end - end - - evaluate <<-ruby do - def m(*, a: 1) a end - ruby - - m().should == 1 - m(1, 2).should == 1 - m(a: 2).should == 2 - m(1, a: 2).should == 2 - suppress_keyword_warning do - m("a" => 1, a: 2).should == 2 - end - end - - evaluate <<-ruby do - def m(*a, b: 1) [a, b] end - ruby - - m().should == [[], 1] - m(1, 2, 3, b: 4).should == [[1, 2, 3], 4] - suppress_keyword_warning do - m("a" => 1, b: 2).should == [[{"a" => 1}], 2] - end - - a = mock("splat") - a.should_not_receive(:to_ary) - m(*a).should == [[a], 1] - end - - evaluate <<-ruby do - def m(*a, **) a end - ruby - - m().should == [] - m(1, 2, 3, a: 4, b: 5).should == [1, 2, 3] - m("a" => 1, a: 1).should == [] - m(1, **{a: 2}).should == [1] - - h = mock("keyword splat") - h.should_receive(:to_hash) - -> { m(**h) }.should raise_error(TypeError) - end - - evaluate <<-ruby do - def m(*, **k) k end - ruby - - m().should == {} - m(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} - m("a" => 1, a: 1).should == {"a" => 1, a: 1} - - h = mock("keyword splat") - h.should_receive(:to_hash).and_return({a: 1}) - suppress_warning do - m(h).should == {a: 1} - end - end - - evaluate <<-ruby do - def m(a = nil, **k) [a, k] end - ruby - - m().should == [nil, {}] - m("a" => 1).should == [nil, {"a" => 1}] - m(a: 1).should == [nil, {a: 1}] - m("a" => 1, a: 1).should == [nil, {"a" => 1, a: 1}] - m({ "a" => 1 }, a: 1).should == [{"a" => 1}, {a: 1}] - suppress_warning do - m({a: 1}, {}).should == [{a: 1}, {}] - - h = {"a" => 1, b: 2} - m(h).should == [{"a" => 1}, {b: 2}] - h.should == {"a" => 1, b: 2} - - h = {"a" => 1} - m(h).first.should == h - - h = {} - r = m(h) - r.first.should be_nil - r.last.should == {} - - hh = {} - h = mock("keyword splat empty hash") - h.should_receive(:to_hash).and_return(hh) - r = m(h) - r.first.should be_nil - r.last.should == {} - - h = mock("keyword splat") - h.should_receive(:to_hash).and_return({"a" => 1, a: 2}) - m(h).should == [{"a" => 1}, {a: 2}] - end - end - - evaluate <<-ruby do - def m(*a, **k) [a, k] end - ruby - - m().should == [[], {}] - m(1).should == [[1], {}] - m(a: 1, b: 2).should == [[], {a: 1, b: 2}] - m(1, 2, 3, a: 2).should == [[1, 2, 3], {a: 2}] - - m("a" => 1).should == [[], {"a" => 1}] - m(a: 1).should == [[], {a: 1}] - m("a" => 1, a: 1).should == [[], {"a" => 1, a: 1}] - m({ "a" => 1 }, a: 1).should == [[{"a" => 1}], {a: 1}] - suppress_warning do - m({a: 1}, {}).should == [[{a: 1}], {}] - end - m({a: 1}, {"a" => 1}).should == [[{a: 1}, {"a" => 1}], {}] - - bo = BasicObject.new - def bo.to_a; [1, 2, 3]; end - def bo.to_hash; {:b => 2, :c => 3}; end - - m(*bo, **bo).should == [[1, 2, 3], {:b => 2, :c => 3}] - end - - evaluate <<-ruby do - def m(*, a:) a end - ruby - - m(a: 1).should == 1 - m(1, 2, a: 3).should == 3 - suppress_keyword_warning do - m("a" => 1, a: 2).should == 2 - end - end - - evaluate <<-ruby do - def m(*a, b:) [a, b] end - ruby - - m(b: 1).should == [[], 1] - m(1, 2, b: 3).should == [[1, 2], 3] - suppress_keyword_warning do - m("a" => 1, b: 2).should == [[{"a" => 1}], 2] - end - end - - evaluate <<-ruby do - def m(*, a: 1) a end - ruby - - m().should == 1 - m(1, 2).should == 1 - m(a: 2).should == 2 - m(1, a: 2).should == 2 - suppress_keyword_warning do - m("a" => 1, a: 2).should == 2 - end - end - - evaluate <<-ruby do - def m(*a, b: 1) [a, b] end - ruby - - m().should == [[], 1] - m(1, 2, 3, b: 4).should == [[1, 2, 3], 4] - suppress_keyword_warning do - m("a" => 1, b: 2).should == [[{"a" => 1}], 2] - end - - a = mock("splat") - a.should_not_receive(:to_ary) - m(*a).should == [[a], 1] - end - - evaluate <<-ruby do - def m(*a, **) a end - ruby - - m().should == [] - m(1, 2, 3, a: 4, b: 5).should == [1, 2, 3] - m("a" => 1, a: 1).should == [] - m(1, **{a: 2}).should == [1] - - h = mock("keyword splat") - h.should_receive(:to_hash) - -> { m(**h) }.should raise_error(TypeError) - end - - evaluate <<-ruby do - def m(*, **k) k end - ruby - - m().should == {} - m(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} - m("a" => 1, a: 1).should == {"a" => 1, a: 1} - - h = mock("keyword splat") - h.should_receive(:to_hash).and_return({a: 1}) - suppress_keyword_warning do - m(h).should == {a: 1} - end - end - - evaluate <<-ruby do - def m(a = nil, **k) [a, k] end - ruby - - m().should == [nil, {}] - m("a" => 1).should == [nil, {"a" => 1}] - m(a: 1).should == [nil, {a: 1}] - m("a" => 1, a: 1).should == [nil, {"a" => 1, a: 1}] - m({ "a" => 1 }, a: 1).should == [{"a" => 1}, {a: 1}] - suppress_keyword_warning do - m({a: 1}, {}).should == [{a: 1}, {}] - end - - h = {"a" => 1, b: 2} - suppress_keyword_warning do - m(h).should == [{"a" => 1}, {b: 2}] - end - h.should == {"a" => 1, b: 2} - - h = {"a" => 1} - m(h).first.should == h - - h = {} - suppress_keyword_warning do - m(h).should == [nil, {}] - end - - hh = {} - h = mock("keyword splat empty hash") - h.should_receive(:to_hash).and_return({a: 1}) - suppress_keyword_warning do - m(h).should == [nil, {a: 1}] - end - - h = mock("keyword splat") - h.should_receive(:to_hash).and_return({"a" => 1}) - m(h).should == [h, {}] - end - - evaluate <<-ruby do - def m(*a, **k) [a, k] end - ruby - - m().should == [[], {}] - m(1).should == [[1], {}] - m(a: 1, b: 2).should == [[], {a: 1, b: 2}] - m(1, 2, 3, a: 2).should == [[1, 2, 3], {a: 2}] - - m("a" => 1).should == [[], {"a" => 1}] - m(a: 1).should == [[], {a: 1}] - m("a" => 1, a: 1).should == [[], {"a" => 1, a: 1}] - m({ "a" => 1 }, a: 1).should == [[{"a" => 1}], {a: 1}] - suppress_keyword_warning do - m({a: 1}, {}).should == [[{a: 1}], {}] - end - m({a: 1}, {"a" => 1}).should == [[{a: 1}, {"a" => 1}], {}] - - bo = BasicObject.new - def bo.to_a; [1, 2, 3]; end - def bo.to_hash; {:b => 2, :c => 3}; end - - m(*bo, **bo).should == [[1, 2, 3], {:b => 2, :c => 3}] - end - end - evaluate <<-ruby do def m(*, &b) b end ruby @@ -1503,44 +988,22 @@ describe "A method" do end end - ruby_version_is ''...'2.7' do - evaluate <<-ruby do - def m(a:, **) a end - ruby - - m(a: 1).should == 1 - m(a: 1, b: 2).should == 1 - -> { m("a" => 1, a: 1, b: 2) }.should raise_error(ArgumentError) - end - - evaluate <<-ruby do - def m(a:, **k) [a, k] end - ruby + evaluate <<-ruby do + def m(a:, **) a end + ruby - m(a: 1).should == [1, {}] - m(a: 1, b: 2, c: 3).should == [1, {b: 2, c: 3}] - -> { m("a" => 1, a: 1, b: 2) }.should raise_error(ArgumentError) - end + m(a: 1).should == 1 + m(a: 1, b: 2).should == 1 + m("a" => 1, a: 1, b: 2).should == 1 end - ruby_version_is '2.7' do - evaluate <<-ruby do - def m(a:, **) a end - ruby - - m(a: 1).should == 1 - m(a: 1, b: 2).should == 1 - m("a" => 1, a: 1, b: 2).should == 1 - end - - evaluate <<-ruby do - def m(a:, **k) [a, k] end - ruby + evaluate <<-ruby do + def m(a:, **k) [a, k] end + ruby - m(a: 1).should == [1, {}] - m(a: 1, b: 2, c: 3).should == [1, {b: 2, c: 3}] - m("a" => 1, a: 1, b: 2).should == [1, {"a" => 1, b: 2}] - end + m(a: 1).should == [1, {}] + m(a: 1, b: 2, c: 3).should == [1, {b: 2, c: 3}] + m("a" => 1, a: 1, b: 2).should == [1, {"a" => 1, b: 2}] end evaluate <<-ruby do @@ -1637,125 +1100,66 @@ describe "A method" do result.should == [1, 1, [], 2, 3, 2, 4, { h: 5, i: 6 }, l] end - ruby_version_is "2.7" do - evaluate <<-ruby do - def m(a, **nil); a end; - ruby + evaluate <<-ruby do + def m(a, **nil); a end; + ruby - m({a: 1}).should == {a: 1} - m({"a" => 1}).should == {"a" => 1} + m({a: 1}).should == {a: 1} + m({"a" => 1}).should == {"a" => 1} - -> { m(a: 1) }.should raise_error(ArgumentError) - -> { m(**{a: 1}) }.should raise_error(ArgumentError) - -> { m("a" => 1) }.should raise_error(ArgumentError) - end + -> { m(a: 1) }.should raise_error(ArgumentError, 'no keywords accepted') + -> { m(**{a: 1}) }.should raise_error(ArgumentError, 'no keywords accepted') + -> { m("a" => 1) }.should raise_error(ArgumentError, 'no keywords accepted') end - ruby_version_is ''...'3.0' do - evaluate <<-ruby do - def m(a, b = nil, c = nil, d, e: nil, **f) - [a, b, c, d, e, f] - end - ruby - - result = m(1, 2) - result.should == [1, nil, nil, 2, nil, {}] - - suppress_warning do - result = m(1, 2, {foo: :bar}) - result.should == [1, nil, nil, 2, nil, {foo: :bar}] + evaluate <<-ruby do + def m(a, b = nil, c = nil, d, e: nil, **f) + [a, b, c, d, e, f] end + ruby - result = m(1, {foo: :bar}) - result.should == [1, nil, nil, {foo: :bar}, nil, {}] - end - end - - ruby_version_is '3.0' do - evaluate <<-ruby do - def m(a, b = nil, c = nil, d, e: nil, **f) - [a, b, c, d, e, f] - end - ruby - - result = m(1, 2) - result.should == [1, nil, nil, 2, nil, {}] - - result = m(1, 2, {foo: :bar}) - result.should == [1, 2, nil, {foo: :bar}, nil, {}] - - result = m(1, {foo: :bar}) - result.should == [1, nil, nil, {foo: :bar}, nil, {}] - end - end - end + result = m(1, 2) + result.should == [1, nil, nil, 2, nil, {}] - ruby_version_is '2.7' do - context 'when passing an empty keyword splat to a method that does not accept keywords' do - evaluate <<-ruby do - def m(*a); a; end - ruby + result = m(1, 2, {foo: :bar}) + result.should == [1, 2, nil, {foo: :bar}, nil, {}] - h = {} - m(**h).should == [] - end + result = m(1, {foo: :bar}) + result.should == [1, nil, nil, {foo: :bar}, nil, {}] end end - ruby_version_is '2.7'...'3.0' do - context 'when passing an empty keyword splat to a method that does not accept keywords' do - evaluate <<-ruby do - def m(a); a; end - ruby - h = {} + context 'when passing an empty keyword splat to a method that does not accept keywords' do + evaluate <<-ruby do + def m(*a); a; end + ruby - -> do - m(**h).should == {} - end.should complain(/warning: Passing the keyword argument as the last hash parameter is deprecated/) - end + h = {} + m(**h).should == [] end end - ruby_version_is ''...'3.0' do - context "assigns keyword arguments from a passed Hash without modifying it" do - evaluate <<-ruby do - def m(a: nil); a; end - ruby + context 'when passing an empty keyword splat to a method that does not accept keywords' do + evaluate <<-ruby do + def m(a); a; end + ruby + h = {} - options = {a: 1}.freeze - -> do - suppress_warning do - m(options).should == 1 - end - end.should_not raise_error - options.should == {a: 1} - end + -> do + m(**h).should == {} + end.should raise_error(ArgumentError) end end - ruby_version_is '3.0' do - context 'when passing an empty keyword splat to a method that does not accept keywords' do - evaluate <<-ruby do - def m(a); a; end - ruby - h = {} - - -> do - m(**h).should == {} - end.should raise_error(ArgumentError) - end - end - - context "raises ArgumentError if passing hash as keyword arguments" do - evaluate <<-ruby do - def m(a: nil); a; end - ruby + context "raises ArgumentError if passing hash as keyword arguments" do + evaluate <<-ruby do + def m(a: nil); a; end + ruby - options = {a: 1}.freeze - -> do - m(options) - end.should raise_error(ArgumentError) - end + options = {a: 1}.freeze + -> do + m(options) + end.should raise_error(ArgumentError) end end @@ -1789,14 +1193,42 @@ describe "A method call with a space between method name and parentheses" do end end - context "when a single argument provided" do - it "assigns it" do + context "when a single argument is provided" do + it "assigns a simple expression" do + args = m (1) + args.should == [1] + end + + it "assigns an expression consisting of multiple statements" do + args = m ((0; 1)) + args.should == [1] + end + + it "assigns one single statement, without the need of parentheses" do args = m (1 == 1 ? true : false) args.should == [true] end + + ruby_version_is "3.3" do + it "supports multiple statements" do + eval("m (1; 2)").should == [2] + end + end end - context "when 2+ arguments provided" do + context "when multiple arguments are provided" do + it "assigns simple expressions" do + args = m (1), (2) + args.should == [1, 2] + end + + it "assigns expressions consisting of multiple statements" do + args = m ((0; 1)), ((2; 3)) + args.should == [1, 3] + end + end + + context "when the argument looks like an argument list" do it "raises a syntax error" do -> { eval("m (1, 2)") @@ -1861,15 +1293,176 @@ describe "An array-dereference method ([])" do end end -ruby_version_is '3.0' do - describe "An endless method definition" do +describe "An endless method definition" do + context "without arguments" do evaluate <<-ruby do - def m(a) = a - ruby + def m() = 42 + ruby + + m.should == 42 + end + + context "without parenthesis" do + evaluate <<-ruby do + def m = 42 + ruby + + m.should == 42 + end + end + end + + context "with arguments" do + evaluate <<-ruby do + def m(a, b) = a + b + ruby + + m(1, 4).should == 5 + end + end + + context "with multiline body" do + evaluate <<-ruby do + def m(n) = + if n > 2 + m(n - 2) + m(n - 1) + else + 1 + end + ruby + + m(6).should == 8 + end + end + + # tested more thoroughly in language/delegation_spec.rb + context "with args forwarding" do + evaluate <<-ruby do + def mm(word, num:) + word * num + end + + def m(...) = mm(...) + mm(...) + ruby + + m("meow", num: 2).should == "meow" * 4 + end + end +end + +describe "Keyword arguments are now separated from positional arguments" do + context "when the method has only positional parameters" do + it "treats incoming keyword arguments as positional for compatibility" do + def foo(a, b, c, hsh) + hsh[:key] + end + + foo(1, 2, 3, key: 42).should == 42 + end + end + + context "when the method takes a ** parameter" do + it "captures the passed literal keyword arguments" do + def foo(a, b, c, **hsh) + hsh[:key] + end + + foo(1, 2, 3, key: 42).should == 42 + end + + it "captures the passed ** keyword arguments" do + def foo(a, b, c, **hsh) + hsh[:key] + end + + h = { key: 42 } + foo(1, 2, 3, **h).should == 42 + end + + it "does not convert a positional Hash to keyword arguments" do + def foo(a, b, c, **hsh) + hsh[:key] + end + + -> { + foo(1, 2, 3, { key: 42 }) + }.should raise_error(ArgumentError, 'wrong number of arguments (given 4, expected 3)') + end + end + + context "when the method takes a key: parameter" do + context "when it's called with a positional Hash and no **" do + it "raises ArgumentError" do + def foo(a, b, c, key: 1) + key + end + + -> { + foo(1, 2, 3, { key: 42 }) + }.should raise_error(ArgumentError, 'wrong number of arguments (given 4, expected 3)') + end + end + + context "when it's called with **" do + it "captures the passed keyword arguments" do + def foo(a, b, c, key: 1) + key + end + + h = { key: 42 } + foo(1, 2, 3, **h).should == 42 + end + end + end +end + +ruby_version_is "3.1" do + describe "kwarg with omitted value in a method call" do + context "accepts short notation 'kwarg' in method call" do + evaluate <<-ruby do + def call(*args, **kwargs) = [args, kwargs] + ruby + + a, b, c = 1, 2, 3 + arr, h = eval('call a:') + h.should == {a: 1} + arr.should == [] + + arr, h = eval('call(a:, b:, c:)') + h.should == {a: 1, b: 2, c: 3} + arr.should == [] + + arr, h = eval('call(a:, b: 10, c:)') + h.should == {a: 1, b: 10, c: 3} + arr.should == [] + end + end + + context "with methods and local variables" do + evaluate <<-ruby do + def call(*args, **kwargs) = [args, kwargs] + + def bar + "baz" + end + + def foo(val) + call bar:, val: + end + ruby + + foo(1).should == [[], {bar: "baz", val: 1}] + end + end + end + + describe "Inside 'endless' method definitions" do + it "allows method calls without parenthesis" do + eval <<-ruby + def greet(person) = "Hi, ".dup.concat person + ruby - a = b = m 1 - a.should == 1 - b.should == 1 + greet("Homer").should == "Hi, Homer" end end end diff --git a/spec/ruby/language/module_spec.rb b/spec/ruby/language/module_spec.rb index 1a5fcaf46f..fffcf9c90d 100644 --- a/spec/ruby/language/module_spec.rb +++ b/spec/ruby/language/module_spec.rb @@ -28,9 +28,18 @@ describe "The module keyword" do ModuleSpecs::Reopened.should be_true end - it "reopens a module included in Object" do - module IncludedModuleSpecs; Reopened = true; end - ModuleSpecs::IncludedInObject::IncludedModuleSpecs::Reopened.should be_true + ruby_version_is '3.2' do + it "does not reopen a module included in Object" do + module IncludedModuleSpecs; Reopened = true; end + ModuleSpecs::IncludedInObject::IncludedModuleSpecs.should_not == Object::IncludedModuleSpecs + end + end + + ruby_version_is ''...'3.2' do + it "reopens a module included in Object" do + module IncludedModuleSpecs; Reopened = true; end + ModuleSpecs::IncludedInObject::IncludedModuleSpecs::Reopened.should be_true + end end it "raises a TypeError if the constant is a Class" do @@ -69,20 +78,10 @@ describe "Assigning an anonymous module to a constant" do mod.name.should == "ModuleSpecs_CS1" end - ruby_version_is ""..."3.0" do - it "does not set the name of a module scoped by an anonymous module" do - a, b = Module.new, Module.new - a::B = b - b.name.should be_nil - end - end - - ruby_version_is "3.0" do - it "sets the name of a module scoped by an anonymous module" do - a, b = Module.new, Module.new - a::B = b - b.name.should.end_with? '::B' - end + it "sets the name of a module scoped by an anonymous module" do + a, b = Module.new, Module.new + a::B = b + b.name.should.end_with? '::B' end it "sets the name of contained modules when assigning a toplevel anonymous module" do diff --git a/spec/ruby/language/numbered_parameters_spec.rb b/spec/ruby/language/numbered_parameters_spec.rb index cd1ddf4555..06f9948c58 100644 --- a/spec/ruby/language/numbered_parameters_spec.rb +++ b/spec/ruby/language/numbered_parameters_spec.rb @@ -1,106 +1,104 @@ require_relative '../spec_helper' -ruby_version_is "2.7" do - describe "Numbered parameters" do - it "provides default parameters _1, _2, ... in a block" do - -> { _1 }.call("a").should == "a" - proc { _1 }.call("a").should == "a" - lambda { _1 }.call("a").should == "a" - ["a"].map { _1 }.should == ["a"] - end +describe "Numbered parameters" do + it "provides default parameters _1, _2, ... in a block" do + -> { _1 }.call("a").should == "a" + proc { _1 }.call("a").should == "a" + lambda { _1 }.call("a").should == "a" + ["a"].map { _1 }.should == ["a"] + end - it "assigns nil to not passed parameters" do - proc { [_1, _2] }.call("a").should == ["a", nil] - proc { [_1, _2] }.call("a", "b").should == ["a", "b"] - end + it "assigns nil to not passed parameters" do + proc { [_1, _2] }.call("a").should == ["a", nil] + proc { [_1, _2] }.call("a", "b").should == ["a", "b"] + end - it "supports variables _1-_9 only for the first 9 passed parameters" do - block = proc { [_1, _2, _3, _4, _5, _6, _7, _8, _9] } - result = block.call(1, 2, 3, 4, 5, 6, 7, 8, 9) - result.should == [1, 2, 3, 4, 5, 6, 7, 8, 9] - end + it "supports variables _1-_9 only for the first 9 passed parameters" do + block = proc { [_1, _2, _3, _4, _5, _6, _7, _8, _9] } + result = block.call(1, 2, 3, 4, 5, 6, 7, 8, 9) + result.should == [1, 2, 3, 4, 5, 6, 7, 8, 9] + end - it "does not support more than 9 parameters" do - -> { - proc { [_10] }.call(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) - }.should raise_error(NameError, /undefined local variable or method `_10'/) - end + it "does not support more than 9 parameters" do + -> { + proc { [_10] }.call(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + }.should raise_error(NameError, /undefined local variable or method [`']_10'/) + end - it "can not be used in both outer and nested blocks at the same time" do - -> { - eval("-> { _1; -> { _2 } }") - }.should raise_error(SyntaxError, /numbered parameter is already used in/m) - end + it "can not be used in both outer and nested blocks at the same time" do + -> { + eval("-> { _1; -> { _2 } }") + }.should raise_error(SyntaxError, /numbered parameter is already used in/m) + end - ruby_version_is '2.7'...'3.0' do - it "can be overwritten with local variable" do - suppress_warning do - eval <<~CODE - _1 = 0 - proc { _1 }.call("a").should == 0 - CODE - end - end - - it "warns when numbered parameter is overwritten with local variable" do - -> { - eval("_1 = 0") - }.should complain(/warning: `_1' is reserved for numbered parameter; consider another name/) - end - end + it "cannot be overwritten with local variable" do + -> { + eval <<~CODE + _1 = 0 + proc { _1 }.call("a").should == 0 + CODE + }.should raise_error(SyntaxError, /_1 is reserved for numbered parameter/) + end - ruby_version_is '3.0' do - it "cannot be overwritten with local variable" do - -> { - eval <<~CODE - _1 = 0 - proc { _1 }.call("a").should == 0 - CODE - }.should raise_error(SyntaxError, /_1 is reserved for numbered parameter/) - end - - it "errors when numbered parameter is overwritten with local variable" do - -> { - eval("_1 = 0") - }.should raise_error(SyntaxError, /_1 is reserved for numbered parameter/) - end - end + it "errors when numbered parameter is overwritten with local variable" do + -> { + eval("_1 = 0") + }.should raise_error(SyntaxError, /_1 is reserved for numbered parameter/) + end - it "raises SyntaxError when block parameters are specified explicitly" do - -> { eval("-> () { _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) - -> { eval("-> (x) { _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + it "raises SyntaxError when block parameters are specified explicitly" do + -> { eval("-> () { _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + -> { eval("-> (x) { _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) - -> { eval("proc { || _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) - -> { eval("proc { |x| _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + -> { eval("proc { || _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + -> { eval("proc { |x| _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) - -> { eval("lambda { || _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) - -> { eval("lambda { |x| _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + -> { eval("lambda { || _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + -> { eval("lambda { |x| _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) - -> { eval("['a'].map { || _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) - -> { eval("['a'].map { |x| _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) - end + -> { eval("['a'].map { || _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + -> { eval("['a'].map { |x| _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + end - it "affects block arity" do - -> { _1 }.arity.should == 1 - -> { _2 }.arity.should == 2 - -> { _3 }.arity.should == 3 - -> { _4 }.arity.should == 4 - -> { _5 }.arity.should == 5 - -> { _6 }.arity.should == 6 - -> { _7 }.arity.should == 7 - -> { _8 }.arity.should == 8 - -> { _9 }.arity.should == 9 - - -> { _9 }.arity.should == 9 - proc { _9 }.arity.should == 9 - lambda { _9 }.arity.should == 9 + describe "assigning to a numbered parameter" do + it "raises SyntaxError" do + -> { eval("proc { _1 = 0 }") }.should raise_error(SyntaxError, /_1 is reserved for numbered parameter/) end + end - it "does not work in methods" do - obj = Object.new - def obj.foo; _1 end + it "affects block arity" do + -> { _1 }.arity.should == 1 + -> { _2 }.arity.should == 2 + -> { _3 }.arity.should == 3 + -> { _4 }.arity.should == 4 + -> { _5 }.arity.should == 5 + -> { _6 }.arity.should == 6 + -> { _7 }.arity.should == 7 + -> { _8 }.arity.should == 8 + -> { _9 }.arity.should == 9 + + -> { _9 }.arity.should == 9 + proc { _9 }.arity.should == 9 + lambda { _9 }.arity.should == 9 + end - -> { obj.foo("a") }.should raise_error(ArgumentError, /wrong number of arguments/) - end + it "affects block parameters" do + -> { _1 }.parameters.should == [[:req, :_1]] + -> { _2 }.parameters.should == [[:req, :_1], [:req, :_2]] + + proc { _1 }.parameters.should == [[:opt, :_1]] + proc { _2 }.parameters.should == [[:opt, :_1], [:opt, :_2]] + end + + it "affects binding local variables" do + -> { _1; binding.local_variables }.call("a").should == [:_1] + -> { _2; binding.local_variables }.call("a", "b").should == [:_1, :_2] + end + + it "does not work in methods" do + obj = Object.new + def obj.foo; _1 end + + -> { obj.foo("a") }.should raise_error(ArgumentError, /wrong number of arguments/) end end diff --git a/spec/ruby/language/numbers_spec.rb b/spec/ruby/language/numbers_spec.rb index 418bc60fa6..a8e023efb6 100644 --- a/spec/ruby/language/numbers_spec.rb +++ b/spec/ruby/language/numbers_spec.rb @@ -45,11 +45,15 @@ describe "A number literal" do eval('-3r').should == Rational(-3, 1) end + it "can be an float literal with trailing 'r' to represent a Rational in a canonical form" do + eval('1.0r').should == Rational(1, 1) + end + it "can be a float literal with trailing 'r' to represent a Rational" do eval('0.0174532925199432957r').should == Rational(174532925199432957, 10000000000000000000) end - it "can be an bignum literal with trailing 'r' to represent a Rational" do + it "can be a bignum literal with trailing 'r' to represent a Rational" do eval('1111111111111111111111111111111111111111111111r').should == Rational(1111111111111111111111111111111111111111111111, 1) eval('-1111111111111111111111111111111111111111111111r').should == Rational(-1111111111111111111111111111111111111111111111, 1) end diff --git a/spec/ruby/language/optional_assignments_spec.rb b/spec/ruby/language/optional_assignments_spec.rb index 217dcab74b..2443cc6b79 100644 --- a/spec/ruby/language/optional_assignments_spec.rb +++ b/spec/ruby/language/optional_assignments_spec.rb @@ -57,7 +57,7 @@ describe 'Optional variable assignments' do end end - describe 'using a accessor' do + describe 'using an accessor' do before do klass = Class.new { attr_accessor :b } @a = klass.new @@ -103,6 +103,16 @@ describe 'Optional variable assignments' do @a.b.should == 10 end + it 'does evaluate receiver only once when assigns' do + ScratchPad.record [] + @a.b = nil + + (ScratchPad << :evaluated; @a).b ||= 10 + + ScratchPad.recorded.should == [:evaluated] + @a.b.should == 10 + end + it 'returns the new value if set to false' do def @a.b=(x) :v @@ -122,29 +132,148 @@ describe 'Optional variable assignments' do (@a.b ||= 20).should == 10 end - it 'works when writer is private' do + it 'ignores method visibility when receiver is self' do + klass_with_private_methods = Class.new do + def initialize(v) @a = v end + def public_method(v); self.a ||= v end + private + def a; @a end + def a=(v) @a = v; 42 end + end + + a = klass_with_private_methods.new(false) + a.public_method(10).should == 10 + end + end + + describe 'using a #[]' do + before do + @a = {} klass = Class.new do - def t - self.b = false - (self.b ||= 10).should == 10 - (self.b ||= 20).should == 10 + def [](k) + @hash ||= {} + @hash[k] end - def b - @b + def []=(k, v) + @hash ||= {} + @hash[k] = v + 7 end + end + @b = klass.new + end + + it 'returns the assigned value, not the result of the []= method with ||=' do + (@b[:k] ||= 12).should == 12 + end - def b=(x) - @b = x - :v + it "evaluates the index precisely once" do + ary = [:x, :y] + @a[:x] = 15 + @a[ary.pop] ||= 25 + ary.should == [:x] + @a.should == { x: 15, y: 25 } + end + + it "evaluates the index arguments in the correct order" do + ary = Class.new(Array) do + def [](x, y) + super(x + 3 * y) + end + + def []=(x, y, value) + super(x + 3 * y, value) end + end.new + ary[0, 0] = 1 + ary[1, 0] = 1 + ary[2, 0] = nil + ary[3, 0] = 1 + ary[4, 0] = 1 + ary[5, 0] = 1 + ary[6, 0] = nil + + foo = [0, 2] + + ary[foo.pop, foo.pop] ||= 2 # expected `ary[2, 0] ||= 2` + + ary[2, 0].should == 2 + ary[6, 0].should == nil # returns the same element as `ary[0, 2]` + end + + it 'evaluates receiver only once when assigns' do + ScratchPad.record [] + @a[:k] = nil + + (ScratchPad << :evaluated; @a)[:k] ||= 2 - private :b= + ScratchPad.recorded.should == [:evaluated] + @a[:k].should == 2 + end + + it 'ignores method visibility when receiver is self' do + klass_with_private_methods = Class.new do + def initialize(h) @a = h end + def public_method(k, v); self[k] ||= v end + private + def [](k) @a[k] end + def []=(k, v) @a[k] = v; 42 end end - klass.new.t + a = klass_with_private_methods.new(k: false) + a.public_method(:k, 10).should == 10 end + context 'splatted argument' do + it 'correctly handles it' do + (@b[*[:m]] ||= 10).should == 10 + @b[:m].should == 10 + + (@b[*(1; [:n])] ||= 10).should == 10 + @b[:n].should == 10 + + (@b[*begin 1; [:k] end] ||= 10).should == 10 + @b[:k].should == 10 + end + + it 'calls #to_a only once' do + k = Object.new + def k.to_a + ScratchPad << :to_a + [:k] + end + + ScratchPad.record [] + (@b[*k] ||= 20).should == 20 + @b[:k].should == 20 + ScratchPad.recorded.should == [:to_a] + end + + it 'correctly handles a nested splatted argument' do + (@b[*[*[:k]]] ||= 20).should == 20 + @b[:k].should == 20 + end + + it 'correctly handles multiple nested splatted arguments' do + klass_with_multiple_parameters = Class.new do + def [](k1, k2, k3) + @hash ||= {} + @hash[:"#{k1}#{k2}#{k3}"] + end + + def []=(k1, k2, k3, v) + @hash ||= {} + @hash[:"#{k1}#{k2}#{k3}"] = v + 7 + end + end + a = klass_with_multiple_parameters.new + + (a[*[:a], *[:b], *[:c]] ||= 20).should == 20 + a[:a, :b, :c].should == 20 + end + end end end @@ -191,7 +320,7 @@ describe 'Optional variable assignments' do end end - describe 'using a single variable' do + describe 'using an accessor' do before do klass = Class.new { attr_accessor :b } @a = klass.new @@ -236,6 +365,29 @@ describe 'Optional variable assignments' do @a.b.should == 20 end + + it 'does evaluate receiver only once when assigns' do + ScratchPad.record [] + @a.b = 10 + + (ScratchPad << :evaluated; @a).b &&= 20 + + ScratchPad.recorded.should == [:evaluated] + @a.b.should == 20 + end + + it 'ignores method visibility when receiver is self' do + klass_with_private_methods = Class.new do + def initialize(v) @a = v end + def public_method(v); self.a &&= v end + private + def a; @a end + def a=(v) @a = v; 42 end + end + + a = klass_with_private_methods.new(true) + a.public_method(10).should == 10 + end end describe 'using a #[]' do @@ -297,13 +449,128 @@ describe 'Optional variable assignments' do end it 'returns the assigned value, not the result of the []= method with ||=' do - (@b[:k] ||= 12).should == 12 + @b[:k] = 10 + (@b[:k] &&= 12).should == 12 + end + + it "evaluates the index precisely once" do + ary = [:x, :y] + @a[:x] = 15 + @a[:y] = 20 + @a[ary.pop] &&= 25 + ary.should == [:x] + @a.should == { x: 15, y: 25 } + end + + it "evaluates the index arguments in the correct order" do + ary = Class.new(Array) do + def [](x, y) + super(x + 3 * y) + end + + def []=(x, y, value) + super(x + 3 * y, value) + end + end.new + ary[0, 0] = 1 + ary[1, 0] = 1 + ary[2, 0] = 1 + ary[3, 0] = 1 + ary[4, 0] = 1 + ary[5, 0] = 1 + ary[6, 0] = 1 + + foo = [0, 2] + + ary[foo.pop, foo.pop] &&= 2 # expected `ary[2, 0] &&= 2` + + ary[2, 0].should == 2 + ary[6, 0].should == 1 # returns the same element as `ary[0, 2]` + end + + it 'evaluates receiver only once when assigns' do + ScratchPad.record [] + @a[:k] = 1 + + (ScratchPad << :evaluated; @a)[:k] &&= 2 + + ScratchPad.recorded.should == [:evaluated] + @a[:k].should == 2 end it 'returns the assigned value, not the result of the []= method with +=' do @b[:k] = 17 (@b[:k] += 12).should == 29 end + + it 'ignores method visibility when receiver is self' do + klass_with_private_methods = Class.new do + def initialize(h) @a = h end + def public_method(k, v); self[k] &&= v end + private + def [](k) @a[k] end + def []=(k, v) @a[k] = v; 42 end + end + + a = klass_with_private_methods.new(k: true) + a.public_method(:k, 10).should == 10 + end + + context 'splatted argument' do + it 'correctly handles it' do + @b[:m] = 0 + (@b[*[:m]] &&= 10).should == 10 + @b[:m].should == 10 + + @b[:n] = 0 + (@b[*(1; [:n])] &&= 10).should == 10 + @b[:n].should == 10 + + @b[:k] = 0 + (@b[*begin 1; [:k] end] &&= 10).should == 10 + @b[:k].should == 10 + end + + it 'calls #to_a only once' do + k = Object.new + def k.to_a + ScratchPad << :to_a + [:k] + end + + ScratchPad.record [] + @b[:k] = 10 + (@b[*k] &&= 20).should == 20 + @b[:k].should == 20 + ScratchPad.recorded.should == [:to_a] + end + + it 'correctly handles a nested splatted argument' do + @b[:k] = 10 + (@b[*[*[:k]]] &&= 20).should == 20 + @b[:k].should == 20 + end + + it 'correctly handles multiple nested splatted arguments' do + klass_with_multiple_parameters = Class.new do + def [](k1, k2, k3) + @hash ||= {} + @hash[:"#{k1}#{k2}#{k3}"] + end + + def []=(k1, k2, k3, v) + @hash ||= {} + @hash[:"#{k1}#{k2}#{k3}"] = v + 7 + end + end + a = klass_with_multiple_parameters.new + + a[:a, :b, :c] = 10 + (a[*[:a], *[:b], *[:c]] &&= 20).should == 20 + a[:a, :b, :c].should == 20 + end + end end end @@ -396,7 +663,7 @@ describe 'Optional constant assignment' do ConstantSpecs::ClassA::OR_ASSIGNED_CONSTANT2.should == :assigned end - it 'causes side-effects of the module part to be applied (for nil constant)' do + it 'causes side-effects of the module part to be applied only once (for nil constant)' do suppress_warning do # already initialized constant ConstantSpecs::ClassA::NIL_OR_ASSIGNED_CONSTANT2 = nil x = 0 @@ -454,5 +721,20 @@ describe 'Optional constant assignment' do ConstantSpecs::OpAssignFalse.should == false ConstantSpecs.send :remove_const, :OpAssignFalse end + + it 'causes side-effects of the module part to be applied only once (when assigns)' do + module ConstantSpecs + OpAssignTrue = true + end + + suppress_warning do # already initialized constant + x = 0 + (x += 1; ConstantSpecs)::OpAssignTrue &&= :assigned + x.should == 1 + ConstantSpecs::OpAssignTrue.should == :assigned + end + + ConstantSpecs.send :remove_const, :OpAssignTrue + end end end diff --git a/spec/ruby/language/pattern_matching_spec.rb b/spec/ruby/language/pattern_matching_spec.rb index dae2828bf6..a8ec078cd0 100644 --- a/spec/ruby/language/pattern_matching_spec.rb +++ b/spec/ruby/language/pattern_matching_spec.rb @@ -1,113 +1,370 @@ require_relative '../spec_helper' -ruby_version_is "2.7" do - describe "Pattern matching" do - # TODO: Remove excessive eval calls when support of previous version - # Ruby 2.6 will be dropped +describe "Pattern matching" do + # TODO: Remove excessive eval calls when Ruby 3 is the minimum version. + # It is best to keep the eval's longer if other Ruby impls cannot parse pattern matching yet. - before :each do - ScratchPad.record [] + before :each do + ScratchPad.record [] + end + + describe "can be standalone assoc operator that" do + it "deconstructs value" do + suppress_warning do + eval(<<-RUBY).should == [0, 1] + [0, 1] => [a, b] + [a, b] + RUBY + end end - ruby_version_is "3.0" do - it "can be standalone assoc operator that deconstructs value" do - suppress_warning do - eval(<<-RUBY).should == [0, 1] + it "deconstructs value and properly scopes variables" do + suppress_warning do + eval(<<-RUBY).should == [0, nil] + a = nil + eval(<<-PATTERN) [0, 1] => [a, b] - [a, b] - RUBY - end + PATTERN + [a, defined?(b)] + RUBY end end + end - it "extends case expression with case/in construction" do - eval(<<~RUBY).should == :bar - case [0, 1] - in [0] - :foo - in [0, 1] - :bar + describe "find pattern" do + it "captures preceding elements to the pattern" do + eval(<<~RUBY).should == [0, 1] + case [0, 1, 2, 3] + in [*pre, 2, 3] + pre + else + false end RUBY end - it "allows using then operator" do - eval(<<~RUBY).should == :bar - case [0, 1] - in [0] then :foo - in [0, 1] then :bar + it "captures following elements to the pattern" do + eval(<<~RUBY).should == [2, 3] + case [0, 1, 2, 3] + in [0, 1, *post] + post + else + false + end + RUBY + end + + it "captures both preceding and following elements to the pattern" do + eval(<<~RUBY).should == [[0, 1], [3, 4]] + case [0, 1, 2, 3, 4] + in [*pre, 2, *post] + [pre, post] + else + false + end + RUBY + end + + it "can capture the entirety of the pattern" do + eval(<<~RUBY).should == [0, 1, 2, 3, 4] + case [0, 1, 2, 3, 4] + in [*everything] + everything + else + false end RUBY end - describe "warning" do + it "will match an empty Array-like structure" do + eval(<<~RUBY).should == [] + case [] + in [*everything] + everything + else + false + end + RUBY + end + + it "can be nested" do + eval(<<~RUBY).should == [[0, [2, 4, 6]], [[4, 16, 64]], 27] + case [0, [2, 4, 6], [3, 9, 27], [4, 16, 64]] + in [*pre, [*, 9, a], *post] + [pre, post, a] + else + false + end + RUBY + end + + it "can be nested with an array pattern" do + eval(<<~RUBY).should == [[4, 16, 64]] + case [0, [2, 4, 6], [3, 9, 27], [4, 16, 64]] + in [_, _, [*, 9, *], *post] + post + else + false + end + RUBY + end + + it "can be nested within a hash pattern" do + eval(<<~RUBY).should == [27] + case {a: [3, 9, 27]} + in {a: [*, 9, *post]} + post + else + false + end + RUBY + end + + it "can nest hash and array patterns" do + eval(<<~RUBY).should == [42, 2] + case [0, {a: 42, b: [0, 1]}, {a: 42, b: [1, 2]}] + in [*, {a:, b: [1, c]}, *] + [a, c] + else + false + end + RUBY + end + end + + it "extends case expression with case/in construction" do + eval(<<~RUBY).should == :bar + case [0, 1] + in [0] + :foo + in [0, 1] + :bar + end + RUBY + end + + it "allows using then operator" do + eval(<<~RUBY).should == :bar + case [0, 1] + in [0] then :foo + in [0, 1] then :bar + end + RUBY + end + + describe "warning" do + before :each do + @experimental, Warning[:experimental] = Warning[:experimental], true + end + + after :each do + Warning[:experimental] = @experimental + end + + context 'when regular form' do + before :each do + @src = 'case [0, 1]; in [a, b]; end' + end + + it "does not warn about pattern matching is experimental feature" do + -> { eval @src }.should_not complain + end + end + + context 'when one-line form' do before :each do - ruby_version_is ""..."3.0" do - @src = 'case [0, 1]; in [a, b]; end' + @src = '[0, 1] => [a, b]' + end + + ruby_version_is ""..."3.1" do + it "warns about pattern matching is experimental feature" do + -> { eval @src }.should complain(/pattern matching is experimental, and the behavior may change in future versions of Ruby!/i) end + end - ruby_version_is "3.0" do - @src = '[0, 1] => [a, b]' + ruby_version_is "3.1" do + it "does not warn about pattern matching is experimental feature" do + -> { eval @src }.should_not complain end + end + end + end - @experimental, Warning[:experimental] = Warning[:experimental], true + it "binds variables" do + eval(<<~RUBY).should == 1 + case [0, 1] + in [0, a] + a end + RUBY + end + + it "cannot mix in and when operators" do + -> { + eval <<~RUBY + case [] + when 1 == 1 + in [] + end + RUBY + }.should raise_error(SyntaxError, /syntax error, unexpected `in'|\(eval\):3: syntax error, unexpected keyword_in|unexpected 'in'/) + + -> { + eval <<~RUBY + case [] + in [] + when 1 == 1 + end + RUBY + }.should raise_error(SyntaxError, /syntax error, unexpected `when'|\(eval\):3: syntax error, unexpected keyword_when|unexpected 'when'/) + end + + it "checks patterns until the first matching" do + eval(<<~RUBY).should == :bar + case [0, 1] + in [0] + :foo + in [0, 1] + :bar + in [0, 1] + :baz + end + RUBY + end - after :each do - Warning[:experimental] = @experimental + it "executes else clause if no pattern matches" do + eval(<<~RUBY).should == false + case [0, 1] + in [0] + true + else + false end + RUBY + end + + it "raises NoMatchingPatternError if no pattern matches and no else clause" do + -> { + eval <<~RUBY + case [0, 1] + in [0] + end + RUBY + }.should raise_error(NoMatchingPatternError, /\[0, 1\]/) + end + + it "raises NoMatchingPatternError if no pattern matches and evaluates the expression only once" do + evals = 0 + -> { + eval <<~RUBY + case (evals += 1; [0, 1]) + in [0] + end + RUBY + }.should raise_error(NoMatchingPatternError, /\[0, 1\]/) + evals.should == 1 + end + + it "does not allow calculation or method calls in a pattern" do + -> { + eval <<~RUBY + case 0 + in 1 + 1 + true + end + RUBY + }.should raise_error(SyntaxError, /unexpected|expected a delimiter after the predicates of a `when` clause/) + end - it "warns about pattern matching is experimental feature" do - -> { eval @src }.should complain(/pattern matching is experimental, and the behavior may change in future versions of Ruby!/i) + it "evaluates the case expression once for multiple patterns, caching the result" do + eval(<<~RUBY).should == true + case (ScratchPad << :foo; 1) + in 0 + false + in 1 + true end + RUBY + + ScratchPad.recorded.should == [:foo] + end + + describe "guards" do + it "supports if guard" do + eval(<<~RUBY).should == false + case 0 + in 0 if false + true + else + false + end + RUBY + + eval(<<~RUBY).should == true + case 0 + in 0 if true + true + else + false + end + RUBY end - it "binds variables" do - eval(<<~RUBY).should == 1 + it "supports unless guard" do + eval(<<~RUBY).should == false + case 0 + in 0 unless true + true + else + false + end + RUBY + + eval(<<~RUBY).should == true + case 0 + in 0 unless false + true + else + false + end + RUBY + end + + it "makes bound variables visible in guard" do + eval(<<~RUBY).should == true case [0, 1] - in [0, a] - a + in [a, 1] if a >= 0 + true end RUBY end - it "cannot mix in and when operators" do - -> { - eval <<~RUBY - case [] - when 1 == 1 - in [] - end - RUBY - }.should raise_error(SyntaxError, /syntax error, unexpected `in'/) + it "does not evaluate guard if pattern does not match" do + eval <<~RUBY + case 0 + in 1 if (ScratchPad << :foo) || true + else + end + RUBY - -> { - eval <<~RUBY - case [] - in [] - when 1 == 1 - end - RUBY - }.should raise_error(SyntaxError, /syntax error, unexpected `when'/) + ScratchPad.recorded.should == [] end - it "checks patterns until the first matching" do + it "takes guards into account when there are several matching patterns" do eval(<<~RUBY).should == :bar - case [0, 1] - in [0] + case 0 + in 0 if false :foo - in [0, 1] + in 0 if true :bar - in [0, 1] - :baz end RUBY end - it "executes else clause if no pattern matches" do + it "executes else clause if no guarded pattern matches" do eval(<<~RUBY).should == false - case [0, 1] - in [0] + case 0 + in 0 if false true else false @@ -115,1023 +372,1138 @@ ruby_version_is "2.7" do RUBY end - it "raises NoMatchingPatternError if no pattern matches and no else clause" do + it "raises NoMatchingPatternError if no guarded pattern matches and no else clause" do -> { eval <<~RUBY case [0, 1] - in [0] + in [0, 1] if false end RUBY }.should raise_error(NoMatchingPatternError, /\[0, 1\]/) end + end - it "does not allow calculation or method calls in a pattern" do - -> { - eval <<~RUBY - case 0 - in 1 + 1 - true - end - RUBY - }.should raise_error(SyntaxError, /unexpected/) + describe "value pattern" do + it "matches an object such that pattern === object" do + eval(<<~RUBY).should == true + case 0 + in 0 + true + end + RUBY + + eval(<<~RUBY).should == true + case 0 + in (-1..1) + true + end + RUBY + + eval(<<~RUBY).should == true + case 0 + in Integer + true + end + RUBY + + eval(<<~RUBY).should == true + case "0" + in /0/ + true + end + RUBY + + eval(<<~RUBY).should == true + case "0" + in ->(s) { s == "0" } + true + end + RUBY end - it "evaluates the case expression once for multiple patterns, caching the result" do + it "allows string literal with interpolation" do + x = "x" + eval(<<~RUBY).should == true - case (ScratchPad << :foo; 1) - in 0 - false - in 1 + case "x" + in "#{x + ""}" true end RUBY + end + end - ScratchPad.recorded.should == [:foo] + describe "variable pattern" do + it "matches a value and binds variable name to this value" do + eval(<<~RUBY).should == 0 + case 0 + in a + a + end + RUBY end - describe "guards" do - it "supports if guard" do - eval(<<~RUBY).should == false - case 0 - in 0 if false - true - else - false - end - RUBY + it "makes bounded variable visible outside a case statement scope" do + eval(<<~RUBY).should == 0 + case 0 + in a + end - eval(<<~RUBY).should == true - case 0 - in 0 if true - true - else - false - end - RUBY - end + a + RUBY + end - it "supports unless guard" do - eval(<<~RUBY).should == false - case 0 - in 0 unless true - true - else - false - end - RUBY + it "create local variables even if a pattern doesn't match" do + eval(<<~RUBY).should == [0, nil, nil] + case 0 + in a + in b + in c + end - eval(<<~RUBY).should == true - case 0 - in 0 unless false - true - else - false - end - RUBY - end + [a, b, c] + RUBY + end - it "makes bound variables visible in guard" do - eval(<<~RUBY).should == true - case [0, 1] - in [a, 1] if a >= 0 - true - end - RUBY - end + it "allow using _ name to drop values" do + eval(<<~RUBY).should == 0 + case [0, 1] + in [a, _] + a + end + RUBY + end - it "does not evaluate guard if pattern does not match" do + it "supports using _ in a pattern several times" do + eval(<<~RUBY).should == true + case [0, 1, 2] + in [0, _, _] + true + end + RUBY + end + + it "supports using any name with _ at the beginning in a pattern several times" do + eval(<<~RUBY).should == true + case [0, 1, 2] + in [0, _x, _x] + true + end + RUBY + + eval(<<~RUBY).should == true + case {a: 0, b: 1, c: 2} + in {a: 0, b: _x, c: _x} + true + end + RUBY + end + + it "does not support using variable name (except _) several times" do + -> { eval <<~RUBY - case 0 - in 1 if (ScratchPad << :foo) || true - else + case [0] + in [a, a] end RUBY + }.should raise_error(SyntaxError, /duplicated variable name/) + end - ScratchPad.recorded.should == [] - end + it "supports existing variables in a pattern specified with ^ operator" do + a = 0 - it "takes guards into account when there are several matching patterns" do - eval(<<~RUBY).should == :bar - case 0 - in 0 if false - :foo - in 0 if true - :bar - end - RUBY - end + eval(<<~RUBY).should == true + case 0 + in ^a + true + end + RUBY + end - it "executes else clause if no guarded pattern matches" do - eval(<<~RUBY).should == false - case 0 - in 0 if false + it "allows applying ^ operator to bound variables" do + eval(<<~RUBY).should == 1 + case [1, 1] + in [n, ^n] + n + end + RUBY + + eval(<<~RUBY).should == false + case [1, 2] + in [n, ^n] + true + else + false + end + RUBY + end + + it "requires bound variable to be specified in a pattern before ^ operator when it relies on a bound variable" do + -> { + eval <<~RUBY + case [1, 2] + in [^n, n] true else false end RUBY - end + }.should raise_error(SyntaxError, /n: no such local variable/) + end + end - it "raises NoMatchingPatternError if no guarded pattern matches and no else clause" do - -> { - eval <<~RUBY - case [0, 1] - in [0, 1] if false - end - RUBY - }.should raise_error(NoMatchingPatternError, /\[0, 1\]/) - end + describe "alternative pattern" do + it "matches if any of patterns matches" do + eval(<<~RUBY).should == true + case 0 + in 0 | 1 | 2 + true + end + RUBY end - describe "value pattern" do - it "matches an object such that pattern === object" do - eval(<<~RUBY).should == true - case 0 - in 0 - true + it "does not support variable binding" do + -> { + eval <<~RUBY + case [0, 1] + in [0, 0] | [0, a] end RUBY + }.should raise_error(SyntaxError, /illegal variable in alternative pattern/) + end - eval(<<~RUBY).should == true - case 0 - in (-1..1) - true - end - RUBY + it "support underscore prefixed variables in alternation" do + eval(<<~RUBY).should == true + case [0, 1] + in [1, _] + false + in [0, 0] | [0, _a] + true + end + RUBY + end - eval(<<~RUBY).should == true - case 0 - in Integer + it "can be used as a nested pattern" do + eval(<<~RUBY).should == true + case [[1], ["2"]] + in [[0] | nil, _] + false + in [[1], [1]] + false + in [[1], [2 | "2"]] true - end - RUBY + end + RUBY - eval(<<~RUBY).should == true - case "0" - in /0/ + eval(<<~RUBY).should == true + case [1, 2] + in [0, _] | {a: 0} + false + in {a: 1, b: 2} | [1, 2] true - end - RUBY + end + RUBY + end + end - eval(<<~RUBY).should == true - case "0" - in ->(s) { s == "0" } - true - end - RUBY - end + describe "AS pattern" do + it "binds a variable to a value if pattern matches" do + eval(<<~RUBY).should == 0 + case 0 + in Integer => n + n + end + RUBY + end - it "allows string literal with interpolation" do - x = "x" + it "can be used as a nested pattern" do + eval(<<~RUBY).should == [2, 3] + case [1, [2, 3]] + in [1, Array => ary] + ary + end + RUBY + end + end - eval(<<~RUBY).should == true - case "x" - in "#{x + ""}" - true - end - RUBY - end + describe "Array pattern" do + it "supports form Constant(pat, pat, ...)" do + eval(<<~RUBY).should == true + case [0, 1, 2] + in Array(0, 1, 2) + true + end + RUBY end - describe "variable pattern" do - it "matches a value and binds variable name to this value" do - eval(<<~RUBY).should == 0 - case 0 - in a - a - end - RUBY - end + it "supports form Constant[pat, pat, ...]" do + eval(<<~RUBY).should == true + case [0, 1, 2] + in Array[0, 1, 2] + true + end + RUBY + end - it "makes bounded variable visible outside a case statement scope" do - eval(<<~RUBY).should == 0 - case 0 - in a - end + it "supports form [pat, pat, ...]" do + eval(<<~RUBY).should == true + case [0, 1, 2] + in [0, 1, 2] + true + end + RUBY + end + it "supports form pat, pat, ..." do + eval(<<~RUBY).should == true + case [0, 1, 2] + in 0, 1, 2 + true + end + RUBY + + eval(<<~RUBY).should == 1 + case [0, 1, 2] + in 0, a, 2 a - RUBY - end + end + RUBY - it "create local variables even if a pattern doesn't match" do - eval(<<~RUBY).should == [0, nil, nil] - case 0 - in a - in b - in c - end + eval(<<~RUBY).should == [1, 2] + case [0, 1, 2] + in 0, *rest + rest + end + RUBY + end - [a, b, c] - RUBY - end + it "matches an object with #deconstruct method which returns an array and each element in array matches element in pattern" do + obj = Object.new + def obj.deconstruct; [0, 1] end - it "allow using _ name to drop values" do - eval(<<~RUBY).should == 0 - case [0, 1] - in [a, _] - a - end - RUBY - end + eval(<<~RUBY).should == true + case obj + in [Integer, Integer] + true + end + RUBY + end - it "supports using _ in a pattern several times" do - eval(<<~RUBY).should == true - case [0, 1, 2] - in [0, _, _] - true - end - RUBY + it "calls #deconstruct once for multiple patterns, caching the result" do + obj = Object.new + + def obj.deconstruct + ScratchPad << :deconstruct + [0, 1] end - it "supports using any name with _ at the beginning in a pattern several times" do - eval(<<~RUBY).should == true - case [0, 1, 2] - in [0, _x, _x] - true - end - RUBY + eval(<<~RUBY).should == true + case obj + in [1, 2] + false + in [0, 1] + true + end + RUBY - eval(<<~RUBY).should == true - case {a: 0, b: 1, c: 2} - in {a: 0, b: _x, c: _x} - true - end - RUBY - end + ScratchPad.recorded.should == [:deconstruct] + end - it "does not support using variable name (except _) several times" do - -> { - eval <<~RUBY - case [0] - in [a, a] - end - RUBY - }.should raise_error(SyntaxError, /duplicated variable name/) + it "calls #deconstruct even on objects that are already an array" do + obj = [1, 2] + def obj.deconstruct + ScratchPad << :deconstruct + [3, 4] end - it "supports existing variables in a pattern specified with ^ operator" do - a = 0 + eval(<<~RUBY).should == true + case obj + in [3, 4] + true + else + false + end + RUBY - eval(<<~RUBY).should == true - case 0 - in ^a - true - end - RUBY - end + ScratchPad.recorded.should == [:deconstruct] + end - it "allows applying ^ operator to bound variables" do - eval(<<~RUBY).should == 1 - case [1, 1] - in [n, ^n] - n - end - RUBY + it "does not match object if Constant === object returns false" do + eval(<<~RUBY).should == false + case [0, 1, 2] + in String[0, 1, 2] + true + else + false + end + RUBY + end - eval(<<~RUBY).should == false - case [1, 2] - in [n, ^n] - true - else - false - end - RUBY - end + it "checks Constant === object before calling #deconstruct" do + c1 = Class.new + obj = c1.new + obj.should_not_receive(:deconstruct) + eval(<<~RUBY).should == false + case obj + in String[1] + true + else + false + end + RUBY + end - it "requires bound variable to be specified in a pattern before ^ operator when it relies on a bound variable" do - -> { - eval <<~RUBY - case [1, 2] - in [^n, n] - true - else - false - end - RUBY - }.should raise_error(SyntaxError, /n: no such local variable/) - end + it "does not match object without #deconstruct method" do + obj = Object.new + obj.should_receive(:respond_to?).with(:deconstruct) + + eval(<<~RUBY).should == false + case obj + in Object[] + true + else + false + end + RUBY end - describe "alternative pattern" do - it "matches if any of patterns matches" do - eval(<<~RUBY).should == true - case 0 - in 0 | 1 | 2 - true + it "raises TypeError if #deconstruct method does not return array" do + obj = Object.new + def obj.deconstruct; "" end + + -> { + eval <<~RUBY + case obj + in Object[] + else end RUBY - end + }.should raise_error(TypeError, /deconstruct must return Array/) + end - it "does not support variable binding" do - -> { - eval <<~RUBY - case [0, 1] - in [0, 0] | [0, a] - end - RUBY - }.should raise_error(SyntaxError, /illegal variable in alternative pattern/) + it "accepts a subclass of Array from #deconstruct" do + obj = Object.new + def obj.deconstruct + Class.new(Array).new([0, 1]) end - it "support underscore prefixed variables in alternation" do - eval(<<~RUBY).should == true - case [0, 1] - in [1, _] - false - in [0, 0] | [0, _a] - true - end - RUBY - end + eval(<<~RUBY).should == true + case obj + in [1, 2] + false + in [0, 1] + true + end + RUBY end - describe "AS pattern" do - it "binds a variable to a value if pattern matches" do - eval(<<~RUBY).should == 0 - case 0 - in Integer => n - n - end - RUBY - end + it "does not match object if elements of array returned by #deconstruct method does not match elements in pattern" do + obj = Object.new + def obj.deconstruct; [1] end - it "can be used as a nested pattern" do - eval(<<~RUBY).should == [2, 3] - case [1, [2, 3]] - in [1, Array => ary] - ary - end - RUBY - end + eval(<<~RUBY).should == false + case obj + in Object[0] + true + else + false + end + RUBY end - describe "Array pattern" do - it "supports form Constant(pat, pat, ...)" do - eval(<<~RUBY).should == true - case [0, 1, 2] - in Array(0, 1, 2) - true - end - RUBY - end + it "binds variables" do + eval(<<~RUBY).should == [0, 1, 2] + case [0, 1, 2] + in [a, b, c] + [a, b, c] + end + RUBY + end - it "supports form Constant[pat, pat, ...]" do - eval(<<~RUBY).should == true - case [0, 1, 2] - in Array[0, 1, 2] - true - end - RUBY - end + it "supports splat operator *rest" do + eval(<<~RUBY).should == [1, 2] + case [0, 1, 2] + in [0, *rest] + rest + end + RUBY + end - it "supports form [pat, pat, ...]" do - eval(<<~RUBY).should == true - case [0, 1, 2] - in [0, 1, 2] - true - end - RUBY - end + it "does not match partially by default" do + eval(<<~RUBY).should == false + case [0, 1, 2, 3] + in [1, 2] + true + else + false + end + RUBY + end - it "supports form pat, pat, ..." do - eval(<<~RUBY).should == true - case [0, 1, 2] - in 0, 1, 2 - true - end - RUBY + it "does match partially from the array beginning if list + , syntax used" do + eval(<<~RUBY).should == true + case [0, 1, 2, 3] + in [0, 1,] + true + end + RUBY - eval(<<~RUBY).should == 1 - case [0, 1, 2] - in 0, a, 2 - a - end - RUBY + eval(<<~RUBY).should == true + case [0, 1, 2, 3] + in 0, 1,; + true + end + RUBY + end - eval(<<~RUBY).should == [1, 2] - case [0, 1, 2] - in 0, *rest - rest - end - RUBY - end + it "matches [] with []" do + eval(<<~RUBY).should == true + case [] + in [] + true + end + RUBY + end - it "matches an object with #deconstruct method which returns an array and each element in array matches element in pattern" do - obj = Object.new - def obj.deconstruct; [0, 1] end + it "matches anything with *" do + eval(<<~RUBY).should == true + case [0, 1] + in *; + true + end + RUBY + end - eval(<<~RUBY).should == true - case obj - in [Integer, Integer] + it "can be used as a nested pattern" do + eval(<<~RUBY).should == true + case [[1], ["2"]] + in [[0] | nil, _] + false + in [[1], [1]] + false + in [[1], [2 | "2"]] true - end - RUBY - end + end + RUBY - ruby_version_is "3.0" do - it "calls #deconstruct once for multiple patterns, caching the result" do - obj = Object.new + eval(<<~RUBY).should == true + case [1, 2] + in [0, _] | {a: 0} + false + in {a: 1, b: 2} | [1, 2] + true + end + RUBY + end + end - def obj.deconstruct - ScratchPad << :deconstruct - [0, 1] - end + describe "Hash pattern" do + it "supports form Constant(id: pat, id: pat, ...)" do + eval(<<~RUBY).should == true + case {a: 0, b: 1} + in Hash(a: 0, b: 1) + true + end + RUBY + end - eval(<<~RUBY).should == true - case obj - in [1, 2] - false - in [0, 1] - true - end - RUBY + it "supports form Constant[id: pat, id: pat, ...]" do + eval(<<~RUBY).should == true + case {a: 0, b: 1} + in Hash[a: 0, b: 1] + true + end + RUBY + end - ScratchPad.recorded.should == [:deconstruct] + it "supports form {id: pat, id: pat, ...}" do + eval(<<~RUBY).should == true + case {a: 0, b: 1} + in {a: 0, b: 1} + true end - end + RUBY + end - it "calls #deconstruct even on objects that are already an array" do - obj = [1, 2] - def obj.deconstruct - ScratchPad << :deconstruct - [3, 4] + it "supports form id: pat, id: pat, ..." do + eval(<<~RUBY).should == true + case {a: 0, b: 1} + in a: 0, b: 1 + true end + RUBY - eval(<<~RUBY).should == true - case obj - in [3, 4] - true - else - false - end - RUBY + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in a: a, b: b + [a, b] + end + RUBY - ScratchPad.recorded.should == [:deconstruct] - end + eval(<<~RUBY).should == { b: 1, c: 2 } + case {a: 0, b: 1, c: 2} + in a: 0, **rest + rest + end + RUBY + end - it "does not match object if Constant === object returns false" do - eval(<<~RUBY).should == false - case [0, 1, 2] - in String[0, 1, 2] - true - else - false - end - RUBY - end + it "supports a: which means a: a" do + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in Hash(a:, b:) + [a, b] + end + RUBY - it "does not match object without #deconstruct method" do - obj = Object.new - obj.should_receive(:respond_to?).with(:deconstruct) + a = b = nil + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in Hash[a:, b:] + [a, b] + end + RUBY - eval(<<~RUBY).should == false - case obj - in Object[] - true - else - false - end - RUBY - end + a = b = nil + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in {a:, b:} + [a, b] + end + RUBY - it "raises TypeError if #deconstruct method does not return array" do - obj = Object.new - def obj.deconstruct; "" end + a = nil + eval(<<~RUBY).should == [0, {b: 1, c: 2}] + case {a: 0, b: 1, c: 2} + in {a:, **rest} + [a, rest] + end + RUBY - -> { - eval <<~RUBY - case obj - in Object[] - else - end - RUBY - }.should raise_error(TypeError, /deconstruct must return Array/) - end + a = b = nil + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in a:, b: + [a, b] + end + RUBY + end - it "accepts a subclass of Array from #deconstruct" do - obj = Object.new - def obj.deconstruct - subarray = Class.new(Array).new(2) - def subarray.[](n) - n - end - subarray + it "can mix key (a:) and key-value (a: b) declarations" do + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in Hash(a:, b: x) + [a, x] end + RUBY + end - eval(<<~RUBY).should == true - case obj - in [1, 2] - false - in [0, 1] - true + it "supports 'string': key literal" do + eval(<<~RUBY).should == true + case {a: 0} + in {"a": 0} + true + end + RUBY + end + + it "does not support non-symbol keys" do + -> { + eval <<~RUBY + case {a: 1} + in {"a" => 1} end RUBY - end + }.should raise_error(SyntaxError, /unexpected|expected a label as the key in the hash pattern/) + end - it "does not match object if elements of array returned by #deconstruct method does not match elements in pattern" do - obj = Object.new - def obj.deconstruct; [1] end + it "does not support string interpolation in keys" do + x = "a" - eval(<<~RUBY).should == false - case obj - in Object[0] - true - else - false + -> { + eval <<~'RUBY' + case {a: 1} + in {"#{x}": 1} end RUBY - end + }.should raise_error(SyntaxError, /symbol literal with interpolation is not allowed|expected a label as the key in the hash pattern/) + end - it "binds variables" do - eval(<<~RUBY).should == [0, 1, 2] - case [0, 1, 2] - in [a, b, c] - [a, b, c] + it "raise SyntaxError when keys duplicate in pattern" do + -> { + eval <<~RUBY + case {a: 1} + in {a: 1, b: 2, a: 3} end RUBY - end + }.should raise_error(SyntaxError, /duplicated key name/) + end - it "supports splat operator *rest" do - eval(<<~RUBY).should == [1, 2] - case [0, 1, 2] - in [0, *rest] - rest - end - RUBY - end + it "matches an object with #deconstruct_keys method which returns a Hash with equal keys and each value in Hash matches value in pattern" do + obj = Object.new + def obj.deconstruct_keys(*); {a: 1} end - it "does not match partially by default" do - eval(<<~RUBY).should == false - case [0, 1, 2, 3] - in [1, 2] - true - else - false - end - RUBY - end + eval(<<~RUBY).should == true + case obj + in {a: 1} + true + end + RUBY + end - it "does match partially from the array beginning if list + , syntax used" do - eval(<<~RUBY).should == true - case [0, 1, 2, 3] - in [0, 1,] - true - end - RUBY + it "calls #deconstruct_keys per pattern" do + obj = Object.new - eval(<<~RUBY).should == true - case [0, 1, 2, 3] - in 0, 1,; - true - end - RUBY + def obj.deconstruct_keys(*) + ScratchPad << :deconstruct_keys + {a: 1} end - it "matches [] with []" do - eval(<<~RUBY).should == true - case [] - in [] - true - end - RUBY - end + eval(<<~RUBY).should == true + case obj + in {b: 1} + false + in {a: 1} + true + end + RUBY - it "matches anything with *" do - eval(<<~RUBY).should == true - case [0, 1] - in *; - true - end - RUBY - end + ScratchPad.recorded.should == [:deconstruct_keys, :deconstruct_keys] end - describe "Hash pattern" do - it "supports form Constant(id: pat, id: pat, ...)" do - eval(<<~RUBY).should == true - case {a: 0, b: 1} - in Hash(a: 0, b: 1) - true - end - RUBY - end + it "does not match object if Constant === object returns false" do + eval(<<~RUBY).should == false + case {a: 1} + in String[a: 1] + true + else + false + end + RUBY + end - it "supports form Constant[id: pat, id: pat, ...]" do - eval(<<~RUBY).should == true - case {a: 0, b: 1} - in Hash[a: 0, b: 1] - true - end - RUBY - end + it "checks Constant === object before calling #deconstruct_keys" do + c1 = Class.new + obj = c1.new + obj.should_not_receive(:deconstruct_keys) + eval(<<~RUBY).should == false + case obj + in String(a: 1) + true + else + false + end + RUBY + end - it "supports form {id: pat, id: pat, ...}" do - eval(<<~RUBY).should == true - case {a: 0, b: 1} - in {a: 0, b: 1} - true - end - RUBY - end + it "does not match object without #deconstruct_keys method" do + obj = Object.new + obj.should_receive(:respond_to?).with(:deconstruct_keys) - it "supports form id: pat, id: pat, ..." do - eval(<<~RUBY).should == true - case {a: 0, b: 1} - in a: 0, b: 1 - true - end - RUBY + eval(<<~RUBY).should == false + case obj + in Object[a: 1] + true + else + false + end + RUBY + end - eval(<<~RUBY).should == [0, 1] - case {a: 0, b: 1} - in a: a, b: b - [a, b] - end - RUBY + it "does not match object if #deconstruct_keys method does not return Hash" do + obj = Object.new + def obj.deconstruct_keys(*); "" end - eval(<<~RUBY).should == { b: 1, c: 2 } - case {a: 0, b: 1, c: 2} - in a: 0, **rest - rest + -> { + eval <<~RUBY + case obj + in Object[a: 1] end RUBY - end + }.should raise_error(TypeError, /deconstruct_keys must return Hash/) + end - it "supports a: which means a: a" do - eval(<<~RUBY).should == [0, 1] - case {a: 0, b: 1} - in Hash(a:, b:) - [a, b] - end - RUBY + it "does not match object if #deconstruct_keys method returns Hash with non-symbol keys" do + obj = Object.new + def obj.deconstruct_keys(*); {"a" => 1} end - a = b = nil - eval(<<~RUBY).should == [0, 1] - case {a: 0, b: 1} - in Hash[a:, b:] - [a, b] - end - RUBY + eval(<<~RUBY).should == false + case obj + in Object[a: 1] + true + else + false + end + RUBY + end - a = b = nil - eval(<<~RUBY).should == [0, 1] - case {a: 0, b: 1} - in {a:, b:} - [a, b] - end - RUBY + it "does not match object if elements of Hash returned by #deconstruct_keys method does not match values in pattern" do + obj = Object.new + def obj.deconstruct_keys(*); {a: 1} end - a = nil - eval(<<~RUBY).should == [0, {b: 1, c: 2}] - case {a: 0, b: 1, c: 2} - in {a:, **rest} - [a, rest] - end - RUBY + eval(<<~RUBY).should == false + case obj + in Object[a: 2] + true + else + false + end + RUBY + end - a = b = nil - eval(<<~RUBY).should == [0, 1] - case {a: 0, b: 1} - in a:, b: - [a, b] - end - RUBY - end + it "passes keys specified in pattern as arguments to #deconstruct_keys method" do + obj = Object.new - it "can mix key (a:) and key-value (a: b) declarations" do - eval(<<~RUBY).should == [0, 1] - case {a: 0, b: 1} - in Hash(a:, b: x) - [a, x] - end - RUBY + def obj.deconstruct_keys(*args) + ScratchPad << args + {a: 1, b: 2, c: 3} end - it "supports 'string': key literal" do - eval(<<~RUBY).should == true - case {a: 0} - in {"a": 0} - true - end - RUBY - end + eval <<~RUBY + case obj + in Object[a: 1, b: 2, c: 3] + end + RUBY - it "does not support non-symbol keys" do - -> { - eval <<~RUBY - case {a: 1} - in {"a" => 1} - end - RUBY - }.should raise_error(SyntaxError, /unexpected/) - end + ScratchPad.recorded.sort.should == [[[:a, :b, :c]]] + end - it "does not support string interpolation in keys" do - x = "a" + it "passes keys specified in pattern to #deconstruct_keys method if pattern contains double splat operator **" do + obj = Object.new - -> { - eval <<~'RUBY' - case {a: 1} - in {"#{x}": 1} - end - RUBY - }.should raise_error(SyntaxError, /symbol literal with interpolation is not allowed/) + def obj.deconstruct_keys(*args) + ScratchPad << args + {a: 1, b: 2, c: 3} end - it "raise SyntaxError when keys duplicate in pattern" do - -> { - eval <<~RUBY - case {a: 1} - in {a: 1, b: 2, a: 3} - end - RUBY - }.should raise_error(SyntaxError, /duplicated key name/) - end + eval <<~RUBY + case obj + in Object[a: 1, b: 2, **] + end + RUBY - it "matches an object with #deconstruct_keys method which returns a Hash with equal keys and each value in Hash matches value in pattern" do - obj = Object.new - def obj.deconstruct_keys(*); {a: 1} end + ScratchPad.recorded.sort.should == [[[:a, :b]]] + end - eval(<<~RUBY).should == true - case obj - in {a: 1} - true - end - RUBY + it "passes nil to #deconstruct_keys method if pattern contains double splat operator **rest" do + obj = Object.new + + def obj.deconstruct_keys(*args) + ScratchPad << args + {a: 1, b: 2} end - it "calls #deconstruct_keys per pattern" do - obj = Object.new + eval <<~RUBY + case obj + in Object[a: 1, **rest] + end + RUBY - def obj.deconstruct_keys(*) - ScratchPad << :deconstruct_keys - {a: 1} + ScratchPad.recorded.should == [[nil]] + end + + it "binds variables" do + eval(<<~RUBY).should == [0, 1, 2] + case {a: 0, b: 1, c: 2} + in {a: x, b: y, c: z} + [x, y, z] end + RUBY + end - eval(<<~RUBY).should == true - case obj - in {b: 1} - false - in {a: 1} - true - end - RUBY + it "supports double splat operator **rest" do + eval(<<~RUBY).should == {b: 1, c: 2} + case {a: 0, b: 1, c: 2} + in {a: 0, **rest} + rest + end + RUBY + end - ScratchPad.recorded.should == [:deconstruct_keys, :deconstruct_keys] - end + it "treats **nil like there should not be any other keys in a matched Hash" do + eval(<<~RUBY).should == true + case {a: 1, b: 2} + in {a: 1, b: 2, **nil} + true + end + RUBY - it "does not match object if Constant === object returns false" do - eval(<<~RUBY).should == false - case {a: 1} - in String[a: 1] - true - else - false - end - RUBY - end + eval(<<~RUBY).should == false + case {a: 1, b: 2} + in {a: 1, **nil} + true + else + false + end + RUBY + end - it "does not match object without #deconstruct_keys method" do - obj = Object.new - obj.should_receive(:respond_to?).with(:deconstruct_keys) + it "can match partially" do + eval(<<~RUBY).should == true + case {a: 1, b: 2} + in {a: 1} + true + end + RUBY + end - eval(<<~RUBY).should == false - case obj - in Object[a: 1] - true - else - false - end - RUBY - end + it "matches {} with {}" do + eval(<<~RUBY).should == true + case {} + in {} + true + end + RUBY + end - it "does not match object if #deconstruct_keys method does not return Hash" do - obj = Object.new - def obj.deconstruct_keys(*); "" end + it "in {} only matches empty hashes" do + eval(<<~RUBY).should == false + case {a: 1} + in {} + true + else + false + end + RUBY + end - -> { - eval <<~RUBY - case obj - in Object[a: 1] - end - RUBY - }.should raise_error(TypeError, /deconstruct_keys must return Hash/) - end + it "in {**nil} only matches empty hashes" do + eval(<<~RUBY).should == true + case {} + in {**nil} + true + else + false + end + RUBY - it "does not match object if #deconstruct_keys method returns Hash with non-symbol keys" do - obj = Object.new - def obj.deconstruct_keys(*); {"a" => 1} end + eval(<<~RUBY).should == false + case {a: 1} + in {**nil} + true + else + false + end + RUBY + end - eval(<<~RUBY).should == false - case obj - in Object[a: 1] + it "matches anything with **" do + eval(<<~RUBY).should == true + case {a: 1} + in **; + true + end + RUBY + end + + it "can be used as a nested pattern" do + eval(<<~RUBY).should == true + case {a: {a: 1, b: 1}, b: {a: 1, b: 2}} + in {a: {a: 0}} + false + in {a: {a: 1}, b: {b: 1}} + false + in {a: {a: 1}, b: {b: 2}} true - else + end + RUBY + + eval(<<~RUBY).should == true + case [{a: 1, b: [1]}, {a: 1, c: ["2"]}] + in [{a:, c:},] + false + in [{a: 1, b:}, {a: 1, c: [Integer]}] false + in [_, {a: 1, c: [String]}] + true + end + RUBY + end + end + + describe "refinements" do + it "are used for #deconstruct" do + refinery = Module.new do + refine Array do + def deconstruct + [0] end - RUBY + end end - it "does not match object if elements of Hash returned by #deconstruct_keys method does not match values in pattern" do - obj = Object.new - def obj.deconstruct_keys(*); {a: 1} end + result = nil + Module.new do + using refinery - eval(<<~RUBY).should == false - case obj - in Object[a: 2] + result = eval(<<~RUBY) + case [] + in [0] true - else - false end RUBY end - it "passes keys specified in pattern as arguments to #deconstruct_keys method" do - obj = Object.new + result.should == true + end - def obj.deconstruct_keys(*args) - ScratchPad << args - {a: 1, b: 2, c: 3} + it "are used for #deconstruct_keys" do + refinery = Module.new do + refine Hash do + def deconstruct_keys(_) + {a: 0} + end end + end - eval <<~RUBY - case obj - in Object[a: 1, b: 2, c: 3] + result = nil + Module.new do + using refinery + + result = eval(<<~RUBY) + case {} + in a: 0 + true end RUBY - - ScratchPad.recorded.sort.should == [[[:a, :b, :c]]] end - it "passes keys specified in pattern to #deconstruct_keys method if pattern contains double splat operator **" do - obj = Object.new + result.should == true + end - def obj.deconstruct_keys(*args) - ScratchPad << args - {a: 1, b: 2, c: 3} + it "are used for #=== in constant pattern" do + refinery = Module.new do + refine Array.singleton_class do + def ===(obj) + obj.is_a?(Hash) + end end + end - eval <<~RUBY - case obj - in Object[a: 1, b: 2, **] + result = nil + Module.new do + using refinery + + result = eval(<<~RUBY) + case {} + in Array + true end RUBY - - ScratchPad.recorded.sort.should == [[[:a, :b]]] end - it "passes nil to #deconstruct_keys method if pattern contains double splat operator **rest" do - obj = Object.new - - def obj.deconstruct_keys(*args) - ScratchPad << args - {a: 1, b: 2} - end + result.should == true + end + end - eval <<~RUBY - case obj - in Object[a: 1, **rest] - end + describe "Ruby 3.1 improvements" do + ruby_version_is "3.1" do + it "can omit parentheses in one line pattern matching" do + eval(<<~RUBY).should == [1, 2] + [1, 2] => a, b + [a, b] RUBY - ScratchPad.recorded.should == [[nil]] + eval(<<~RUBY).should == 1 + {a: 1} => a: + a + RUBY end - it "binds variables" do - eval(<<~RUBY).should == [0, 1, 2] - case {a: 0, b: 1, c: 2} - in {a: x, b: y, c: z} - [x, y, z] + it "supports pinning instance variables" do + eval(<<~RUBY).should == true + @a = /a/ + case 'abc' + in ^@a + true end RUBY end - it "supports double splat operator **rest" do - eval(<<~RUBY).should == {b: 1, c: 2} - case {a: 0, b: 1, c: 2} - in {a: 0, **rest} - rest - end - RUBY + it "supports pinning class variables" do + result = nil + Module.new do + result = module_eval(<<~RUBY) + @@a = 0..10 + + case 2 + in ^@@a + true + end + RUBY + end + + result.should == true end - it "treats **nil like there should not be any other keys in a matched Hash" do + it "supports pinning global variables" do eval(<<~RUBY).should == true - case {a: 1, b: 2} - in {a: 1, b: 2, **nil} + $a = /a/ + case 'abc' + in ^$a true end RUBY + end - eval(<<~RUBY).should == false - case {a: 1, b: 2} - in {a: 1, **nil} + it "supports pinning expressions" do + eval(<<~RUBY).should == true + case 'abc' + in ^(/a/) true - else - false end RUBY - end - it "can match partially" do eval(<<~RUBY).should == true - case {a: 1, b: 2} - in {a: 1} + case 0 + in ^(0+0) true end RUBY end - it "matches {} with {}" do + it "supports pinning expressions in array pattern" do eval(<<~RUBY).should == true - case {} - in {} + case [3] + in [^(1+2)] true end RUBY end - it "matches anything with **" do + it "supports pinning expressions in hash pattern" do eval(<<~RUBY).should == true - case {a: 1} - in **; + case {name: '2.6', released_at: Time.new(2018, 12, 25)} + in {released_at: ^(Time.new(2010)..Time.new(2020))} true end RUBY end end + end - describe "refinements" do - it "are used for #deconstruct" do - refinery = Module.new do - refine Array do - def deconstruct - [0] - end - end - end - - result = nil - Module.new do - using refinery - - result = eval(<<~RUBY) - case [] - in [0] - true - end - RUBY - end - - result.should == true - end - - it "are used for #deconstruct_keys" do - refinery = Module.new do - refine Hash do - def deconstruct_keys(_) - {a: 0} - end - end - end + describe "value in pattern" do + it "returns true if the pattern matches" do + eval("1 in 1").should == true - result = nil - Module.new do - using refinery + eval("1 in Integer").should == true - result = eval(<<~RUBY) - case {} - in a: 0 - true - end - RUBY - end + e = nil + eval("[1, 2] in [1, e]").should == true + e.should == 2 - result.should == true - end + k = nil + eval("{k: 1} in {k:}").should == true + k.should == 1 + end - it "are used for #=== in constant pattern" do - refinery = Module.new do - refine Array.singleton_class do - def ===(obj) - obj.is_a?(Hash) - end - end - end + it "returns false if the pattern does not match" do + eval("1 in 2").should == false - result = nil - Module.new do - using refinery + eval("1 in Float").should == false - result = eval(<<~RUBY) - case {} - in Array - true - end - RUBY - end + eval("[1, 2] in [2, e]").should == false - result.should == true - end + eval("{k: 1} in {k: 2}").should == false end end end diff --git a/spec/ruby/language/precedence_spec.rb b/spec/ruby/language/precedence_spec.rb index 5a3c2861ce..c5adcca2c0 100644 --- a/spec/ruby/language/precedence_spec.rb +++ b/spec/ruby/language/precedence_spec.rb @@ -14,46 +14,44 @@ require_relative 'fixtures/precedence' # the level below (as well as showing associativity within # the precedence level). -=begin -Excerpted from 'Programming Ruby: The Pragmatic Programmer's Guide' -Second Edition by Dave Thomas, Chad Fowler, and Andy Hunt, page 324 - -Table 22.4. Ruby operators (high to low precedence) -Method Operator Description ------------------------------------------------------------------------ - :: . - x* [ ] [ ]= Element reference, element set - x ** Exponentiation - x ! ~ + - Not, complement, unary plus and minus - (method names for the last two are +@ and -@) - x * / % Multiply, divide, and modulo - x + - Plus and minus - x >> << Right and left shift - x & “And” (bitwise for integers) - x ^ | Exclusive “or” and regular “or” (bitwise for integers) - x <= < > >= Comparison operators - x <=> == === != =~ !~ Equality and pattern match operators (!= - and !~ may not be defined as methods) - && Logical “and” - || Logical “or” - .. ... Range (inclusive and exclusive) - ? : Ternary if-then-else - = %= /= -= += |= &= Assignment - >>= <<= *= &&= ||= **= - defined? Check if symbol defined - not Logical negation - or and Logical composition - if unless while until Expression modifiers - begin/end Block expression ------------------------------------------------------------------------ - -* Operators marked with 'x' in the Method column are implemented as methods -and can be overridden (except != and !~ as noted). (But see the specs -below for implementations that define != and !~ as methods.) - -** These are not included in the excerpted table but are shown here for -completeness. -=end +# Excerpted from 'Programming Ruby: The Pragmatic Programmer's Guide' +# Second Edition by Dave Thomas, Chad Fowler, and Andy Hunt, page 324 +# +# Table 22.4. Ruby operators (high to low precedence) +# Method Operator Description +# ----------------------------------------------------------------------- +# :: . +# x* [ ] [ ]= Element reference, element set +# x ** Exponentiation +# x ! ~ + - Not, complement, unary plus and minus +# (method names for the last two are +@ and -@) +# x * / % Multiply, divide, and modulo +# x + - Plus and minus +# x >> << Right and left shift +# x & “And” (bitwise for integers) +# x ^ | Exclusive “or” and regular “or” (bitwise for integers) +# x <= < > >= Comparison operators +# x <=> == === != =~ !~ Equality and pattern match operators (!= +# and !~ may not be defined as methods) +# && Logical “and” +# || Logical “or” +# .. ... Range (inclusive and exclusive) +# ? : Ternary if-then-else +# = %= /= -= += |= &= Assignment +# >>= <<= *= &&= ||= **= +# defined? Check if symbol defined +# not Logical negation +# or and Logical composition +# if unless while until Expression modifiers +# begin/end Block expression +# ----------------------------------------------------------------------- +# +# * Operators marked with 'x' in the Method column are implemented as methods +# and can be overridden (except != and !~ as noted). (But see the specs +# below for implementations that define != and !~ as methods.) +# +# ** These are not included in the excerpted table but are shown here for +# completeness. # ----------------------------------------------------------------------- # It seems that this table is not correct anymore diff --git a/spec/ruby/language/predefined_spec.rb b/spec/ruby/language/predefined_spec.rb index 24a8cf2f67..ac28f1e8a0 100644 --- a/spec/ruby/language/predefined_spec.rb +++ b/spec/ruby/language/predefined_spec.rb @@ -7,37 +7,33 @@ require 'stringio' # Entries marked [r/o] are read-only and an error will be raised of the program attempts to # modify them. Entries marked [thread] are thread local. -=begin -Exception Information ---------------------------------------------------------------------------------------------------- - -$! Exception The exception object passed to raise. [thread] -$@ Array The stack backtrace generated by the last exception. [thread] -=end - -=begin -Pattern Matching Variables ---------------------------------------------------------------------------------------------------- - -These variables are set to nil after an unsuccessful pattern match. - -$& String The string matched (following a successful pattern match). This variable is - local to the current scope. [r/o, thread] -$+ String The contents of the highest-numbered group matched following a successful - pattern match. Thus, in "cat" =~/(c|a)(t|z)/, $+ will be set to “t”. This - variable is local to the current scope. [r/o, thread] -$` String The string preceding the match in a successful pattern match. This variable - is local to the current scope. [r/o, thread] -$' String The string following the match in a successful pattern match. This variable - is local to the current scope. [r/o, thread] -$1 to $<N> String The contents of successive groups matched in a successful pattern match. In - "cat" =~/(c|a)(t|z)/, $1 will be set to “a” and $2 to “t”. This variable - is local to the current scope. [r/o, thread] -$~ MatchData An object that encapsulates the results of a successful pattern match. The - variables $&, $`, $', and $1 to $<N> are all derived from $~. Assigning to $~ - changes the values of these derived variables. This variable is local to the - current scope. [thread] -=end +# Exception Information +# --------------------------------------------------------------------------------------------------- +# +# $! Exception The exception object passed to raise. [thread] +# $@ Array The stack backtrace generated by the last exception. [thread] + +# Pattern Matching Variables +# --------------------------------------------------------------------------------------------------- +# +# These variables are set to nil after an unsuccessful pattern match. +# +# $& String The string matched (following a successful pattern match). This variable is +# local to the current scope. [r/o, thread] +# $+ String The contents of the highest-numbered group matched following a successful +# pattern match. Thus, in "cat" =~/(c|a)(t|z)/, $+ will be set to “t”. This +# variable is local to the current scope. [r/o, thread] +# $` String The string preceding the match in a successful pattern match. This variable +# is local to the current scope. [r/o, thread] +# $' String The string following the match in a successful pattern match. This variable +# is local to the current scope. [r/o, thread] +# $1 to $<N> String The contents of successive groups matched in a successful pattern match. In +# "cat" =~/(c|a)(t|z)/, $1 will be set to “a” and $2 to “t”. This variable +# is local to the current scope. [r/o, thread] +# $~ MatchData An object that encapsulates the results of a successful pattern match. The +# variables $&, $`, $', and $1 to $<N> are all derived from $~. Assigning to $~ +# changes the values of these derived variables. This variable is local to the +# current scope. [thread] describe "Predefined global $~" do @@ -137,7 +133,7 @@ describe "Predefined global $&" do end it "sets the encoding to the encoding of the source String" do - "abc".force_encoding(Encoding::EUC_JP) =~ /b/ + "abc".dup.force_encoding(Encoding::EUC_JP) =~ /b/ $&.encoding.should equal(Encoding::EUC_JP) end end @@ -150,12 +146,12 @@ describe "Predefined global $`" do end it "sets the encoding to the encoding of the source String" do - "abc".force_encoding(Encoding::EUC_JP) =~ /b/ + "abc".dup.force_encoding(Encoding::EUC_JP) =~ /b/ $`.encoding.should equal(Encoding::EUC_JP) end it "sets an empty result to the encoding of the source String" do - "abc".force_encoding(Encoding::ISO_8859_1) =~ /a/ + "abc".dup.force_encoding(Encoding::ISO_8859_1) =~ /a/ $`.encoding.should equal(Encoding::ISO_8859_1) end end @@ -168,12 +164,12 @@ describe "Predefined global $'" do end it "sets the encoding to the encoding of the source String" do - "abc".force_encoding(Encoding::EUC_JP) =~ /b/ + "abc".dup.force_encoding(Encoding::EUC_JP) =~ /b/ $'.encoding.should equal(Encoding::EUC_JP) end it "sets an empty result to the encoding of the source String" do - "abc".force_encoding(Encoding::ISO_8859_1) =~ /c/ + "abc".dup.force_encoding(Encoding::ISO_8859_1) =~ /c/ $'.encoding.should equal(Encoding::ISO_8859_1) end end @@ -191,7 +187,7 @@ describe "Predefined global $+" do end it "sets the encoding to the encoding of the source String" do - "abc".force_encoding(Encoding::EUC_JP) =~ /(b)/ + "abc".dup.force_encoding(Encoding::EUC_JP) =~ /(b)/ $+.encoding.should equal(Encoding::EUC_JP) end end @@ -218,7 +214,7 @@ describe "Predefined globals $1..N" do end it "sets the encoding to the encoding of the source String" do - "abc".force_encoding(Encoding::EUC_JP) =~ /(b)/ + "abc".dup.force_encoding(Encoding::EUC_JP) =~ /(b)/ $1.encoding.should equal(Encoding::EUC_JP) end end @@ -506,41 +502,39 @@ describe "Predefined global $!" do end end -=begin -Input/Output Variables ---------------------------------------------------------------------------------------------------- - -$/ String The input record separator (newline by default). This is the value that rou- - tines such as Kernel#gets use to determine record boundaries. If set to - nil, gets will read the entire file. -$-0 String Synonym for $/. -$\ String The string appended to the output of every call to methods such as - Kernel#print and IO#write. The default value is nil. -$, String The separator string output between the parameters to methods such as - Kernel#print and Array#join. Defaults to nil, which adds no text. -$. Integer The number of the last line read from the current input file. -$; String The default separator pattern used by String#split. May be set from the - command line using the -F flag. -$< Object An object that provides access to the concatenation of the contents of all - the files given as command-line arguments or $stdin (in the case where - there are no arguments). $< supports methods similar to a File object: - binmode, close, closed?, each, each_byte, each_line, eof, eof?, - file, filename, fileno, getc, gets, lineno, lineno=, path, pos, pos=, - read, readchar, readline, readlines, rewind, seek, skip, tell, to_a, - to_i, to_io, to_s, along with the methods in Enumerable. The method - file returns a File object for the file currently being read. This may change - as $< reads through the files on the command line. [r/o] -$> IO The destination of output for Kernel#print and Kernel#printf. The - default value is $stdout. -$_ String The last line read by Kernel#gets or Kernel#readline. Many string- - related functions in the Kernel module operate on $_ by default. The vari- - able is local to the current scope. [thread] -$-F String Synonym for $;. -$stderr IO The current standard error output. -$stdin IO The current standard input. -$stdout IO The current standard output. Assignment to $stdout is deprecated: use - $stdout.reopen instead. -=end +# Input/Output Variables +# --------------------------------------------------------------------------------------------------- +# +# $/ String The input record separator (newline by default). This is the value that rou- +# tines such as Kernel#gets use to determine record boundaries. If set to +# nil, gets will read the entire file. +# $-0 String Synonym for $/. +# $\ String The string appended to the output of every call to methods such as +# Kernel#print and IO#write. The default value is nil. +# $, String The separator string output between the parameters to methods such as +# Kernel#print and Array#join. Defaults to nil, which adds no text. +# $. Integer The number of the last line read from the current input file. +# $; String The default separator pattern used by String#split. May be set from the +# command line using the -F flag. +# $< Object An object that provides access to the concatenation of the contents of all +# the files given as command-line arguments or $stdin (in the case where +# there are no arguments). $< supports methods similar to a File object: +# binmode, close, closed?, each, each_byte, each_line, eof, eof?, +# file, filename, fileno, getc, gets, lineno, lineno=, path, pos, pos=, +# read, readchar, readline, readlines, rewind, seek, skip, tell, to_a, +# to_i, to_io, to_s, along with the methods in Enumerable. The method +# file returns a File object for the file currently being read. This may change +# as $< reads through the files on the command line. [r/o] +# $> IO The destination of output for Kernel#print and Kernel#printf. The +# default value is $stdout. +# $_ String The last line read by Kernel#gets or Kernel#readline. Many string- +# related functions in the Kernel module operate on $_ by default. The vari- +# able is local to the current scope. [thread] +# $-F String Synonym for $;. +# $stderr IO The current standard error output. +# $stdin IO The current standard input. +# $stdout IO The current standard output. Assignment to $stdout is deprecated: use +# $stdout.reopen instead. describe "Predefined global $/" do before :each do @@ -570,7 +564,6 @@ describe "Predefined global $/" do ($/ = "xyz").should == "xyz" end - it "changes $-0" do $/ = "xyz" $-0.should equal($/) @@ -641,6 +634,45 @@ describe "Predefined global $-0" do end end +describe "Predefined global $\\" do + before :each do + @verbose, $VERBOSE = $VERBOSE, nil + @dollar_backslash = $\ + end + + after :each do + $\ = @dollar_backslash + $VERBOSE = @verbose + end + + it "can be assigned a String" do + str = "abc" + $\ = str + $\.should equal(str) + end + + it "can be assigned nil" do + $\ = nil + $\.should be_nil + end + + it "returns the value assigned" do + ($\ = "xyz").should == "xyz" + end + + it "does not call #to_str to convert the object to a String" do + obj = mock("$\\ value") + obj.should_not_receive(:to_str) + + -> { $\ = obj }.should raise_error(TypeError) + end + + it "raises a TypeError if assigned not String" do + -> { $\ = 1 }.should raise_error(TypeError) + -> { $\ = true }.should raise_error(TypeError) + end +end + describe "Predefined global $," do after :each do $, = nil @@ -654,10 +686,8 @@ describe "Predefined global $," do -> { $, = Object.new }.should raise_error(TypeError) end - ruby_version_is "2.7" do - it "warns if assigned non-nil" do - -> { $, = "_" }.should complain(/warning: `\$,' is deprecated/) - end + it "warns if assigned non-nil" do + -> { $, = "_" }.should complain(/warning: [`']\$,' is deprecated/) end end @@ -693,10 +723,8 @@ describe "Predefined global $;" do $; = nil end - ruby_version_is "2.7" do - it "warns if assigned non-nil" do - -> { $; = "_" }.should complain(/warning: `\$;' is deprecated/) - end + it "warns if assigned non-nil" do + -> { $; = "_" }.should complain(/warning: [`']\$;' is deprecated/) end end @@ -769,54 +797,52 @@ describe "Predefined global $_" do end end -=begin -Execution Environment Variables ---------------------------------------------------------------------------------------------------- - -$0 String The name of the top-level Ruby program being executed. Typically this will - be the program’s filename. On some operating systems, assigning to this - variable will change the name of the process reported (for example) by the - ps(1) command. -$* Array An array of strings containing the command-line options from the invoca- - tion of the program. Options used by the Ruby interpreter will have been - removed. [r/o] -$" Array An array containing the filenames of modules loaded by require. [r/o] -$$ Integer The process number of the program being executed. [r/o] -$? Process::Status The exit status of the last child process to terminate. [r/o, thread] -$: Array An array of strings, where each string specifies a directory to be searched for - Ruby scripts and binary extensions used by the load and require methods. - The initial value is the value of the arguments passed via the -I command- - line option, followed by an installation-defined standard library location, fol- - lowed by the current directory (“.”). This variable may be set from within a - program to alter the default search path; typically, programs use $: << dir - to append dir to the path. [r/o] -$-a Object True if the -a option is specified on the command line. [r/o] -$-d Object Synonym for $DEBUG. -$DEBUG Object Set to true if the -d command-line option is specified. -__FILE__ String The name of the current source file. [r/o] -$F Array The array that receives the split input line if the -a command-line option is - used. -$FILENAME String The name of the current input file. Equivalent to $<.filename. [r/o] -$-i String If in-place edit mode is enabled (perhaps using the -i command-line - option), $-i holds the extension used when creating the backup file. If you - set a value into $-i, enables in-place edit mode. -$-I Array Synonym for $:. [r/o] -$-K String Sets the multibyte coding system for strings and regular expressions. Equiv- - alent to the -K command-line option. -$-l Object Set to true if the -l option (which enables line-end processing) is present - on the command line. [r/o] -__LINE__ String The current line number in the source file. [r/o] -$LOAD_PATH Array A synonym for $:. [r/o] -$-p Object Set to true if the -p option (which puts an implicit while gets . . . end - loop around your program) is present on the command line. [r/o] -$VERBOSE Object Set to true if the -v, --version, -W, or -w option is specified on the com- - mand line. Set to false if no option, or -W1 is given. Set to nil if -W0 - was specified. Setting this option to true causes the interpreter and some - library routines to report additional information. Setting to nil suppresses - all warnings (including the output of Kernel.warn). -$-v Object Synonym for $VERBOSE. -$-w Object Synonym for $VERBOSE. -=end +# Execution Environment Variables +# --------------------------------------------------------------------------------------------------- +# +# $0 String The name of the top-level Ruby program being executed. Typically this will +# be the program’s filename. On some operating systems, assigning to this +# variable will change the name of the process reported (for example) by the +# ps(1) command. +# $* Array An array of strings containing the command-line options from the invoca- +# tion of the program. Options used by the Ruby interpreter will have been +# removed. [r/o] +# $" Array An array containing the filenames of modules loaded by require. [r/o] +# $$ Integer The process number of the program being executed. [r/o] +# $? Process::Status The exit status of the last child process to terminate. [r/o, thread] +# $: Array An array of strings, where each string specifies a directory to be searched for +# Ruby scripts and binary extensions used by the load and require methods. +# The initial value is the value of the arguments passed via the -I command- +# line option, followed by an installation-defined standard library location, fol- +# lowed by the current directory (“.”). This variable may be set from within a +# program to alter the default search path; typically, programs use $: << dir +# to append dir to the path. [r/o] +# $-a Object True if the -a option is specified on the command line. [r/o] +# $-d Object Synonym for $DEBUG. +# $DEBUG Object Set to true if the -d command-line option is specified. +# __FILE__ String The name of the current source file. [r/o] +# $F Array The array that receives the split input line if the -a command-line option is +# used. +# $FILENAME String The name of the current input file. Equivalent to $<.filename. [r/o] +# $-i String If in-place edit mode is enabled (perhaps using the -i command-line +# option), $-i holds the extension used when creating the backup file. If you +# set a value into $-i, enables in-place edit mode. +# $-I Array Synonym for $:. [r/o] +# $-K String Sets the multibyte coding system for strings and regular expressions. Equiv- +# alent to the -K command-line option. +# $-l Object Set to true if the -l option (which enables line-end processing) is present +# on the command line. [r/o] +# __LINE__ String The current line number in the source file. [r/o] +# $LOAD_PATH Array A synonym for $:. [r/o] +# $-p Object Set to true if the -p option (which puts an implicit while gets . . . end +# loop around your program) is present on the command line. [r/o] +# $VERBOSE Object Set to true if the -v, --version, -W, or -w option is specified on the com- +# mand line. Set to false if no option, or -W1 is given. Set to nil if -W0 +# was specified. Setting this option to true causes the interpreter and some +# library routines to report additional information. Setting to nil suppresses +# all warnings (including the output of Kernel.warn). +# $-v Object Synonym for $VERBOSE. +# $-w Object Synonym for $VERBOSE. describe "Execution variable $:" do it "is initialized to an array of strings" do $:.is_a?(Array).should == true @@ -835,6 +861,8 @@ describe "Execution variable $:" do it "can be changed via <<" do $: << "foo" $:.should include("foo") + ensure + $:.delete("foo") end it "is read-only" do @@ -850,6 +878,16 @@ describe "Execution variable $:" do $-I = [] }.should raise_error(NameError) end + + it "default $LOAD_PATH entries until sitelibdir included have @gem_prelude_index set" do + skip "no sense in ruby itself" if MSpecScript.instance_variable_defined?(:@testing_ruby) + + $:.should.include?(RbConfig::CONFIG['sitelibdir']) + idx = $:.index(RbConfig::CONFIG['sitelibdir']) + + $:[idx..-1].all? { |p| p.instance_variable_defined?(:@gem_prelude_index) }.should be_true + $:[0...idx].all? { |p| !p.instance_variable_defined?(:@gem_prelude_index) }.should be_true + end end describe "Global variable $\"" do @@ -941,6 +979,10 @@ describe "Global variable $VERBOSE" do $VERBOSE = @verbose end + it "is false by default" do + $VERBOSE.should be_false + end + it "converts truthy values to true" do [true, 1, 0, [], ""].each do |true_value| $VERBOSE = true_value @@ -995,7 +1037,7 @@ describe "Global variable $0" do it "is the path given as the main script and the same as __FILE__" do script = "fixtures/dollar_zero.rb" - Dir.chdir(File.dirname(__FILE__)) do + Dir.chdir(__dir__) do ruby_exe(script).should == "#{script}\n#{script}\nOK" end end @@ -1022,22 +1064,20 @@ describe "Global variable $0" do end end -=begin -Standard Objects ---------------------------------------------------------------------------------------------------- - -ARGF Object A synonym for $<. -ARGV Array A synonym for $*. -ENV Object A hash-like object containing the program’s environment variables. An - instance of class Object, ENV implements the full set of Hash methods. Used - to query and set the value of an environment variable, as in ENV["PATH"] - and ENV["term"]="ansi". -false FalseClass Singleton instance of class FalseClass. [r/o] -nil NilClass The singleton instance of class NilClass. The value of uninitialized - instance and global variables. [r/o] -self Object The receiver (object) of the current method. [r/o] -true TrueClass Singleton instance of class TrueClass. [r/o] -=end +# Standard Objects +# --------------------------------------------------------------------------------------------------- +# +# ARGF Object A synonym for $<. +# ARGV Array A synonym for $*. +# ENV Object A hash-like object containing the program’s environment variables. An +# instance of class Object, ENV implements the full set of Hash methods. Used +# to query and set the value of an environment variable, as in ENV["PATH"] +# and ENV["term"]="ansi". +# false FalseClass Singleton instance of class FalseClass. [r/o] +# nil NilClass The singleton instance of class NilClass. The value of uninitialized +# instance and global variables. [r/o] +# self Object The receiver (object) of the current method. [r/o] +# true TrueClass Singleton instance of class TrueClass. [r/o] describe "The predefined standard objects" do it "includes ARGF" do @@ -1090,35 +1130,54 @@ describe "The self pseudo-variable" do end end -=begin -Global Constants ---------------------------------------------------------------------------------------------------- - -The following constants are defined by the Ruby interpreter. - -DATA IO If the main program file contains the directive __END__, then - the constant DATA will be initialized so that reading from it will - return lines following __END__ from the source file. -RUBY_PLATFORM String The identifier of the platform running this program. This string - is in the same form as the platform identifier used by the GNU - configure utility (which is not a coincidence). -RUBY_RELEASE_DATE String The date of this release. -RUBY_VERSION String The version number of the interpreter. -STDERR IO The actual standard error stream for the program. The initial - value of $stderr. -STDIN IO The actual standard input stream for the program. The initial - value of $stdin. -STDOUT IO The actual standard output stream for the program. The initial - value of $stdout. -SCRIPT_LINES__ Hash If a constant SCRIPT_LINES__ is defined and references a Hash, - Ruby will store an entry containing the contents of each file it - parses, with the file’s name as the key and an array of strings as - the value. -TOPLEVEL_BINDING Binding A Binding object representing the binding at Ruby’s top level— - the level where programs are initially executed. -=end +# Global Constants +# --------------------------------------------------------------------------------------------------- +# +# The following constants are defined by the Ruby interpreter. +# +# DATA IO If the main program file contains the directive __END__, then +# the constant DATA will be initialized so that reading from it will +# return lines following __END__ from the source file. +# FALSE FalseClass Synonym for false (deprecated, removed in Ruby 3). +# NIL NilClass Synonym for nil (deprecated, removed in Ruby 3). +# RUBY_PLATFORM String The identifier of the platform running this program. This string +# is in the same form as the platform identifier used by the GNU +# configure utility (which is not a coincidence). +# RUBY_RELEASE_DATE String The date of this release. +# RUBY_VERSION String The version number of the interpreter. +# STDERR IO The actual standard error stream for the program. The initial +# value of $stderr. +# STDIN IO The actual standard input stream for the program. The initial +# value of $stdin. +# STDOUT IO The actual standard output stream for the program. The initial +# value of $stdout. +# SCRIPT_LINES__ Hash If a constant SCRIPT_LINES__ is defined and references a Hash, +# Ruby will store an entry containing the contents of each file it +# parses, with the file’s name as the key and an array of strings as +# the value. +# TOPLEVEL_BINDING Binding A Binding object representing the binding at Ruby’s top level— +# the level where programs are initially executed. +# TRUE TrueClass Synonym for true (deprecated, removed in Ruby 3). describe "The predefined global constants" do + describe "TRUE" do + it "is no longer defined" do + Object.const_defined?(:TRUE).should == false + end + end + + describe "FALSE" do + it "is no longer defined" do + Object.const_defined?(:FALSE).should == false + end + end + + describe "NIL" do + it "is no longer defined" do + Object.const_defined?(:NIL).should == false + end + end + it "includes STDIN" do Object.const_defined?(:STDIN).should == true end @@ -1146,7 +1205,6 @@ describe "The predefined global constants" do it "includes TOPLEVEL_BINDING" do Object.const_defined?(:TOPLEVEL_BINDING).should == true end - end describe "The predefined global constant" do @@ -1161,13 +1219,24 @@ describe "The predefined global constant" do end describe "STDIN" do - it "has the same external encoding as Encoding.default_external" do - STDIN.external_encoding.should equal(Encoding.default_external) - end + platform_is_not :windows do + it "has the same external encoding as Encoding.default_external" do + STDIN.external_encoding.should equal(Encoding.default_external) + end + + it "has the same external encoding as Encoding.default_external when that encoding is changed" do + Encoding.default_external = Encoding::ISO_8859_16 + STDIN.external_encoding.should equal(Encoding::ISO_8859_16) + end - it "has the same external encoding as Encoding.default_external when that encoding is changed" do - Encoding.default_external = Encoding::ISO_8859_16 - STDIN.external_encoding.should equal(Encoding::ISO_8859_16) + it "has nil for the internal encoding" do + STDIN.internal_encoding.should be_nil + end + + it "has nil for the internal encoding despite Encoding.default_internal being changed" do + Encoding.default_internal = Encoding::IBM437 + STDIN.internal_encoding.should be_nil + end end it "has the encodings set by #set_encoding" do @@ -1182,15 +1251,6 @@ describe "The predefined global constant" do "p [STDIN.external_encoding.name, STDIN.internal_encoding.name]" ruby_exe(code).chomp.should == %{["IBM775", "IBM866"]} end - - it "has nil for the internal encoding" do - STDIN.internal_encoding.should be_nil - end - - it "has nil for the internal encoding despite Encoding.default_internal being changed" do - Encoding.default_internal = Encoding::IBM437 - STDIN.internal_encoding.should be_nil - end end describe "STDOUT" do @@ -1255,32 +1315,57 @@ describe "The predefined global constant" do end end -ruby_version_is "2.7" do - describe "$LOAD_PATH.resolve_feature_path" do - it "returns what will be loaded without actual loading, .rb file" do - extension, path = $LOAD_PATH.resolve_feature_path('set') - extension.should == :rb - path.should.end_with?('/set.rb') - end +describe "$LOAD_PATH.resolve_feature_path" do + it "returns what will be loaded without actual loading, .rb file" do + extension, path = $LOAD_PATH.resolve_feature_path('set') + extension.should == :rb + path.should.end_with?('/set.rb') + end - it "returns what will be loaded without actual loading, .so file" do - require 'rbconfig' + it "returns what will be loaded without actual loading, .so file" do + require 'rbconfig' + skip "no dynamically loadable standard extension" if RbConfig::CONFIG["EXTSTATIC"] == "static" - extension, path = $LOAD_PATH.resolve_feature_path('etc') - extension.should == :so - path.should.end_with?("/etc.#{RbConfig::CONFIG['DLEXT']}") - end + extension, path = $LOAD_PATH.resolve_feature_path('etc') + extension.should == :so + path.should.end_with?("/etc.#{RbConfig::CONFIG['DLEXT']}") + end - ruby_version_is "2.7"..."3.1" do - it "raises LoadError if feature cannot be found" do - -> { $LOAD_PATH.resolve_feature_path('noop') }.should raise_error(LoadError) - end + ruby_version_is ""..."3.1" do + it "raises LoadError if feature cannot be found" do + -> { $LOAD_PATH.resolve_feature_path('noop') }.should raise_error(LoadError) end + end - ruby_version_is "3.1" do - it "return nil if feature cannot be found" do - $LOAD_PATH.resolve_feature_path('noop').should be_nil - end + ruby_version_is "3.1" do + it "return nil if feature cannot be found" do + $LOAD_PATH.resolve_feature_path('noop').should be_nil end end end + +# Some other pre-defined global variables + +describe "Predefined global $=" do + before :each do + @verbose, $VERBOSE = $VERBOSE, nil + @dollar_assign = $= + end + + after :each do + $= = @dollar_assign + $VERBOSE = @verbose + end + + it "warns when accessed" do + -> { a = $= }.should complain(/is no longer effective/) + end + + it "warns when assigned" do + -> { $= = "_" }.should complain(/is no longer effective/) + end + + it "returns the value assigned" do + ($= = "xyz").should == "xyz" + end +end diff --git a/spec/ruby/language/proc_spec.rb b/spec/ruby/language/proc_spec.rb index c44e711d2b..cc69b7799c 100644 --- a/spec/ruby/language/proc_spec.rb +++ b/spec/ruby/language/proc_spec.rb @@ -161,6 +161,18 @@ describe "A Proc" do end end + describe "taking |*a, b| arguments" do + it "assigns [] to the argument when passed no values" do + proc { |*a, b| [a, b] }.call.should == [[], nil] + end + end + + describe "taking |a, *b, c| arguments" do + it "assigns [] to the argument when passed no values" do + proc { |a, *b, c| [a, b, c] }.call.should == [nil, [], nil] + end + end + describe "taking |a, | arguments" do before :each do @l = lambda { |a, | a } @@ -217,4 +229,21 @@ describe "A Proc" do lambda { @l.call(obj) }.should raise_error(TypeError) end end + + describe "taking |*a, **kw| arguments" do + before :each do + @p = proc { |*a, **kw| [a, kw] } + end + + it 'does not autosplat keyword arguments' do + @p.call([1, {a: 1}]).should == [[[1, {a: 1}]], {}] + end + end + + describe "taking |required keyword arguments, **kw| arguments" do + it "raises ArgumentError for missing required argument" do + p = proc { |a:, **kw| [a, kw] } + -> { p.call() }.should raise_error(ArgumentError) + end + end end diff --git a/spec/ruby/language/range_spec.rb b/spec/ruby/language/range_spec.rb index c0f90f84d6..ccc9f55537 100644 --- a/spec/ruby/language/range_spec.rb +++ b/spec/ruby/language/range_spec.rb @@ -10,17 +10,21 @@ describe "Literal Ranges" do (1...10).should == Range.new(1, 10, true) end - ruby_version_is "2.6" do - it "creates endless ranges" do - eval("(1..)").should == Range.new(1, nil) - eval("(1...)").should == Range.new(1, nil, true) + it "creates a simple range as an object literal" do + ary = [] + 2.times do + ary.push(1..3) end + ary[0].should.equal?(ary[1]) end - ruby_version_is "2.7" do - it "creates beginless ranges" do - eval("(..1)").should == Range.new(nil, 1) - eval("(...1)").should == Range.new(nil, 1, true) - end + it "creates endless ranges" do + (1..).should == Range.new(1, nil) + (1...).should == Range.new(1, nil, true) + end + + it "creates beginless ranges" do + (..1).should == Range.new(nil, 1) + (...1).should == Range.new(nil, 1, true) end end diff --git a/spec/ruby/language/regexp/character_classes_spec.rb b/spec/ruby/language/regexp/character_classes_spec.rb index 0cf1e9b6f4..98d431a817 100644 --- a/spec/ruby/language/regexp/character_classes_spec.rb +++ b/spec/ruby/language/regexp/character_classes_spec.rb @@ -609,10 +609,13 @@ describe "Regexp with character classes" do "루비(Ruby)".match(/\p{Hangul}+/u).to_a.should == ["루비"] end - ruby_bug "#17340", ''...'3.0' do - it "raises a RegexpError for an unterminated unicode property" do - -> { Regexp.new('\p{') }.should raise_error(RegexpError) - end + it "supports negated property condition" do + "a".match(eval("/\P{L}/")).should be_nil + "1".match(eval("/\P{N}/")).should be_nil + end + + it "raises a RegexpError for an unterminated unicode property" do + -> { Regexp.new('\p{') }.should raise_error(RegexpError) end it "supports \\X (unicode 9.0 with UTR #51 workarounds)" do diff --git a/spec/ruby/language/regexp/encoding_spec.rb b/spec/ruby/language/regexp/encoding_spec.rb index 8e2a294b95..0571b2d3cf 100644 --- a/spec/ruby/language/regexp/encoding_spec.rb +++ b/spec/ruby/language/regexp/encoding_spec.rb @@ -4,18 +4,18 @@ require_relative '../fixtures/classes' describe "Regexps with encoding modifiers" do it "supports /e (EUC encoding)" do - match = /./e.match("\303\251".force_encoding(Encoding::EUC_JP)) - match.to_a.should == ["\303\251".force_encoding(Encoding::EUC_JP)] + match = /./e.match("\303\251".dup.force_encoding(Encoding::EUC_JP)) + match.to_a.should == ["\303\251".dup.force_encoding(Encoding::EUC_JP)] end it "supports /e (EUC encoding) with interpolation" do - match = /#{/./}/e.match("\303\251".force_encoding(Encoding::EUC_JP)) - match.to_a.should == ["\303\251".force_encoding(Encoding::EUC_JP)] + match = /#{/./}/e.match("\303\251".dup.force_encoding(Encoding::EUC_JP)) + match.to_a.should == ["\303\251".dup.force_encoding(Encoding::EUC_JP)] end it "supports /e (EUC encoding) with interpolation /o" do - match = /#{/./}/e.match("\303\251".force_encoding(Encoding::EUC_JP)) - match.to_a.should == ["\303\251".force_encoding(Encoding::EUC_JP)] + match = /#{/./}/e.match("\303\251".dup.force_encoding(Encoding::EUC_JP)) + match.to_a.should == ["\303\251".dup.force_encoding(Encoding::EUC_JP)] end it 'uses EUC-JP as /e encoding' do @@ -38,6 +38,10 @@ describe "Regexps with encoding modifiers" do /#{/./}/n.match("\303\251").to_a.should == ["\303"] end + it "warns when using /n with a match string with non-ASCII characters and an encoding other than ASCII-8BIT" do + -> { /./n.match("\303\251".dup.force_encoding('utf-8')) }.should complain(%r{historical binary regexp match /.../n against UTF-8 string}) + end + it 'uses US-ASCII as /n encoding if all chars are 7-bit' do /./n.encoding.should == Encoding::US_ASCII end @@ -59,18 +63,18 @@ describe "Regexps with encoding modifiers" do end it "supports /s (Windows_31J encoding)" do - match = /./s.match("\303\251".force_encoding(Encoding::Windows_31J)) - match.to_a.should == ["\303".force_encoding(Encoding::Windows_31J)] + match = /./s.match("\303\251".dup.force_encoding(Encoding::Windows_31J)) + match.to_a.should == ["\303".dup.force_encoding(Encoding::Windows_31J)] end it "supports /s (Windows_31J encoding) with interpolation" do - match = /#{/./}/s.match("\303\251".force_encoding(Encoding::Windows_31J)) - match.to_a.should == ["\303".force_encoding(Encoding::Windows_31J)] + match = /#{/./}/s.match("\303\251".dup.force_encoding(Encoding::Windows_31J)) + match.to_a.should == ["\303".dup.force_encoding(Encoding::Windows_31J)] end it "supports /s (Windows_31J encoding) with interpolation and /o" do - match = /#{/./}/s.match("\303\251".force_encoding(Encoding::Windows_31J)) - match.to_a.should == ["\303".force_encoding(Encoding::Windows_31J)] + match = /#{/./}/s.match("\303\251".dup.force_encoding(Encoding::Windows_31J)) + match.to_a.should == ["\303".dup.force_encoding(Encoding::Windows_31J)] end it 'uses Windows-31J as /s encoding' do @@ -82,15 +86,15 @@ describe "Regexps with encoding modifiers" do end it "supports /u (UTF8 encoding)" do - /./u.match("\303\251".force_encoding('utf-8')).to_a.should == ["\u{e9}"] + /./u.match("\303\251".dup.force_encoding('utf-8')).to_a.should == ["\u{e9}"] end it "supports /u (UTF8 encoding) with interpolation" do - /#{/./}/u.match("\303\251".force_encoding('utf-8')).to_a.should == ["\u{e9}"] + /#{/./}/u.match("\303\251".dup.force_encoding('utf-8')).to_a.should == ["\u{e9}"] end it "supports /u (UTF8 encoding) with interpolation and /o" do - /#{/./}/u.match("\303\251".force_encoding('utf-8')).to_a.should == ["\u{e9}"] + /#{/./}/u.match("\303\251".dup.force_encoding('utf-8')).to_a.should == ["\u{e9}"] end it 'uses UTF-8 as /u encoding' do @@ -117,14 +121,27 @@ describe "Regexps with encoding modifiers" do -> { /\A[[:space:]]*\z/ =~ " ".encode("UTF-16LE") }.should raise_error(Encoding::CompatibilityError) end + it "raises Encoding::CompatibilityError when the regexp has a fixed, non-ASCII-compatible encoding" do + -> { Regexp.new("".dup.force_encoding("UTF-16LE"), Regexp::FIXEDENCODING) =~ " ".encode("UTF-8") }.should raise_error(Encoding::CompatibilityError) + end + + it "raises Encoding::CompatibilityError when the regexp has a fixed encoding and the match string has non-ASCII characters" do + -> { Regexp.new("".dup.force_encoding("US-ASCII"), Regexp::FIXEDENCODING) =~ "\303\251".dup.force_encoding('UTF-8') }.should raise_error(Encoding::CompatibilityError) + end + + it "raises ArgumentError when trying to match a broken String" do + s = "\x80".dup.force_encoding('UTF-8') + -> { s =~ /./ }.should raise_error(ArgumentError, "invalid byte sequence in UTF-8") + end + it "computes the Regexp Encoding for each interpolated Regexp instance" do make_regexp = -> str { /#{str}/ } - r = make_regexp.call("été".force_encoding(Encoding::UTF_8)) + r = make_regexp.call("été".dup.force_encoding(Encoding::UTF_8)) r.should.fixed_encoding? r.encoding.should == Encoding::UTF_8 - r = make_regexp.call("abc".force_encoding(Encoding::UTF_8)) + r = make_regexp.call("abc".dup.force_encoding(Encoding::UTF_8)) r.should_not.fixed_encoding? r.encoding.should == Encoding::US_ASCII end diff --git a/spec/ruby/language/regexp/escapes_spec.rb b/spec/ruby/language/regexp/escapes_spec.rb index 2e5fe5ad2e..16a4d8c23b 100644 --- a/spec/ruby/language/regexp/escapes_spec.rb +++ b/spec/ruby/language/regexp/escapes_spec.rb @@ -2,8 +2,10 @@ require_relative '../../spec_helper' require_relative '../fixtures/classes' +# TODO: synchronize with spec/core/regexp/new_spec.rb - +# escaping is also tested there describe "Regexps with escape characters" do - it "they're supported" do + it "supports escape sequences" do /\t/.match("\t").to_a.should == ["\t"] # horizontal tab /\v/.match("\v").to_a.should == ["\v"] # vertical tab /\n/.match("\n").to_a.should == ["\n"] # newline @@ -15,9 +17,7 @@ describe "Regexps with escape characters" do # \nnn octal char (encoded byte value) end - it "support quoting meta-characters via escape sequence" do - /\\/.match("\\").to_a.should == ["\\"] - /\//.match("/").to_a.should == ["/"] + it "supports quoting meta-characters via escape sequence" do # parenthesis, etc /\(/.match("(").to_a.should == ["("] /\)/.match(")").to_a.should == [")"] @@ -25,6 +25,8 @@ describe "Regexps with escape characters" do /\]/.match("]").to_a.should == ["]"] /\{/.match("{").to_a.should == ["{"] /\}/.match("}").to_a.should == ["}"] + /\</.match("<").to_a.should == ["<"] + /\>/.match(">").to_a.should == [">"] # alternation separator /\|/.match("|").to_a.should == ["|"] # quantifiers @@ -37,11 +39,81 @@ describe "Regexps with escape characters" do /\$/.match("$").to_a.should == ["$"] end + it "supports quoting meta-characters via escape sequence when used as a terminator" do + # parenthesis, etc + # %r[[, %r((, etc literals - are forbidden + %r(\().match("(").to_a.should == ["("] + %r(\)).match(")").to_a.should == [")"] + %r)\().match("(").to_a.should == ["("] + %r)\)).match(")").to_a.should == [")"] + + %r[\[].match("[").to_a.should == ["["] + %r[\]].match("]").to_a.should == ["]"] + %r]\[].match("[").to_a.should == ["["] + %r]\]].match("]").to_a.should == ["]"] + + %r{\{}.match("{").to_a.should == ["{"] + %r{\}}.match("}").to_a.should == ["}"] + %r}\{}.match("{").to_a.should == ["{"] + %r}\}}.match("}").to_a.should == ["}"] + + %r<\<>.match("<").to_a.should == ["<"] + %r<\>>.match(">").to_a.should == [">"] + %r>\<>.match("<").to_a.should == ["<"] + %r>\>>.match(">").to_a.should == [">"] + + # alternation separator + %r|\||.match("|").to_a.should == ["|"] + # quantifiers + %r?\??.match("?").to_a.should == ["?"] + %r.\...match(".").to_a.should == ["."] + %r*\**.match("*").to_a.should == ["*"] + %r+\++.match("+").to_a.should == ["+"] + # line anchors + %r^\^^.match("^").to_a.should == ["^"] + %r$\$$.match("$").to_a.should == ["$"] + end + + it "supports quoting non-meta-characters via escape sequence when used as a terminator" do + non_meta_character_terminators = [ + '!', '"', '#', '%', '&', "'", ',', '-', ':', ';', '@', '_', '`', '/', '=', '~' + ] + + non_meta_character_terminators.each do |c| + pattern = eval("%r" + c + "\\" + c + c) + pattern.match(c).to_a.should == [c] + end + end + + it "does not change semantics of escaped non-meta-character when used as a terminator" do + all_terminators = [*("!".."/"), *(":".."@"), *("[".."`"), *("{".."~")] + meta_character_terminators = ["$", "^", "*", "+", ".", "?", "|", "}", ")", ">", "]"] + special_cases = ['(', '{', '[', '<', '\\'] + + # it should be equivalent to + # [ '!', '"', '#', '%', '&', "'", ',', '-', ':', ';', '@', '_', '`', '/', '=', '~' ] + non_meta_character_terminators = all_terminators - meta_character_terminators - special_cases + + non_meta_character_terminators.each do |c| + pattern = eval("%r" + c + "\\" + c + c) + pattern.should == /#{c}/ + end + end + + it "does not change semantics of escaped meta-character when used as a terminator" do + meta_character_terminators = ["$", "^", "*", "+", ".", "?", "|", "}", ")", ">", "]"] + + meta_character_terminators.each do |c| + pattern = eval("%r" + c + "\\" + c + c) + pattern.should == eval("/\\#{c}/") + end + end + it "allows any character to be escaped" do /\y/.match("y").to_a.should == ["y"] end - it "support \\x (hex characters)" do + it "supports \\x (hex characters)" do /\xA/.match("\nxyz").to_a.should == ["\n"] /\x0A/.match("\n").to_a.should == ["\n"] /\xAA/.match("\nA").should be_nil @@ -53,7 +125,7 @@ describe "Regexps with escape characters" do # \x{7HHHHHHH} wide hexadecimal char (character code point value) end - it "support \\c (control characters)" do + it "supports \\c (control characters)" do #/\c \c@\c`/.match("\00\00\00").to_a.should == ["\00\00\00"] /\c#\cc\cC/.match("\03\03\03").to_a.should == ["\03\03\03"] /\c'\cG\cg/.match("\a\a\a").to_a.should == ["\a\a\a"] diff --git a/spec/ruby/language/regexp/grouping_spec.rb b/spec/ruby/language/regexp/grouping_spec.rb index e2abe71d0d..2f04a04018 100644 --- a/spec/ruby/language/regexp/grouping_spec.rb +++ b/spec/ruby/language/regexp/grouping_spec.rb @@ -32,4 +32,32 @@ describe "Regexps with grouping" do b # there is no capture group on this line (not even here) $/x.match("ab").to_a.should == [ "ab", "a" ] end + + it "does not consider # inside a character class as a comment" do + # From https://github.com/rubocop/rubocop/blob/39fcf1c568/lib/rubocop/cop/utils/format_string.rb#L18 + regexp = / + % (?<type>%) # line comment + | % (?<flags>(?-mix:[ #0+-]|(?-mix:(\d+)\$))*) (?#group comment) + (?: + (?: (?-mix:(?<width>(?-mix:\d+|(?-mix:\*(?-mix:(\d+)\$)?))))? (?-mix:\.(?<precision>(?-mix:\d+|(?-mix:\*(?-mix:(\d+)\$)?))))? (?-mix:<(?<name>\w+)>)? + | (?-mix:(?<width>(?-mix:\d+|(?-mix:\*(?-mix:(\d+)\$)?))))? (?-mix:<(?<name>\w+)>) (?-mix:\.(?<precision>(?-mix:\d+|(?-mix:\*(?-mix:(\d+)\$)?))))? + | (?-mix:<(?<name>\w+)>) (?<more_flags>(?-mix:[ #0+-]|(?-mix:(\d+)\$))*) (?-mix:(?<width>(?-mix:\d+|(?-mix:\*(?-mix:(\d+)\$)?))))? (?-mix:\.(?<precision>(?-mix:\d+|(?-mix:\*(?-mix:(\d+)\$)?))))? + ) (?-mix:(?<type>[bBdiouxXeEfgGaAcps])) + | (?-mix:(?<width>(?-mix:\d+|(?-mix:\*(?-mix:(\d+)\$)?))))? (?-mix:\.(?<precision>(?-mix:\d+|(?-mix:\*(?-mix:(\d+)\$)?))))? (?-mix:\{(?<name>\w+)\}) + ) + /x + regexp.named_captures.should == { + "type" => [1, 13], + "flags" => [2], + "width" => [3, 6, 11, 14], + "precision" => [4, 8, 12, 15], + "name" => [5, 7, 9, 16], + "more_flags" => [10] + } + match = regexp.match("%6.3f") + match[:width].should == '6' + match[:precision].should == '3' + match[:type].should == 'f' + match.to_a.should == [ "%6.3f", nil, "", "6", "3"] + [nil] * 8 + ["f"] + [nil] * 3 + end end diff --git a/spec/ruby/language/regexp/repetition_spec.rb b/spec/ruby/language/regexp/repetition_spec.rb index 9a191d74e2..d76619688f 100644 --- a/spec/ruby/language/regexp/repetition_spec.rb +++ b/spec/ruby/language/regexp/repetition_spec.rb @@ -87,9 +87,7 @@ describe "Regexps with repetition" do /a+?*/.match("a")[0].should == "a" /(a+?)*/.match("a")[0].should == "a" - ruby_bug '#17341', ''...'3.0' do - /a+?*/.match("aa")[0].should == "aa" - end + /a+?*/.match("aa")[0].should == "aa" /(a+?)*/.match("aa")[0].should == "aa" # a+?+ should not be reduced, it should be equivalent to (a+?)+ @@ -100,9 +98,7 @@ describe "Regexps with repetition" do /a+?+/.match("a")[0].should == "a" /(a+?)+/.match("a")[0].should == "a" - ruby_bug '#17341', ''...'3.0' do - /a+?+/.match("aa")[0].should == "aa" - end + /a+?+/.match("aa")[0].should == "aa" /(a+?)+/.match("aa")[0].should == "aa" # both a**? and a+*? should be equivalent to (a+)?? diff --git a/spec/ruby/language/regexp_spec.rb b/spec/ruby/language/regexp_spec.rb index def9bba5f7..89d0914807 100644 --- a/spec/ruby/language/regexp_spec.rb +++ b/spec/ruby/language/regexp_spec.rb @@ -18,10 +18,8 @@ describe "Literal Regexps" do /Hello/.should be_kind_of(Regexp) end - ruby_version_is "3.0" do - it "is frozen" do - /Hello/.should.frozen? - end + it "is frozen" do + /Hello/.should.frozen? end it "caches the Regexp object" do @@ -96,7 +94,6 @@ describe "Literal Regexps" do /./.match("\0").to_a.should == ["\0"] end - it "supports | (alternations)" do /a|b/.match("a").to_a.should == ["a"] end @@ -115,10 +112,11 @@ describe "Literal Regexps" do /foo.(?<=\d)/.match("fooA foo1").to_a.should == ["foo1"] end - # https://bugs.ruby-lang.org/issues/13671 - it "raises a RegexpError for lookbehind with specific characters" do - r = Regexp.new("(?<!dss)", Regexp::IGNORECASE) - -> { r =~ "✨" }.should raise_error(RegexpError) + ruby_bug "#13671", ""..."3.5" do # https://bugs.ruby-lang.org/issues/13671 + it "handles a lookbehind with ss characters" do + r = Regexp.new("(?<!dss)", Regexp::IGNORECASE) + r.should =~ "✨" + end end it "supports (?<! ) (negative lookbehind)" do @@ -160,26 +158,6 @@ describe "Literal Regexps" do pattern.should_not =~ 'T' end - escapable_terminators = ['!', '"', '#', '%', '&', "'", ',', '-', ':', ';', '@', '_', '`'] - - it "supports escaping characters when used as a terminator" do - escapable_terminators.each do |c| - ref = "(?-mix:#{c})" - pattern = eval("%r" + c + "\\" + c + c) - pattern.to_s.should == ref - end - end - - it "treats an escaped non-escapable character normally when used as a terminator" do - all_terminators = [*("!".."/"), *(":".."@"), *("[".."`"), *("{".."~")] - special_cases = ['(', '{', '[', '<', '\\', '=', '~'] - (all_terminators - special_cases - escapable_terminators).each do |c| - ref = "(?-mix:\\#{c})" - pattern = eval("%r" + c + "\\" + c + c) - pattern.to_s.should == ref - end - end - it "support handling unicode 9.0 characters with POSIX bracket expressions" do char_lowercase = "\u{104D8}" # OSAGE SMALL LETTER A /[[:lower:]]/.match(char_lowercase).to_s.should == char_lowercase diff --git a/spec/ruby/language/rescue_spec.rb b/spec/ruby/language/rescue_spec.rb index 1c78f3935a..a3ee4807ac 100644 --- a/spec/ruby/language/rescue_spec.rb +++ b/spec/ruby/language/rescue_spec.rb @@ -61,6 +61,78 @@ describe "The rescue keyword" do end end + describe 'capturing in a local variable (that defines it)' do + it 'captures successfully in a method' do + ScratchPad.record [] + + def a + raise "message" + rescue => e + ScratchPad << e.message + end + + a + ScratchPad.recorded.should == ["message"] + end + + it 'captures successfully in a block' do + ScratchPad.record [] + + p = proc do + raise "message" + rescue => e + ScratchPad << e.message + end + + p.call + ScratchPad.recorded.should == ["message"] + end + + it 'captures successfully in a class' do + ScratchPad.record [] + + class RescueSpecs::C + raise "message" + rescue => e + ScratchPad << e.message + end + + ScratchPad.recorded.should == ["message"] + end + + it 'captures successfully in a module' do + ScratchPad.record [] + + module RescueSpecs::M + raise "message" + rescue => e + ScratchPad << e.message + end + + ScratchPad.recorded.should == ["message"] + end + + it 'captures sucpcessfully in a singleton class' do + ScratchPad.record [] + + class << Object.new + raise "message" + rescue => e + ScratchPad << e.message + end + + ScratchPad.recorded.should == ["message"] + end + + it 'captures successfully at the top-level' do + ScratchPad.record [] + + require_relative 'fixtures/rescue/top_level' + + ScratchPad.recorded.should == ["message"] + end + end + it "returns value from `rescue` if an exception was raised" do begin raise @@ -115,6 +187,18 @@ describe "The rescue keyword" do end end + it "converts the splatted list of exceptions using #to_a" do + exceptions = mock("to_a") + exceptions.should_receive(:to_a).and_return(exception_list) + caught_it = false + begin + raise SpecificExampleException, "not important" + rescue *exceptions + caught_it = true + end + caught_it.should be_true + end + it "can combine a splatted list of exceptions with a literal list of exceptions" do caught_it = false begin @@ -179,7 +263,7 @@ describe "The rescue keyword" do rescue ArgumentError end rescue StandardError => e - e.backtrace.first.should include ":in `raise_standard_error'" + e.backtrace.first.should =~ /:in [`'](?:RescueSpecs\.)?raise_standard_error'/ else fail("exception wasn't handled by the correct rescue block") end @@ -238,34 +322,16 @@ describe "The rescue keyword" do ScratchPad.recorded.should == [:one, :else_ran, :ensure_ran, :outside_begin] end - ruby_version_is ''...'2.6' do - it "will execute an else block even without rescue and ensure" do - -> { - eval <<-ruby - begin - ScratchPad << :begin - else - ScratchPad << :else - end - ruby - }.should complain(/else without rescue is useless/) - - ScratchPad.recorded.should == [:begin, :else] - end - end - - ruby_version_is '2.6' do - it "raises SyntaxError when else is used without rescue and ensure" do - -> { - eval <<-ruby - begin - ScratchPad << :begin - else - ScratchPad << :else - end - ruby - }.should raise_error(SyntaxError, /else without rescue is useless/) - end + it "raises SyntaxError when else is used without rescue and ensure" do + -> { + eval <<-ruby + begin + ScratchPad << :begin + else + ScratchPad << :else + end + ruby + }.should raise_error(SyntaxError, /else without rescue is useless/) end it "will not execute an else block if an exception was raised" do @@ -487,6 +553,23 @@ describe "The rescue keyword" do eval('1.+((1 rescue 1))').should == 2 end + ruby_version_is "3.4" do + it "does not introduce extra backtrace entries" do + def foo + begin + raise "oops" + rescue + return caller(0, 2) + end + end + line = __LINE__ + foo.should == [ + "#{__FILE__}:#{line-3}:in 'foo'", + "#{__FILE__}:#{line+1}:in 'block (3 levels) in <top (required)>'" + ] + end + end + describe "inline form" do it "can be inlined" do a = 1/0 rescue 1 @@ -510,14 +593,12 @@ describe "The rescue keyword" do }.should raise_error(Exception) end - ruby_version_is "2.7" do - it "rescues with multiple assignment" do + it "rescues with multiple assignment" do - a, b = raise rescue [1, 2] + a, b = raise rescue [1, 2] - a.should == 1 - b.should == 2 - end + a.should == 1 + b.should == 2 end end end diff --git a/spec/ruby/language/return_spec.rb b/spec/ruby/language/return_spec.rb index d8506834c8..a62ed1242d 100644 --- a/spec/ruby/language/return_spec.rb +++ b/spec/ruby/language/return_spec.rb @@ -422,18 +422,31 @@ describe "The return keyword" do end describe "within a block within a class" do - ruby_version_is "2.7" do - it "is not allowed" do - File.write(@filename, <<-END_OF_CODE) - class ReturnSpecs::A - ScratchPad << "before return" - 1.times { return } - ScratchPad << "after return" - end - END_OF_CODE - - -> { load @filename }.should raise_error(LocalJumpError) - end + it "is not allowed" do + File.write(@filename, <<-END_OF_CODE) + class ReturnSpecs::A + ScratchPad << "before return" + 1.times { return } + ScratchPad << "after return" + end + END_OF_CODE + + -> { load @filename }.should raise_error(LocalJumpError) + end + end + + describe "within BEGIN" do + it "is allowed" do + File.write(@filename, <<-END_OF_CODE) + BEGIN { + ScratchPad << "before call" + return + ScratchPad << "after call" + } + END_OF_CODE + + load @filename + ScratchPad.recorded.should == ["before call"] end end @@ -464,25 +477,13 @@ describe "The return keyword" do end describe "return with argument" do - ruby_version_is ""..."2.7" do - it "does not affect exit status" do - ruby_exe(<<-END_OF_CODE).should == "" - return 10 - END_OF_CODE - - $?.exitstatus.should == 0 - end - end - - ruby_version_is "2.7" do - it "warns but does not affect exit status" do - err = ruby_exe(<<-END_OF_CODE, args: "2>&1") - return 10 - END_OF_CODE - $?.exitstatus.should == 0 + it "warns but does not affect exit status" do + err = ruby_exe(<<-END_OF_CODE, args: "2>&1") + return 10 + END_OF_CODE + $?.exitstatus.should == 0 - err.should =~ /warning: argument of top-level return is ignored/ - end + err.should =~ /warning: argument of top-level return is ignored/ end end end diff --git a/spec/ruby/language/safe_navigator_spec.rb b/spec/ruby/language/safe_navigator_spec.rb index c3aecff2dd..b1e28c3963 100644 --- a/spec/ruby/language/safe_navigator_spec.rb +++ b/spec/ruby/language/safe_navigator_spec.rb @@ -7,43 +7,43 @@ describe "Safe navigator" do context "when context is nil" do it "always returns nil" do - eval("nil&.unknown").should == nil - eval("[][10]&.unknown").should == nil + nil&.unknown.should == nil + [][10]&.unknown.should == nil end it "can be chained" do - eval("nil&.one&.two&.three").should == nil + nil&.one&.two&.three.should == nil end it "doesn't evaluate arguments" do obj = Object.new obj.should_not_receive(:m) - eval("nil&.unknown(obj.m) { obj.m }") + nil&.unknown(obj.m) { obj.m } end end context "when context is false" do it "calls the method" do - eval("false&.to_s").should == "false" + false&.to_s.should == "false" - -> { eval("false&.unknown") }.should raise_error(NoMethodError) + -> { false&.unknown }.should raise_error(NoMethodError) end end context "when context is truthy" do it "calls the method" do - eval("1&.to_s").should == "1" + 1&.to_s.should == "1" - -> { eval("1&.unknown") }.should raise_error(NoMethodError) + -> { 1&.unknown }.should raise_error(NoMethodError) end end it "takes a list of arguments" do - eval("[1,2,3]&.first(2)").should == [1,2] + [1,2,3]&.first(2).should == [1,2] end it "takes a block" do - eval("[1,2]&.map { |i| i * 2 }").should == [2, 4] + [1,2]&.map { |i| i * 2 }.should == [2, 4] end it "allows assignment methods" do @@ -56,29 +56,77 @@ describe "Safe navigator" do end obj = klass.new - eval("obj&.foo = 3").should == 3 + (obj&.foo = 3).should == 3 obj.foo.should == 3 obj = nil - eval("obj&.foo = 3").should == nil + (obj&.foo = 3).should == nil end it "allows assignment operators" do klass = Class.new do - attr_accessor :m + attr_reader :m def initialize @m = 0 end + + def m=(v) + @m = v + 42 + end end obj = klass.new - eval("obj&.m += 3") + obj&.m += 3 obj.m.should == 3 obj = nil - eval("obj&.m += 3").should == nil + (obj&.m += 3).should == nil + end + + it "allows ||= operator" do + klass = Class.new do + attr_reader :m + + def initialize + @m = false + end + + def m=(v) + @m = v + 42 + end + end + + obj = klass.new + + (obj&.m ||= true).should == true + obj.m.should == true + + obj = nil + (obj&.m ||= true).should == nil + obj.should == nil + end + + it "allows &&= operator" do + klass = Class.new do + attr_accessor :m + + def initialize + @m = true + end + end + + obj = klass.new + + (obj&.m &&= false).should == false + obj.m.should == false + + obj = nil + (obj&.m &&= false).should == nil + obj.should == nil end it "does not call the operator method lazily with an assignment operator" do @@ -91,7 +139,7 @@ describe "Safe navigator" do obj = klass.new -> { - eval("obj&.foo += 3") + obj&.foo += 3 }.should raise_error(NoMethodError) { |e| e.name.should == :+ } diff --git a/spec/ruby/language/safe_spec.rb b/spec/ruby/language/safe_spec.rb index f3a7efc953..03ae96148e 100644 --- a/spec/ruby/language/safe_spec.rb +++ b/spec/ruby/language/safe_spec.rb @@ -1,152 +1,11 @@ require_relative '../spec_helper' describe "The $SAFE variable" do - ruby_version_is ""..."2.7" do - ruby_version_is "2.6" do - after :each do - $SAFE = 0 - end - end - - it "is 0 by default" do - $SAFE.should == 0 - proc { - $SAFE.should == 0 - }.call - end - - it "can be set to 0" do - proc { - $SAFE = 0 - $SAFE.should == 0 - }.call - end - - it "can be set to 1" do - proc { - $SAFE = 1 - $SAFE.should == 1 - }.call - end - - [2, 3, 4].each do |n| - it "cannot be set to #{n}" do - -> { - proc { - $SAFE = n - }.call - }.should raise_error(ArgumentError, /\$SAFE=2 to 4 are obsolete/) - end - end - - ruby_version_is ""..."2.6" do - it "cannot be set to values below 0" do - -> { - proc { - $SAFE = -100 - }.call - }.should raise_error(SecurityError, /tried to downgrade safe level from 0 to -100/) - end - end - - ruby_version_is "2.6" do - it "raises ArgumentError when set to values below 0" do - -> { - proc { - $SAFE = -100 - }.call - }.should raise_error(ArgumentError, "$SAFE should be >= 0") - end - end - - it "cannot be set to values above 4" do - -> { - proc { - $SAFE = 100 - }.call - }.should raise_error(ArgumentError, /\$SAFE=2 to 4 are obsolete/) - end - - ruby_version_is ""..."2.6" do - it "cannot be manually lowered" do - proc { - $SAFE = 1 - -> { - $SAFE = 0 - }.should raise_error(SecurityError, /tried to downgrade safe level from 1 to 0/) - }.call - end - - it "is automatically lowered when leaving a proc" do - $SAFE.should == 0 - proc { - $SAFE = 1 - }.call - $SAFE.should == 0 - end - - it "is automatically lowered when leaving a lambda" do - $SAFE.should == 0 - -> { - $SAFE = 1 - }.call - $SAFE.should == 0 - end - end - - ruby_version_is "2.6" do - it "can be manually lowered" do - $SAFE = 1 - $SAFE = 0 - $SAFE.should == 0 - end - - it "is not Proc local" do - $SAFE.should == 0 - proc { - $SAFE = 1 - }.call - $SAFE.should == 1 - end - - it "is not lambda local" do - $SAFE.should == 0 - -> { - $SAFE = 1 - }.call - $SAFE.should == 1 - end - - it "is global like regular global variables" do - Thread.new { $SAFE }.value.should == 0 - $SAFE = 1 - Thread.new { $SAFE }.value.should == 1 - end - end - - it "can be read when default from Thread#safe_level" do - Thread.current.safe_level.should == 0 - end - - it "can be read when modified from Thread#safe_level" do - proc { - $SAFE = 1 - Thread.current.safe_level.should == 1 - }.call - end - end - - ruby_version_is "2.7"..."3.0" do - it "warn when access" do - -> { - $SAFE - }.should complain(/\$SAFE will become a normal global variable in Ruby 3.0/) - end - - it "warn when set" do - -> { - $SAFE = 1 - }.should complain(/\$SAFE will become a normal global variable in Ruby 3.0/) - end + it "$SAFE is a regular global variable" do + $SAFE.should == nil + $SAFE = 42 + $SAFE.should == 42 + ensure + $SAFE = nil end end diff --git a/spec/ruby/language/send_spec.rb b/spec/ruby/language/send_spec.rb index e57e2c65dc..aaccdf0998 100644 --- a/spec/ruby/language/send_spec.rb +++ b/spec/ruby/language/send_spec.rb @@ -43,7 +43,7 @@ describe "Invoking a method" do end describe "with optional arguments" do - it "uses the optional argument if none is is passed" do + it "uses the optional argument if none is passed" do specs.fooM0O1.should == [1] end @@ -258,20 +258,10 @@ describe "Invoking a private setter method" do end describe "Invoking a private getter method" do - ruby_version_is ""..."2.7" do - it "does not permit self as a receiver" do - receiver = LangSendSpecs::PrivateGetter.new - -> { receiver.call_self_foo }.should raise_error(NoMethodError) - -> { receiver.call_self_foo_or_equals(6) }.should raise_error(NoMethodError) - end - end - - ruby_version_is "2.7" do - it "permits self as a receiver" do - receiver = LangSendSpecs::PrivateGetter.new - receiver.call_self_foo_or_equals(6) - receiver.call_self_foo.should == 6 - end + it "permits self as a receiver" do + receiver = LangSendSpecs::PrivateGetter.new + receiver.call_self_foo_or_equals(6) + receiver.call_self_foo.should == 6 end end @@ -421,36 +411,18 @@ describe "Invoking a method" do specs.rest_len(0,*a,4,*5,6,7,*c,-1).should == 11 end - ruby_version_is ""..."3.0" do - it "expands the Array elements from the splat after executing the arguments and block if no other arguments follow the splat" do - def self.m(*args, &block) - [args, block] - end - - args = [1, nil] - m(*args, &args.pop).should == [[1], nil] - - args = [1, nil] - order = [] - m(*(order << :args; args), &(order << :block; args.pop)).should == [[1], nil] - order.should == [:args, :block] + it "expands the Array elements from the splat before applying block argument operations" do + def self.m(*args, &block) + [args, block] end - end - - ruby_version_is "3.0" do - it "expands the Array elements from the splat before applying block argument operations" do - def self.m(*args, &block) - [args, block] - end - args = [1, nil] - m(*args, &args.pop).should == [[1, nil], nil] + args = [1, nil] + m(*args, &args.pop).should == [[1, nil], nil] - args = [1, nil] - order = [] - m(*(order << :args; args), &(order << :block; args.pop)).should == [[1, nil], nil] - order.should == [:args, :block] - end + args = [1, nil] + order = [] + m(*(order << :args; args), &(order << :block; args.pop)).should == [[1, nil], nil] + order.should == [:args, :block] end it "evaluates the splatted arguments before the block if there are other arguments after the splat" do diff --git a/spec/ruby/language/singleton_class_spec.rb b/spec/ruby/language/singleton_class_spec.rb index 7e9370d6f9..45e1f7f3ad 100644 --- a/spec/ruby/language/singleton_class_spec.rb +++ b/spec/ruby/language/singleton_class_spec.rb @@ -70,7 +70,7 @@ describe "A singleton class" do end it "has class String as the superclass of a String instance" do - "blah".singleton_class.superclass.should == String + "blah".dup.singleton_class.superclass.should == String end it "doesn't have singleton class" do @@ -157,23 +157,6 @@ describe "A constant on a singleton class" do end end -describe "Defining yield in singleton class" do - ruby_version_is "2.7"..."3.0" do - it 'emits a deprecation warning' do - code = <<~RUBY - def m - class << Object.new - yield - end - end - m { :ok } - RUBY - - -> { eval(code) }.should complain(/warning: `yield' in class syntax will not be supported from Ruby 3.0/) - end - end -end - describe "Defining instance methods on a singleton class" do before :each do @k = ClassSpecs::K.new @@ -308,3 +291,27 @@ describe "Instantiating a singleton class" do }.should raise_error(TypeError) end end + +describe "Frozen properties" do + it "is frozen if the object it is created from is frozen" do + o = Object.new + o.freeze + klass = o.singleton_class + klass.frozen?.should == true + end + + it "will be frozen if the object it is created from becomes frozen" do + o = Object.new + klass = o.singleton_class + klass.frozen?.should == false + o.freeze + klass.frozen?.should == true + end + + it "will be unfrozen if the frozen object is cloned with freeze set to false" do + o = Object.new + o.freeze + o2 = o.clone(freeze: false) + o2.singleton_class.frozen?.should == false + end +end diff --git a/spec/ruby/language/source_encoding_spec.rb b/spec/ruby/language/source_encoding_spec.rb index 19364fc676..7135bc0a70 100644 --- a/spec/ruby/language/source_encoding_spec.rb +++ b/spec/ruby/language/source_encoding_spec.rb @@ -15,7 +15,7 @@ describe "Source files" do end describe "encoded in UTF-16 LE without a BOM" do - it "are parsed because empty as they contain a NUL byte before the encoding comment" do + it "are parsed as empty because they contain a NUL byte before the encoding comment" do ruby_exe(fixture(__FILE__, "utf16-le-nobom.rb"), args: "2>&1").should == "" end end diff --git a/spec/ruby/language/string_spec.rb b/spec/ruby/language/string_spec.rb index ce4941569e..083a7f5db5 100644 --- a/spec/ruby/language/string_spec.rb +++ b/spec/ruby/language/string_spec.rb @@ -56,28 +56,6 @@ describe "Ruby character strings" do "#\$".should == '#$' end - ruby_version_is ''...'2.7' do - it "taints the result of interpolation when an interpolated value is tainted" do - "#{"".taint}".tainted?.should be_true - - @ip.taint - "#@ip".tainted?.should be_true - - $ip.taint - "#$ip".tainted?.should be_true - end - - it "untrusts the result of interpolation when an interpolated value is untrusted" do - "#{"".untrust}".untrusted?.should be_true - - @ip.untrust - "#@ip".untrusted?.should be_true - - $ip.untrust - "#$ip".untrusted?.should be_true - end - end - it "allows using non-alnum characters as string delimiters" do %(hey #{@ip}).should == "hey xxx" %[hey #{@ip}].should == "hey xxx" @@ -253,8 +231,16 @@ describe "Ruby String literals" do ruby_exe(fixture(__FILE__, "freeze_magic_comment_across_files.rb")).chomp.should == "true" end - it "produce different objects for literals with the same content in different files if the other file doesn't have the comment" do - ruby_exe(fixture(__FILE__, "freeze_magic_comment_across_files_no_comment.rb")).chomp.should == "true" + guard -> { !(eval("'test'").frozen? && "test".equal?("test")) } do + it "produces different objects for literals with the same content in different files if the other file doesn't have the comment and String literals aren't frozen by default" do + ruby_exe(fixture(__FILE__, "freeze_magic_comment_across_files_no_comment.rb")).chomp.should == "true" + end + end + + guard -> { eval("'test'").frozen? && "test".equal?("test") } do + it "produces the same objects for literals with the same content in different files if the other file doesn't have the comment and String literals are frozen by default" do + ruby_exe(fixture(__FILE__, "freeze_magic_comment_across_files_no_comment.rb")).chomp.should == "false" + end end it "produce different objects for literals with the same content in different files if they have different encodings" do @@ -273,12 +259,12 @@ describe "Ruby String interpolation" do it "returns a string with the source encoding by default" do "a#{"b"}c".encoding.should == Encoding::BINARY - eval('"a#{"b"}c"'.force_encoding("us-ascii")).encoding.should == Encoding::US_ASCII + eval('"a#{"b"}c"'.dup.force_encoding("us-ascii")).encoding.should == Encoding::US_ASCII eval("# coding: US-ASCII \n 'a#{"b"}c'").encoding.should == Encoding::US_ASCII end it "returns a string with the source encoding, even if the components have another encoding" do - a = "abc".force_encoding("euc-jp") + a = "abc".dup.force_encoding("euc-jp") "#{a}".encoding.should == Encoding::BINARY b = "abc".encode("utf-8") @@ -287,7 +273,7 @@ describe "Ruby String interpolation" do it "raises an Encoding::CompatibilityError if the Encodings are not compatible" do a = "\u3042" - b = "\xff".force_encoding "binary" + b = "\xff".dup.force_encoding "binary" -> { "#{a} #{b}" }.should raise_error(Encoding::CompatibilityError) end @@ -299,23 +285,11 @@ describe "Ruby String interpolation" do eval(code).should_not.frozen? end - ruby_version_is "3.0" do - it "creates a non-frozen String when # frozen-string-literal: true is used" do - code = <<~'RUBY' - # frozen-string-literal: true - "a#{6*7}c" - RUBY - eval(code).should_not.frozen? - end - end - - ruby_version_is ""..."3.0" do - it "creates a frozen String when # frozen-string-literal: true is used" do - code = <<~'RUBY' - # frozen-string-literal: true - "a#{6*7}c" - RUBY - eval(code).should.frozen? - end + it "creates a non-frozen String when # frozen-string-literal: true is used" do + code = <<~'RUBY' + # frozen-string-literal: true + "a#{6*7}c" + RUBY + eval(code).should_not.frozen? end end diff --git a/spec/ruby/language/super_spec.rb b/spec/ruby/language/super_spec.rb index 1ac5c5e1be..a98b3b3091 100644 --- a/spec/ruby/language/super_spec.rb +++ b/spec/ruby/language/super_spec.rb @@ -203,6 +203,25 @@ describe "The super keyword" do -> { klass.new.a(:a_called) }.should raise_error(RuntimeError) end + it "is able to navigate to super, when a method is defined dynamically on the singleton class" do + foo_class = Class.new do + def bar + "bar" + end + end + + mixin_module = Module.new do + def bar + "super_" + super + end + end + + foo = foo_class.new + foo.singleton_class.define_method(:bar, mixin_module.instance_method(:bar)) + + foo.bar.should == "super_bar" + end + # Rubinius ticket github#157 it "calls method_missing when a superclass method is not found" do SuperSpecs::MM_B.new.is_a?(Hash).should == false @@ -316,12 +335,23 @@ describe "The super keyword" do it "without explicit arguments that are '_'" do SuperSpecs::ZSuperWithUnderscores::B.new.m(1, 2).should == [1, 2] + SuperSpecs::ZSuperWithUnderscores::B.new.m3(1, 2, 3).should == [1, 2, 3] + SuperSpecs::ZSuperWithUnderscores::B.new.m4(1, 2, 3, 4).should == [1, 2, 3, 4] + SuperSpecs::ZSuperWithUnderscores::B.new.m_default(1).should == [1] + SuperSpecs::ZSuperWithUnderscores::B.new.m_default.should == [0] + SuperSpecs::ZSuperWithUnderscores::B.new.m_pre_default_rest_post(1, 2, 3, 4, 5, 6, 7).should == [1, 2, 3, 4, 5, 6, 7] + SuperSpecs::ZSuperWithUnderscores::B.new.m_rest(1, 2).should == [1, 2] + SuperSpecs::ZSuperWithUnderscores::B.new.m_kwrest(a: 1).should == {a: 1} end it "without explicit arguments that are '_' including any modifications" do SuperSpecs::ZSuperWithUnderscores::B.new.m_modified(1, 2).should == [14, 2] end + it "should pass method arguments when called within a closure" do + SuperSpecs::ZSuperInBlock::B.new.m(arg: 1).should == 1 + end + describe 'when using keyword arguments' do before :each do @req = SuperSpecs::Keywords::RequiredArguments.new diff --git a/spec/ruby/language/symbol_spec.rb b/spec/ruby/language/symbol_spec.rb index d6a41d3059..0801d3223e 100644 --- a/spec/ruby/language/symbol_spec.rb +++ b/spec/ruby/language/symbol_spec.rb @@ -96,11 +96,13 @@ describe "A Symbol literal" do %I{a b #{"c"}}.should == [:a, :b, :c] end - it "with invalid bytes raises an EncodingError at parse time" do - ScratchPad.record [] - -> { - eval 'ScratchPad << 1; :"\xC3"' - }.should raise_error(EncodingError, /invalid/) - ScratchPad.recorded.should == [] + ruby_bug "#20280", ""..."3.4" do + it "raises an SyntaxError at parse time when Symbol with invalid bytes" do + ScratchPad.record [] + -> { + eval 'ScratchPad << 1; :"\xC3"' + }.should raise_error(SyntaxError, /invalid symbol/) + ScratchPad.recorded.should == [] + end end end diff --git a/spec/ruby/language/undef_spec.rb b/spec/ruby/language/undef_spec.rb index 4e473b803f..29dba4afb4 100644 --- a/spec/ruby/language/undef_spec.rb +++ b/spec/ruby/language/undef_spec.rb @@ -38,12 +38,19 @@ describe "The undef keyword" do -> { @obj.meth(5) }.should raise_error(NoMethodError) end - it "with a interpolated symbol" do + it "with an interpolated symbol" do @undef_class.class_eval do undef :"#{'meth'}" end -> { @obj.meth(5) }.should raise_error(NoMethodError) end + + it "with an interpolated symbol when interpolated expression is not a String literal" do + @undef_class.class_eval do + undef :"#{'meth'.to_sym}" + end + -> { @obj.meth(5) }.should raise_error(NoMethodError) + end end it "allows undefining multiple methods at a time" do diff --git a/spec/ruby/language/variables_spec.rb b/spec/ruby/language/variables_spec.rb index ce072baa2c..01be61a9dc 100644 --- a/spec/ruby/language/variables_spec.rb +++ b/spec/ruby/language/variables_spec.rb @@ -1,6 +1,86 @@ require_relative '../spec_helper' require_relative 'fixtures/variables' +describe "Evaluation order during assignment" do + context "with single assignment" do + it "evaluates from left to right" do + obj = VariablesSpecs::EvalOrder.new + obj.instance_eval do + foo[0] = a + end + + obj.order.should == ["foo", "a", "foo[]="] + end + end + + context "with multiple assignment" do + ruby_version_is ""..."3.1" do + it "does not evaluate from left to right" do + obj = VariablesSpecs::EvalOrder.new + + obj.instance_eval do + foo[0], bar.baz = a, b + end + + obj.order.should == ["a", "b", "foo", "foo[]=", "bar", "bar.baz="] + end + + it "cannot be used to swap variables with nested method calls" do + node = VariablesSpecs::EvalOrder.new.node + + original_node = node + original_node_left = node.left + original_node_left_right = node.left.right + + node.left, node.left.right, node = node.left.right, node, node.left + # Should evaluate in the order of: + # RHS: node.left.right, node, node.left + # LHS: + # * node(original_node), original_node.left = original_node_left_right + # * node(original_node), node.left(changed in the previous assignment to original_node_left_right), + # original_node_left_right.right = original_node + # * node = original_node_left + + node.should == original_node_left + node.right.should_not == original_node + node.right.left.should_not == original_node_left_right + end + end + + ruby_version_is "3.1" do + it "evaluates from left to right, receivers first then methods" do + obj = VariablesSpecs::EvalOrder.new + obj.instance_eval do + foo[0], bar.baz = a, b + end + + obj.order.should == ["foo", "bar", "a", "b", "foo[]=", "bar.baz="] + end + + it "can be used to swap variables with nested method calls" do + node = VariablesSpecs::EvalOrder.new.node + + original_node = node + original_node_left = node.left + original_node_left_right = node.left.right + + node.left, node.left.right, node = node.left.right, node, node.left + # Should evaluate in the order of: + # LHS: node, node.left(original_node_left) + # RHS: original_node_left_right, original_node, original_node_left + # Ops: + # * node(original_node), original_node.left = original_node_left_right + # * original_node_left.right = original_node + # * node = original_node_left + + node.should == original_node_left + node.right.should == original_node + node.right.left.should == original_node_left_right + end + end + end +end + describe "Multiple assignment" do context "with a single RHS value" do it "assigns a simple MLHS" do @@ -287,8 +367,13 @@ describe "Multiple assignment" do it "assigns indexed elements" do a = [] - a[1], a[2] = 1 - a.should == [nil, 1, nil] + a[1], a[2] = 1, 2 + a.should == [nil, 1, 2] + + # with splatted argument + a = [] + a[*[1]], a[*[2]] = 1, 2 + a.should == [nil, 1, 2] end it "assigns constants" do @@ -715,6 +800,18 @@ describe "Multiple assignment" do x.should == [1, 2, 3, 4, 5] end + it "can be used to swap array elements" do + a = [1, 2] + a[0], a[1] = a[1], a[0] + a.should == [2, 1] + end + + it "can be used to swap range of array elements" do + a = [1, 2, 3, 4] + a[0, 2], a[2, 2] = a[2, 2], a[0, 2] + a.should == [3, 4, 1, 2] + end + it "assigns RHS values to LHS constants" do module VariableSpecs MRHS_VALUES_1, MRHS_VALUES_2 = 1, 2 @@ -770,47 +867,21 @@ describe "A local variable assigned only within a conditional block" do end describe 'Local variable shadowing' do - ruby_version_is ""..."2.6" do - it "leads to warning in verbose mode" do - -> do - eval <<-CODE - a = [1, 2, 3] - a.each { |a| a = 3 } - CODE - end.should complain(/shadowing outer local variable/, verbose: true) - end - end - - ruby_version_is "2.6" do - it "does not warn in verbose mode" do - result = nil + it "does not warn in verbose mode" do + result = nil - -> do - eval <<-CODE - a = [1, 2, 3] - result = a.map { |a| a = 3 } - CODE - end.should_not complain(verbose: true) + -> do + eval <<-CODE + a = [1, 2, 3] + result = a.map { |a| a = 3 } + CODE + end.should_not complain(verbose: true) - result.should == [3, 3, 3] - end + result.should == [3, 3, 3] end end describe 'Allowed characters' do - ruby_version_is "2.6" do - # new feature in 2.6 -- https://bugs.ruby-lang.org/issues/13770 - it 'does not allow non-ASCII upcased characters at the beginning' do - -> do - eval <<-CODE - def test - ἍBB = 1 - end - CODE - end.should raise_error(SyntaxError, /dynamic constant assignment/) - end - end - it 'allows non-ASCII lowercased characters at the beginning' do result = nil @@ -824,4 +895,50 @@ describe 'Allowed characters' do result.should == 1 end + + it 'parses a non-ASCII upcased character as a constant identifier' do + -> do + eval <<-CODE + def test + ἍBB = 1 + end + CODE + end.should raise_error(SyntaxError, /dynamic constant assignment/) + end +end + +describe "Instance variables" do + context "when instance variable is uninitialized" do + it "doesn't warn about accessing uninitialized instance variable" do + obj = Object.new + def obj.foobar; a = @a; end + + -> { obj.foobar }.should_not complain(verbose: true) + end + + it "doesn't warn at lazy initialization" do + obj = Object.new + def obj.foobar; @a ||= 42; end + + -> { obj.foobar }.should_not complain(verbose: true) + end + end + + describe "global variable" do + context "when global variable is uninitialized" do + it "warns about accessing uninitialized global variable in verbose mode" do + obj = Object.new + def obj.foobar; a = $specs_uninitialized_global_variable; end + + -> { obj.foobar }.should complain(/warning: global variable [`']\$specs_uninitialized_global_variable' not initialized/, verbose: true) + end + + it "doesn't warn at lazy initialization" do + obj = Object.new + def obj.foobar; $specs_uninitialized_global_variable_lazy ||= 42; end + + -> { obj.foobar }.should_not complain(verbose: true) + end + end + end end diff --git a/spec/ruby/language/yield_spec.rb b/spec/ruby/language/yield_spec.rb index 5fad7cb176..5283517636 100644 --- a/spec/ruby/language/yield_spec.rb +++ b/spec/ruby/language/yield_spec.rb @@ -183,5 +183,26 @@ describe "The yield call" do it "uses captured block of a block used in define_method" do @y.deep(2).should == 4 end +end + +describe "Using yield in a singleton class literal" do + it 'raises a SyntaxError' do + code = <<~RUBY + class << Object.new + yield + end + RUBY + + -> { eval(code) }.should raise_error(SyntaxError, /Invalid yield/) + end +end +describe "Using yield in non-lambda block" do + it 'raises a SyntaxError' do + code = <<~RUBY + 1.times { yield } + RUBY + + -> { eval(code) }.should raise_error(SyntaxError, /Invalid yield/) + end end |