diff options
Diffstat (limited to 'spec/ruby/language')
99 files changed, 8080 insertions, 3880 deletions
diff --git a/spec/ruby/language/BEGIN_spec.rb b/spec/ruby/language/BEGIN_spec.rb index 5aef5a1d7c..25db32b96a 100644 --- a/spec/ruby/language/BEGIN_spec.rb +++ b/spec/ruby/language/BEGIN_spec.rb @@ -15,7 +15,7 @@ describe "The BEGIN keyword" do end it "must appear in a top-level context" do - -> { eval "1.times { BEGIN { 1 } }" }.should raise_error(SyntaxError) + -> { eval "1.times { BEGIN { 1 } }" }.should.raise(SyntaxError) end it "uses top-level for self" do 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/README b/spec/ruby/language/README index 74eaf58709..ae08e17fb1 100644 --- a/spec/ruby/language/README +++ b/spec/ruby/language/README @@ -4,7 +4,7 @@ words. These words significantly describe major elements of the language, including flow control constructs like 'for' and 'while', conditional execution like 'if' and 'unless', exceptional execution control like 'rescue', etc. There are also literals for the basic "types" like String, Regexp, Array -and Fixnum. +and Integer. Behavioral specifications describe the behavior of concrete entities. Rather than using concepts of computation to organize these spec files, we use diff --git a/spec/ruby/language/alias_spec.rb b/spec/ruby/language/alias_spec.rb index 13f5c49513..4b3d36d308 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 @@ -122,7 +140,7 @@ describe "The alias keyword" do end @obj.__value.should == 5 - -> { AliasObject.new.__value }.should raise_error(NoMethodError) + -> { AliasObject.new.__value }.should.raise(NoMethodError) end it "operates on the class/module metaclass when used in instance_eval" do @@ -131,7 +149,7 @@ describe "The alias keyword" do end AliasObject.__klass_method.should == 7 - -> { Object.__klass_method }.should raise_error(NoMethodError) + -> { Object.__klass_method }.should.raise(NoMethodError) end it "operates on the class/module metaclass when used in instance_exec" do @@ -140,7 +158,7 @@ describe "The alias keyword" do end AliasObject.__klass_method2.should == 7 - -> { Object.__klass_method2 }.should raise_error(NoMethodError) + -> { Object.__klass_method2 }.should.raise(NoMethodError) end it "operates on methods defined via attr, attr_reader, and attr_accessor" do @@ -218,31 +236,44 @@ describe "The alias keyword" do subclass.new.test("testing").should == 4 end - it "is not allowed against Fixnum or String instances" do + it "is not allowed against Integer or String instances" do -> do 1.instance_eval do alias :foo :to_s end - end.should raise_error(TypeError) + end.should.raise(TypeError) -> do :blah.instance_eval do alias :foo :to_s end - end.should raise_error(TypeError) + end.should.raise(TypeError) end 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 - -> { @meta.class_eval { alias undef_method not_exist } }.should raise_error(NameError) { |e| + -> { @meta.class_eval { alias undef_method not_exist } }.should.raise(NameError) { |e| # a NameError and not a NoMethodError 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/and_spec.rb b/spec/ruby/language/and_spec.rb index 55a2a3103a..c5c255989b 100644 --- a/spec/ruby/language/and_spec.rb +++ b/spec/ruby/language/and_spec.rb @@ -5,7 +5,7 @@ describe "The '&&' statement" do it "short-circuits evaluation at the first condition to be false" do x = nil true && false && x = 1 - x.should be_nil + x.should == nil end it "evaluates to the first condition not to be true" do @@ -33,9 +33,9 @@ describe "The '&&' statement" do end it "treats empty expressions as nil" do - (() && true).should be_nil - (true && ()).should be_nil - (() && ()).should be_nil + (() && true).should == nil + (true && ()).should == nil + (() && ()).should == nil end end @@ -44,7 +44,7 @@ describe "The 'and' statement" do it "short-circuits evaluation at the first condition to be false" do x = nil true and false and x = 1 - x.should be_nil + x.should == nil end it "evaluates to the first condition not to be true" do @@ -72,9 +72,9 @@ describe "The 'and' statement" do end it "treats empty expressions as nil" do - (() and true).should be_nil - (true and ()).should be_nil - (() and ()).should be_nil + (() and true).should == nil + (true and ()).should == nil + (() and ()).should == nil end end diff --git a/spec/ruby/language/array_spec.rb b/spec/ruby/language/array_spec.rb index 2583cffbf7..3601eb0e00 100644 --- a/spec/ruby/language/array_spec.rb +++ b/spec/ruby/language/array_spec.rb @@ -4,7 +4,7 @@ require_relative 'fixtures/array' describe "Array literals" do it "[] should return a new array populated with the given elements" do array = [1, 'a', nil] - array.should be_kind_of(Array) + array.should.is_a?(Array) array[0].should == 1 array[1].should == 'a' array[2].should == nil @@ -12,7 +12,7 @@ describe "Array literals" do it "[] treats empty expressions as nil elements" do array = [0, (), 2, (), 4] - array.should be_kind_of(Array) + array.should.is_a?(Array) array[0].should == 0 array[1].should == nil array[2].should == 2 @@ -89,7 +89,7 @@ describe "The unpacking splat operator (*)" do it "returns a new array containing the same values when applied to an array inside an empty array" do splatted_array = [3, 4, 5] [*splatted_array].should == splatted_array - [*splatted_array].should_not equal(splatted_array) + [*splatted_array].should_not.equal?(splatted_array) end it "unpacks the start and count arguments in an array slice assignment" do @@ -135,7 +135,7 @@ describe "The unpacking splat operator (*)" do it "when applied to a non-Array value uses it unchanged if it does not respond_to?(:to_a)" do obj = Object.new - obj.should_not respond_to(:to_a) + obj.should_not.respond_to?(:to_a) [1, *obj].should == [1, obj] end diff --git a/spec/ruby/language/assignments_spec.rb b/spec/ruby/language/assignments_spec.rb new file mode 100644 index 0000000000..d621c9f0c6 --- /dev/null +++ b/spec/ruby/language/assignments_spec.rb @@ -0,0 +1,582 @@ +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 + + 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 + + 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(TypeError) + + ScratchPad.recorded.should == [:rhs] + end + end + + context "given block argument" do + before do + @klass = Class.new do + def initialize(h) @h = h end + def [](k, &block) @h[k]; end + def []=(k, v, &block) @h[k] = v; end + end + end + + ruby_version_is ""..."3.4" do + it "accepts block argument" do + obj = @klass.new(a: 1) + block = proc {} + + eval "obj[:a, &block] = 2" + eval("obj[:a, &block]").should == 2 + end + end + + ruby_version_is "3.4" do + it "raises SyntaxError" do + obj = @klass.new(a: 1) + block = proc {} + + -> { + eval "obj[:a, &block] = 2" + }.should.raise(SyntaxError, /unexpected block arg given in index assignment|block arg given in index assignment/) + end + end + end + + context "given keyword arguments" do + before do + @klass = Class.new do + attr_reader :x + + def []=(*args, **kw) + @x = [args, kw] + end + end + end + + ruby_version_is ""..."3.4" do + it "supports keyword arguments in index assignments" do + a = @klass.new + eval "a[1, 2, 3, b: 4] = 5" + a.x.should == [[1, 2, 3, {b: 4}, 5], {}] + end + end + + ruby_version_is "3.4" do + it "raises SyntaxError when given keyword arguments in index assignments" do + a = @klass.new + -> { eval "a[1, 2, 3, b: 4] = 5" }.should.raise(SyntaxError, + /keywords are not allowed in index assignment expressions|keyword arg given in index assignment/) # prism|parse.y + end + 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 "given block argument" do + before do + @klass = Class.new do + def initialize(h) @h = h end + def [](k, &block) @h[k]; end + def []=(k, v, &block) @h[k] = v; end + end + end + + ruby_version_is ""..."3.4" do + it "accepts block argument" do + obj = @klass.new(a: 1) + block = proc {} + + eval "obj[:a, &block] += 2" + eval("obj[:a, &block]").should == 3 + end + end + + ruby_version_is "3.4" do + it "raises SyntaxError" do + obj = @klass.new(a: 1) + block = proc {} + + -> { + eval "obj[:a, &block] += 2" + }.should.raise(SyntaxError, /unexpected block arg given in index assignment|block arg given in index assignment/) + end + end + end + + context "given keyword arguments" do + before do + @klass = Class.new do + attr_reader :x + + def [](*args) + 100 + end + + def []=(*args, **kw) + @x = [args, kw] + end + end + end + + ruby_version_is ""..."3.4" do + it "supports keyword arguments in index assignments" do + a = @klass.new + eval "a[1, 2, 3, b: 4] += 5" + a.x.should == [[1, 2, 3, 105], {b: 4}] + end + end + + ruby_version_is "3.4" do + it "raises SyntaxError when given keyword arguments in index assignments" do + a = @klass.new + -> { eval "a[1, 2, 3, b: 4] += 5" }.should.raise(SyntaxError, + /keywords are not allowed in index assignment expressions|keyword arg given in index assignment/) # prism|parse.y + end + end + 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 + 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 + + 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 + + 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 + + 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 45a8ec5f9a..5bdb993aea 100644 --- a/spec/ruby/language/block_spec.rb +++ b/spec/ruby/language/block_spec.rb @@ -13,7 +13,7 @@ describe "A block yielded a single" do it "receives the identical Array object" do ary = [1, 2] - m(ary) { |a| a }.should equal(ary) + m(ary) { |a| a }.should.equal?(ary) end it "assigns the Array to a single rest argument" do @@ -40,80 +40,54 @@ 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 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 - 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}) - - result = m([obj]) { |a=nil, **b| [a, b] } - result.should == [{"a" => 1}, b: 2] - 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 - 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, {}] - 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 - - 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) + 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 - result = m([obj]) { |a=nil, **b| [a, b] } - result.should == [[obj], {}] - end - end + 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 - 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}) + 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(ArgumentError, "missing keywords: :b, :c") 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}) + 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 - 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 +96,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 + 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" do - result = m(["a" => 10]) { |a = nil, **b| [a, b] } - result.should == [{"a" => 10}, {}] - end - end - - 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 + 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 - 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, {}] + result = m(["a" => 10, b: 2]) { |a=nil, **b| [a, b] } + result.should == [[{"a" => 10, b: 2}], {}] end end + end - it "calls #to_hash on the last element when there are more arguments than parameters" 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}] - 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,11 +178,70 @@ 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 "calls #respond_to? on a BasicObject to check if object has method #to_ary" do + ScratchPad.record [] + obj = BasicObject.new + def obj.respond_to?(name, *) + ScratchPad << [:respond_to?, name] + name == :to_ary ? true : super + end + def obj.to_ary + ScratchPad << :to_ary + [1, 2] + end + + m(obj) { |a, b, c| [a, b, c] }.should == [1, 2, nil] + ScratchPad.recorded.should == [[:respond_to?, :to_ary], :to_ary] + 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) + -> { m(obj) { |a, b| } }.should.raise(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(RuntimeError, "Exception raised in #to_ary") end end end @@ -318,7 +291,7 @@ describe "A block" do end it "may include a rescue clause" do - eval("@y.z do raise ArgumentError; rescue ArgumentError; 7; end").should == 7 + @y.z do raise ArgumentError; rescue ArgumentError; 7; end.should == 7 end end @@ -332,13 +305,13 @@ describe "A block" do end it "may include a rescue clause" do - eval('@y.z do || raise ArgumentError; rescue ArgumentError; 7; end').should == 7 + @y.z do || raise ArgumentError; rescue ArgumentError; 7; end.should == 7 end end describe "taking |a| arguments" do it "assigns nil to the argument when no values are yielded" do - @y.z { |a| a }.should be_nil + @y.z { |a| a }.should == nil end it "assigns the value yielded to the argument" do @@ -349,7 +322,7 @@ describe "A block" do obj = mock("block yield to_ary") obj.should_not_receive(:to_ary) - @y.s(obj) { |a| a }.should equal(obj) + @y.s(obj) { |a| a }.should.equal?(obj) end it "assigns the first value yielded to the argument" do @@ -361,7 +334,7 @@ describe "A block" do end it "may include a rescue clause" do - eval('@y.s(1) do |x| raise ArgumentError; rescue ArgumentError; 7; end').should == 7 + @y.s(1) do |x| raise ArgumentError; rescue ArgumentError; 7; end.should == 7 end end @@ -425,16 +398,15 @@ describe "A block" do obj = mock("block yield to_ary invalid") obj.should_receive(:to_ary).and_return(1) - -> { @y.s(obj) { |a, b| } }.should raise_error(TypeError) + -> { @y.s(obj) { |a, b| } }.should.raise(TypeError) end it "raises the original exception if #to_ary raises an exception" do obj = mock("block yield to_ary raising an exception") obj.should_receive(:to_ary).and_raise(ZeroDivisionError) - -> { @y.s(obj) { |a, b| } }.should raise_error(ZeroDivisionError) + -> { @y.s(obj) { |a, b| } }.should.raise(ZeroDivisionError) end - end describe "taking |a, *b| arguments" do @@ -489,7 +461,7 @@ describe "A block" do obj = mock("block yield to_ary invalid") obj.should_receive(:to_ary).and_return(1) - -> { @y.s(obj) { |a, *b| } }.should raise_error(TypeError) + -> { @y.s(obj) { |a, *b| } }.should.raise(TypeError) end end @@ -567,7 +539,7 @@ describe "A block" do describe "taking |a, | arguments" do it "assigns nil to the argument when no values are yielded" do - @y.z { |a, | a }.should be_nil + @y.z { |a, | a }.should == nil end it "assigns the argument a single value yielded" do @@ -583,7 +555,7 @@ describe "A block" do end it "assigns nil to the argument when passed an empty Array" do - @y.s([]) { |a, | a }.should be_nil + @y.s([]) { |a, | a }.should == nil end it "assigns the argument the first element of the Array when passed a single Array" do @@ -614,7 +586,7 @@ describe "A block" do obj = mock("block yield to_ary invalid") obj.should_receive(:to_ary).and_return(1) - -> { @y.s(obj) { |a, | } }.should raise_error(TypeError) + -> { @y.s(obj) { |a, | } }.should.raise(TypeError) end end @@ -656,7 +628,7 @@ describe "A block" do obj = mock("block yield to_ary invalid") obj.should_receive(:to_ary).and_return(1) - -> { @y.s(obj) { |(a, b)| } }.should raise_error(TypeError) + -> { @y.s(obj) { |(a, b)| } }.should.raise(TypeError) end end @@ -697,7 +669,7 @@ describe "A block" do obj = mock("block yield to_ary invalid") obj.should_receive(:to_ary).and_return(1) - -> { @y.s(obj) { |(a, b), c| } }.should raise_error(TypeError) + -> { @y.s(obj) { |(a, b), c| } }.should.raise(TypeError) end end @@ -756,15 +728,51 @@ describe "A block" do describe "taking identically-named arguments" do it "raises a SyntaxError for standard arguments" do - -> { eval "lambda { |x,x| }" }.should raise_error(SyntaxError) - -> { eval "->(x,x) {}" }.should raise_error(SyntaxError) - -> { eval "Proc.new { |x,x| }" }.should raise_error(SyntaxError) + -> { eval "lambda { |x,x| }" }.should.raise(SyntaxError) + -> { eval "->(x,x) {}" }.should.raise(SyntaxError) + -> { eval "Proc.new { |x,x| }" }.should.raise(SyntaxError) end it "accepts unnamed arguments" do - eval("lambda { |_,_| }").should be_an_instance_of(Proc) - eval("->(_,_) {}").should be_an_instance_of(Proc) - eval("Proc.new { |_,_| }").should be_an_instance_of(Proc) + lambda { |_,_| }.should.instance_of?(Proc) # rubocop:disable Style/Lambda + -> _,_ {}.should.instance_of?(Proc) + Proc.new { |_,_| }.should.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 @@ -779,29 +787,29 @@ describe "Block-local variables" do end it "can not have the same name as one of the standard parameters" do - -> { eval "[1].each {|foo; foo| }" }.should raise_error(SyntaxError) - -> { eval "[1].each {|foo, bar; glark, bar| }" }.should raise_error(SyntaxError) + -> { eval "[1].each {|foo; foo| }" }.should.raise(SyntaxError) + -> { eval "[1].each {|foo, bar; glark, bar| }" }.should.raise(SyntaxError) end it "can not be prefixed with an asterisk" do - -> { eval "[1].each {|foo; *bar| }" }.should raise_error(SyntaxError) + -> { eval "[1].each {|foo; *bar| }" }.should.raise(SyntaxError) -> do eval "[1].each {|foo, bar; glark, *fnord| }" - end.should raise_error(SyntaxError) + end.should.raise(SyntaxError) end it "can not be prefixed with an ampersand" do - -> { eval "[1].each {|foo; &bar| }" }.should raise_error(SyntaxError) + -> { eval "[1].each {|foo; &bar| }" }.should.raise(SyntaxError) -> do eval "[1].each {|foo, bar; glark, &fnord| }" - end.should raise_error(SyntaxError) + end.should.raise(SyntaxError) end it "can not be assigned default values" do - -> { eval "[1].each {|foo; bar=1| }" }.should raise_error(SyntaxError) + -> { eval "[1].each {|foo; bar=1| }" }.should.raise(SyntaxError) -> do eval "[1].each {|foo, bar; glark, fnord=:fnord| }" - end.should raise_error(SyntaxError) + end.should.raise(SyntaxError) end it "need not be preceded by standard parameters" do @@ -810,8 +818,8 @@ describe "Block-local variables" do end it "only allow a single semi-colon in the parameter list" do - -> { eval "[1].each {|foo; bar; glark| }" }.should raise_error(SyntaxError) - -> { eval "[1].each {|; bar; glark| }" }.should raise_error(SyntaxError) + -> { eval "[1].each {|foo; bar; glark| }" }.should.raise(SyntaxError) + -> { eval "[1].each {|; bar; glark| }" }.should.raise(SyntaxError) end it "override shadowed variables from the outer scope" do @@ -836,21 +844,21 @@ describe "Block-local variables" do end it "are not automatically instantiated in the outer scope" do - defined?(glark).should be_nil + defined?(glark).should == nil [1].each {|;glark| 1} - defined?(glark).should be_nil + defined?(glark).should == nil end it "are automatically instantiated in the block" do [1].each do |;glark| - glark.should be_nil + glark.should == nil end end it "are visible in deeper scopes before initialization" do [1].each {|;glark| [1].each { - defined?(glark).should_not be_nil + defined?(glark).should_not == nil glark = 1 } glark.should == 1 @@ -873,12 +881,18 @@ describe "Post-args" do end.call(1, 2, 3).should == [[], 1, 2, 3] end - it "are required" do + it "are required for a lambda" do -> { -> *a, b do [a, b] end.call - }.should raise_error(ArgumentError) + }.should.raise(ArgumentError) + end + + it "are assigned to nil when not enough arguments are given to a proc" do + proc do |a, *b, c| + [a, b, c] + end.call.should == [nil, [], nil] end describe "with required args" do @@ -943,44 +957,27 @@ 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 + ruby_version_is ""..."3.4" do + it "raises a SyntaxError if using the argument in its default value" 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 + eval "proc { |a=a| a }" + }.should.raise(SyntaxError) end 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 + ruby_version_is "3.4" do + it "is nil if using the argument in its default value" do -> { - @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) + eval "proc { |a=a| a }.call" + }.call.should == nil end end + end - it "calls an existing method with the same name as the argument if explicitly using ()" do - def a; 1; end - proc { |a=a()| a }.call.should == 1 - end + it "calls an existing method with the same name as the argument if explicitly using ()" do + def a; 1; end + proc { |a=a()| a }.call.should == 1 end end @@ -998,3 +995,154 @@ describe "Post-args" do end end end + +# tested more thoroughly in language/delegation_spec.rb +describe "Anonymous block forwarding" do + it "forwards blocks to other method that formally declares anonymous block" do + def b(&); c(&) end + def c(&); yield :non_null end + + 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(SyntaxError) + end + + it "works when it's the only declared parameter" do + def inner; yield end + def block_only(&); inner(&) end + + block_only { 1 }.should == 1 + end + + it "works alongside positional parameters" do + def inner; yield end + def pos(arg1, &); inner(&) end + + pos(:a) { 1 }.should == 1 + end + + it "works alongside positional arguments and splatted keyword arguments" do + def inner; yield end + def pos_kwrest(arg1, **kw, &); inner(&) end + + pos_kwrest(:a, arg: 3) { 1 }.should == 1 + end + + it "works alongside positional arguments and disallowed keyword arguments" do + def inner; yield end + def no_kw(arg1, **nil, &); inner(&) end + + no_kw(:a) { 1 }.should == 1 + end + + 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 + +describe "`it` calls without arguments in a block" do + ruby_version_is ""..."3.4" do + it "emits a deprecation warning" do + -> { + eval "proc { it }" + }.should complain(/warning: `it` calls without arguments will refer to the first block param in Ruby 3.4; use it\(\) or self.it/) + end + + it "emits a deprecation warning if numbered parameters are used" do + -> { + eval "proc { it; _1 }" + }.should complain(/warning: `it` calls without arguments will refer to the first block param in Ruby 3.4; use it\(\) or self.it/) + end + + it "does not emit a deprecation warning when a block has parameters" do + -> { eval "proc { |a, b| it }" }.should_not complain + -> { eval "proc { |*rest| it }" }.should_not complain + -> { eval "proc { |*| it }" }.should_not complain + -> { eval "proc { |a:, b:| it }" }.should_not complain + -> { eval "proc { |**kw| it }" }.should_not complain + -> { eval "proc { |**| it }" }.should_not complain + -> { eval "proc { |&block| it }" }.should_not complain + -> { eval "proc { |&| it }" }.should_not complain + -> { eval "proc { || it }" }.should_not complain + end + + it "does not emit a deprecation warning when `it` calls with arguments" do + -> { eval "proc { it(42) }" }.should_not complain + -> { eval "proc { it 42 }" }.should_not complain + end + + it "does not emit a deprecation warning when `it` calls with a block" do + -> { eval "proc { it {} }" }.should_not complain + end + + it "does not emit a deprecation warning when a local variable inside the block named `it` exists" do + -> { eval "proc { it = 42; it }" }.should_not complain + end + + it "does not emit a deprecation warning when `it` calls with explicit empty arguments list" do + -> { eval "proc { it() }" }.should_not complain + end + + it "calls the method `it` if defined" do + o = Object.new + def o.it + 21 + end + suppress_warning do + o.instance_eval("proc { it * 2 }").call(1).should == 42 + end + end + end + + ruby_version_is "4.1" do + it "works alongside disallowed block argument" do + no_block = eval <<-EOF + proc {|arg1, &nil| arg1} + EOF + + no_block.call(:a).should == :a + -> { no_block.call(:a) {} }.should.raise(ArgumentError, 'no block accepted') + end + end +end + +# Duplicates specs in language/it_parameter_spec.rb +# Need them here to run on Ruby versions prior 3.4 +# TODO: remove when the minimal supported Ruby version is 3.4 +describe "if `it` is defined as a variable" do + it "treats `it` as a captured variable if defined outside of a block" do + it = 5 + proc { it }.call(0).should == 5 + end + + it "treats `it` as a local variable if defined inside of a block" do + proc { it = 5; it }.call(0).should == 5 + end +end + +describe "Block-parameter destructuring" do + it "does not warn about unused inner names in verbose mode" do + -> { + eval <<~RUBY, binding, __FILE__, __LINE__ + 1 + proc { |key, (val1, val2)| [key, val2] } + RUBY + }.should_not complain(verbose: true) + end +end diff --git a/spec/ruby/language/break_spec.rb b/spec/ruby/language/break_spec.rb index 627cb4a071..5c9b8060c3 100644 --- a/spec/ruby/language/break_spec.rb +++ b/spec/ruby/language/break_spec.rb @@ -52,29 +52,29 @@ describe "The break statement in a captured block" do describe "when the invocation of the scope creating the block is still active" do it "raises a LocalJumpError when invoking the block from the scope creating the block" do - -> { @program.break_in_method }.should raise_error(LocalJumpError) + -> { @program.break_in_method }.should.raise(LocalJumpError) ScratchPad.recorded.should == [:a, :xa, :d, :b] end it "raises a LocalJumpError when invoking the block from a method" do - -> { @program.break_in_nested_method }.should raise_error(LocalJumpError) + -> { @program.break_in_nested_method }.should.raise(LocalJumpError) ScratchPad.recorded.should == [:a, :xa, :cc, :aa, :b] end it "raises a LocalJumpError when yielding to the block" do - -> { @program.break_in_yielding_method }.should raise_error(LocalJumpError) + -> { @program.break_in_yielding_method }.should.raise(LocalJumpError) ScratchPad.recorded.should == [:a, :xa, :cc, :aa, :b] end end describe "from a scope that has returned" do it "raises a LocalJumpError when calling the block from a method" do - -> { @program.break_in_method_captured }.should raise_error(LocalJumpError) + -> { @program.break_in_method_captured }.should.raise(LocalJumpError) ScratchPad.recorded.should == [:a, :za, :xa, :zd, :zb] end it "raises a LocalJumpError when yielding to the block" do - -> { @program.break_in_yield_captured }.should raise_error(LocalJumpError) + -> { @program.break_in_yield_captured }.should.raise(LocalJumpError) ScratchPad.recorded.should == [:a, :za, :xa, :zd, :aa, :zb] end end @@ -88,7 +88,7 @@ describe "The break statement in a captured block" do e end end - thread_with_break.value.should be_an_instance_of(LocalJumpError) + thread_with_break.value.should.instance_of?(LocalJumpError) end end end @@ -252,6 +252,25 @@ describe "Break inside a while loop" do end end +describe "The break statement in a method" do + it "is invalid and raises a SyntaxError" do + -> { + eval("def m; break; end") + }.should.raise(SyntaxError) + end +end + +describe "The break statement in a module literal" do + it "is invalid and raises a SyntaxError" do + code = <<~RUBY + module BreakSpecs:ModuleWithBreak + break + end + RUBY + + -> { eval(code) }.should.raise(SyntaxError) + end +end # TODO: Rewrite all the specs from here to the end of the file in the style # above. @@ -369,15 +388,15 @@ describe "Executing break from within a block" do -> do cls2.new.foo.should == 1 - end.should_not raise_error + end.should_not.raise 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 } -> do cls2.new.foo - end.should raise_error(LocalJumpError) + end.should.raise(LocalJumpError) end end diff --git a/spec/ruby/language/case_spec.rb b/spec/ruby/language/case_spec.rb index 410cb9afb1..8355062e44 100644 --- a/spec/ruby/language/case_spec.rb +++ b/spec/ruby/language/case_spec.rb @@ -94,38 +94,38 @@ describe "The 'case'-construct" do it "tests with matching regexps and sets $~ and captures" do case "foo42" when /oo(\d+)/ - $~.should be_kind_of(MatchData) + $~.should.is_a?(MatchData) $1.should == "42" else flunk end - $~.should be_kind_of(MatchData) + $~.should.is_a?(MatchData) $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})/ - $~.should be_kind_of(MatchData) + $~.should.is_a?(MatchData) $1.should == "44" else flunk end - $~.should be_kind_of(MatchData) + $~.should.is_a?(MatchData) $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})/ - $~.should be_kind_of(MatchData) + $~.should.is_a?(MatchData) $1.should == "43" else flunk end - $~.should be_kind_of(MatchData) + $~.should.is_a?(MatchData) $1.should == "43" end @@ -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'] @@ -259,7 +268,7 @@ describe "The 'case'-construct" do true end CODE - }.should raise_error(SyntaxError) + }.should.raise(SyntaxError) end it "raises a SyntaxError when 'else' is used before a 'when' was given" do @@ -271,7 +280,7 @@ describe "The 'case'-construct" do when 4; false end CODE - }.should raise_error(SyntaxError) + }.should.raise(SyntaxError) end it "supports nested case statements" do @@ -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,104 @@ 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 + + ruby_version_is ""..."3.4" do + 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|'when' clause on line \d+ duplicates 'when' clause on line \d+ and is ignored)/, verbose: true) + end + end + + ruby_version_is "3.4" do + it "warns if there are identical when clauses" do + -> { + eval <<~RUBY + case 1 + when 2 + :foo + when 2 + :bar + end + RUBY + }.should complain(/warning: 'when' clause on line \d+ duplicates 'when' clause on line \d+ and is ignored/, verbose: true) + end + 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 +497,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 fa30e22c3a..7ea4857514 100644 --- a/spec/ruby/language/class_spec.rb +++ b/spec/ruby/language/class_spec.rb @@ -10,19 +10,32 @@ end describe "The class keyword" do it "creates a new class with semicolon" do class ClassSpecsKeywordWithSemicolon; end - ClassSpecsKeywordWithSemicolon.should be_an_instance_of(Class) + ClassSpecsKeywordWithSemicolon.should.instance_of?(Class) end it "does not raise a SyntaxError when opening a class without a semicolon" do eval "class ClassSpecsKeywordWithoutSemicolon end" - ClassSpecsKeywordWithoutSemicolon.should be_an_instance_of(Class) + ClassSpecsKeywordWithoutSemicolon.should.instance_of?(Class) + end + + it "can redefine a class when called from a block" do + ClassSpecs::DEFINE_CLASS.call + A.should.instance_of?(Class) + + Object.send(:remove_const, :A) + defined?(A).should == nil + + ClassSpecs::DEFINE_CLASS.call + A.should.instance_of?(Class) + ensure + Object.send(:remove_const, :A) if defined?(::A) end end describe "A class definition" do it "creates a new class" do - ClassSpecs::A.should be_kind_of(Class) - ClassSpecs::A.new.should be_kind_of(ClassSpecs::A) + ClassSpecs::A.should.is_a?(Class) + ClassSpecs::A.new.should.is_a?(ClassSpecs::A) end it "has no class variables" do @@ -33,7 +46,14 @@ describe "A class definition" do -> { class ClassSpecsNumber end - }.should raise_error(TypeError) + }.should.raise(TypeError, /\AClassSpecsNumber is not a class/) + end + + it "raises TypeError if constant given as class name exists and is a Module but not a Class" do + -> { + class ClassSpecs + end + }.should.raise(TypeError, /\AClassSpecs is not a class/) end # test case known to be detecting bugs (JRuby, MRI) @@ -41,19 +61,19 @@ describe "A class definition" do -> { class nil::Foo end - }.should raise_error(TypeError) + }.should.raise(TypeError) end it "raises TypeError if any constant qualifying the class is not a Module" do -> { class ClassSpecs::Number::MyClass end - }.should raise_error(TypeError) + }.should.raise(TypeError) -> { class ClassSpecsNumber::MyClass end - }.should raise_error(TypeError) + }.should.raise(TypeError) end it "inherits from Object by default" do @@ -67,7 +87,7 @@ describe "A class definition" do -> { class SuperclassResetToSubclass < M end - }.should raise_error(TypeError, /superclass mismatch/) + }.should.raise(TypeError, /superclass mismatch/) end end @@ -80,7 +100,7 @@ describe "A class definition" do -> { class SuperclassReopenedBasicObject < BasicObject end - }.should raise_error(TypeError, /superclass mismatch/) + }.should.raise(TypeError, /superclass mismatch/) SuperclassReopenedBasicObject.superclass.should == A end end @@ -95,7 +115,7 @@ describe "A class definition" do -> { class SuperclassReopenedObject < Object end - }.should raise_error(TypeError, /superclass mismatch/) + }.should.raise(TypeError, /superclass mismatch/) SuperclassReopenedObject.superclass.should == A end end @@ -120,7 +140,7 @@ describe "A class definition" do -> { class NoSuperclassSet < String end - }.should raise_error(TypeError, /superclass mismatch/) + }.should.raise(TypeError, /superclass mismatch/) end end @@ -129,7 +149,7 @@ describe "A class definition" do -> { class ShouldNotWork < self; end - }.should raise_error(TypeError) + }.should.raise(TypeError) end it "first evaluates the superclass before checking if the class already exists" do @@ -148,7 +168,7 @@ describe "A class definition" do it "raises a TypeError if inheriting from a metaclass" do obj = mock("metaclass super") meta = obj.singleton_class - -> { class ClassSpecs::MetaclassSuper < meta; end }.should raise_error(TypeError) + -> { class ClassSpecs::MetaclassSuper < meta; end }.should.raise(TypeError) end it "allows the declaration of class variables in the body" do @@ -157,7 +177,7 @@ describe "A class definition" do end it "stores instance variables defined in the class body in the class object" do - ClassSpecs.string_instance_variables(ClassSpecs::B).should include("@ivar") + ClassSpecs.string_instance_variables(ClassSpecs::B).should.include?("@ivar") ClassSpecs::B.instance_variable_get(:@ivar).should == :ivar end @@ -169,9 +189,9 @@ describe "A class definition" do end it "allows the definition of class-level instance variables in a class method" do - ClassSpecs.string_instance_variables(ClassSpecs::C).should_not include("@civ") + ClassSpecs.string_instance_variables(ClassSpecs::C).should_not.include?("@civ") ClassSpecs::C.make_class_instance_variable - ClassSpecs.string_instance_variables(ClassSpecs::C).should include("@civ") + ClassSpecs.string_instance_variables(ClassSpecs::C).should.include?("@civ") ClassSpecs::C.remove_instance_variable :@civ end @@ -258,13 +278,16 @@ describe "A class definition" do AnonWithConstant.name.should == 'AnonWithConstant' klass.get_class_name.should == 'AnonWithConstant' + ensure + Object.send(:remove_const, :AnonWithConstant) end end end describe "An outer class definition" do it "contains the inner classes" do - ClassSpecs::Container.constants.should include(:A, :B) + ClassSpecs::Container.constants.should.include?(:A) + ClassSpecs::Container.constants.should.include?(:B) end end @@ -282,33 +305,23 @@ describe "A class definition extending an object (sclass)" do end end CODE - }.should raise_error(TypeError) + }.should.raise(TypeError) end it "raises a TypeError when trying to extend non-Class" do - error_msg = /superclass must be a Class/ - -> { class TestClass < ""; end }.should raise_error(TypeError, error_msg) - -> { class TestClass < 1; end }.should raise_error(TypeError, error_msg) - -> { class TestClass < :symbol; end }.should raise_error(TypeError, error_msg) - -> { class TestClass < mock('o'); end }.should raise_error(TypeError, error_msg) - -> { class TestClass < Module.new; end }.should raise_error(TypeError, error_msg) - -> { 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 + error_msg = /superclass must be a.* Class/ + -> { class TestClass < ""; end }.should.raise(TypeError, error_msg) + -> { class TestClass < 1; end }.should.raise(TypeError, error_msg) + -> { class TestClass < :symbol; end }.should.raise(TypeError, error_msg) + -> { class TestClass < mock('o'); end }.should.raise(TypeError, error_msg) + -> { class TestClass < Module.new; end }.should.raise(TypeError, error_msg) + -> { class TestClass < BasicObject.new; end }.should.raise(TypeError, error_msg) 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(SyntaxError) end it "can use return to cause the enclosing method to return" do @@ -328,11 +341,11 @@ describe "Reopening a class" do end it "raises a TypeError when superclasses mismatch" do - -> { class ClassSpecs::A < Array; end }.should raise_error(TypeError) + -> { class ClassSpecs::A < Array; end }.should.raise(TypeError) end it "adds new methods to subclasses" do - -> { ClassSpecs::M.m }.should raise_error(NoMethodError) + -> { ClassSpecs::M.m }.should.raise(NoMethodError) class ClassSpecs::L def self.m 1 @@ -341,6 +354,39 @@ describe "Reopening a class" do ClassSpecs::M.m.should == 1 ClassSpecs::L.singleton_class.send(:remove_method, :m) end + + it "does not reopen a class included in Object" do + ruby_exe(<<~RUBY).should == "false" + module IncludedInObject + class IncludedClass + end + end + class Object + include IncludedInObject + end + class IncludedClass + end + print IncludedInObject::IncludedClass == Object::IncludedClass + RUBY + end + + it "does not reopen a class included in non-Object modules" do + ruby_exe(<<~RUBY).should == "false/false" + module Included + module IncludedClass; end + end + module M + include Included + module IncludedClass; end + end + class C + include Included + module IncludedClass; end + end + print Included::IncludedClass == M::IncludedClass, "/", + Included::IncludedClass == C::IncludedClass + RUBY + end end describe "class provides hooks" do diff --git a/spec/ruby/language/class_variable_spec.rb b/spec/ruby/language/class_variable_spec.rb index dffab47a6b..84a7684c6d 100644 --- a/spec/ruby/language/class_variable_spec.rb +++ b/spec/ruby/language/class_variable_spec.rb @@ -30,19 +30,19 @@ describe "A class variable defined in a module" do end it "is not defined in these classes" do - ClassVariablesSpec::ClassC.cvar_defined?.should be_false + ClassVariablesSpec::ClassC.cvar_defined?.should == false end it "is only updated in the module a method defined in the module is used" do ClassVariablesSpec::ClassC.cvar_m = "new value" ClassVariablesSpec::ClassC.cvar_m.should == "new value" - ClassVariablesSpec::ClassC.cvar_defined?.should be_false + ClassVariablesSpec::ClassC.cvar_defined?.should == false end it "is updated in the class when a Method defined in the class is used" do ClassVariablesSpec::ClassC.cvar_c = "new value" - ClassVariablesSpec::ClassC.cvar_defined?.should be_true + ClassVariablesSpec::ClassC.cvar_defined?.should == true end it "can be accessed inside the class using the module methods" do @@ -55,11 +55,11 @@ describe "A class variable defined in a module" do end it "is defined in the extended module" do - ClassVariablesSpec::ModuleN.class_variable_defined?(:@@cvar_n).should be_true + ClassVariablesSpec::ModuleN.class_variable_defined?(:@@cvar_n).should == true end it "is not defined in the extending module" do - ClassVariablesSpec::ModuleO.class_variable_defined?(:@@cvar_n).should be_false + ClassVariablesSpec::ModuleO.class_variable_defined?(:@@cvar_n).should == false end end @@ -70,15 +70,45 @@ describe 'A class variable definition' do c = Class.new(b) b.class_variable_set(:@@cv, :value) - -> { a.class_variable_get(:@@cv) }.should raise_error(NameError) + -> { a.class_variable_get(:@@cv) }.should.raise(NameError) b.class_variable_get(:@@cv).should == :value c.class_variable_get(:@@cv).should == :value # updates the same variable c.class_variable_set(:@@cv, :next) - -> { a.class_variable_get(:@@cv) }.should raise_error(NameError) + -> { a.class_variable_get(:@@cv) }.should.raise(NameError) b.class_variable_get(:@@cv).should == :next 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(RuntimeError, 'class variable access from toplevel') + -> { + eval "@@cvar_toplevel2 = 2" + }.should.raise(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 + 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(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 4d46cf2f84..0880230a36 100644 --- a/spec/ruby/language/constants_spec.rb +++ b/spec/ruby/language/constants_spec.rb @@ -51,8 +51,8 @@ describe "Literal (A::X) constant resolution" do it "does not search the singleton class of the class or module" do -> do ConstantSpecs::ContainerA::ChildA::CS_CONST14 - end.should raise_error(NameError) - -> { ConstantSpecs::CS_CONST14 }.should raise_error(NameError) + end.should.raise(NameError) + -> { ConstantSpecs::CS_CONST14 }.should.raise(NameError) end end @@ -72,39 +72,60 @@ describe "Literal (A::X) constant resolution" do ConstantSpecs::ModuleA::CS_CONST101 = :const101_5 ConstantSpecs::ModuleA::CS_CONST101.should == :const101_5 + ensure + ConstantSpecs::ClassB.send(:remove_const, :CS_CONST101) + ConstantSpecs::ParentB.send(:remove_const, :CS_CONST101) + ConstantSpecs::ContainerB.send(:remove_const, :CS_CONST101) + ConstantSpecs::ContainerB::ChildB.send(:remove_const, :CS_CONST101) + ConstantSpecs::ModuleA.send(:remove_const, :CS_CONST101) end it "searches a module included in the immediate class before the superclass" do ConstantSpecs::ParentB::CS_CONST102 = :const102_1 ConstantSpecs::ModuleF::CS_CONST102 = :const102_2 ConstantSpecs::ContainerB::ChildB::CS_CONST102.should == :const102_2 + ensure + ConstantSpecs::ParentB.send(:remove_const, :CS_CONST102) + ConstantSpecs::ModuleF.send(:remove_const, :CS_CONST102) end it "searches the superclass before a module included in the superclass" do ConstantSpecs::ModuleE::CS_CONST103 = :const103_1 ConstantSpecs::ParentB::CS_CONST103 = :const103_2 ConstantSpecs::ContainerB::ChildB::CS_CONST103.should == :const103_2 + ensure + ConstantSpecs::ModuleE.send(:remove_const, :CS_CONST103) + ConstantSpecs::ParentB.send(:remove_const, :CS_CONST103) end it "searches a module included in the superclass" do ConstantSpecs::ModuleA::CS_CONST104 = :const104_1 ConstantSpecs::ModuleE::CS_CONST104 = :const104_2 ConstantSpecs::ContainerB::ChildB::CS_CONST104.should == :const104_2 + ensure + ConstantSpecs::ModuleA.send(:remove_const, :CS_CONST104) + ConstantSpecs::ModuleE.send(:remove_const, :CS_CONST104) end it "searches the superclass chain" do ConstantSpecs::ModuleA::CS_CONST105 = :const105 ConstantSpecs::ContainerB::ChildB::CS_CONST105.should == :const105 + ensure + ConstantSpecs::ModuleA.send(:remove_const, :CS_CONST105) end it "searches Object if no class or module qualifier is given" do CS_CONST106 = :const106 CS_CONST106.should == :const106 + ensure + Object.send(:remove_const, :CS_CONST106) end it "searches Object if a toplevel qualifier (::X) is given" do ::CS_CONST107 = :const107 ::CS_CONST107.should == :const107 + ensure + Object.send(:remove_const, :CS_CONST107) end it "does not search the singleton class of the class or module" do @@ -114,7 +135,7 @@ describe "Literal (A::X) constant resolution" do -> do ConstantSpecs::ContainerB::ChildB::CS_CONST108 - end.should raise_error(NameError) + end.should.raise(NameError) module ConstantSpecs class << self @@ -122,7 +143,10 @@ describe "Literal (A::X) constant resolution" do end end - -> { ConstantSpecs::CS_CONST108 }.should raise_error(NameError) + -> { ConstantSpecs::CS_CONST108 }.should.raise(NameError) + ensure + ConstantSpecs::ContainerB::ChildB.singleton_class.send(:remove_const, :CS_CONST108) + ConstantSpecs.singleton_class.send(:remove_const, :CS_CONST108) end it "returns the updated value when a constant is reassigned" do @@ -133,55 +157,53 @@ describe "Literal (A::X) constant resolution" do ConstantSpecs::ClassB::CS_CONST109 = :const109_2 }.should complain(/already initialized constant/) ConstantSpecs::ClassB::CS_CONST109.should == :const109_2 + ensure + ConstantSpecs::ClassB.send(:remove_const, :CS_CONST109) end - it "evaluates the right hand side before evaluating a constant path" do + it "evaluates left-to-right" do mod = Module.new mod.module_eval <<-EOC - ConstantSpecsRHS::B = begin - module ConstantSpecsRHS; end - - "hello" - end + order = [] + ConstantSpecsRHS = Module.new + (order << :lhs; ConstantSpecsRHS)::B = (order << :rhs) EOC - mod::ConstantSpecsRHS::B.should == 'hello' + mod::ConstantSpecsRHS::B.should == [:lhs, :rhs] end end it "raises a NameError if no constant is defined in the search path" do - -> { ConstantSpecs::ParentA::CS_CONSTX }.should raise_error(NameError) + -> { ConstantSpecs::ParentA::CS_CONSTX }.should.raise(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(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(NameError, /uninitialized constant <unusable info>::DOES_NOT_EXIST/) end it "sends #const_missing to the original class or module scope" do @@ -193,10 +215,10 @@ describe "Literal (A::X) constant resolution" do end it "raises a TypeError if a non-class or non-module qualifier is given" do - -> { CS_CONST1::CS_CONST }.should raise_error(TypeError) - -> { 1::CS_CONST }.should raise_error(TypeError) - -> { "mod"::CS_CONST }.should raise_error(TypeError) - -> { false::CS_CONST }.should raise_error(TypeError) + -> { CS_CONST1::CS_CONST }.should.raise(TypeError) + -> { 1::CS_CONST }.should.raise(TypeError) + -> { "mod"::CS_CONST }.should.raise(TypeError) + -> { false::CS_CONST }.should.raise(TypeError) end end @@ -242,7 +264,7 @@ describe "Constant resolution within methods" do end it "does not search the lexical scope of the caller" do - -> { ConstantSpecs::ClassA.const16 }.should raise_error(NameError) + -> { ConstantSpecs::ClassA.const16 }.should.raise(NameError) end it "searches the lexical scope of a block" do @@ -257,7 +279,7 @@ describe "Constant resolution within methods" do it "does not search the lexical scope of qualifying modules" do -> do ConstantSpecs::ContainerA::ChildA.const23 - end.should raise_error(NameError) + end.should.raise(NameError) end end @@ -278,6 +300,12 @@ describe "Constant resolution within methods" do ConstantSpecs::ClassB.new.const201.should == :const201_2 ConstantSpecs::ParentB.new.const201.should == :const201_3 ConstantSpecs::ContainerB::ChildB.new.const201.should == :const201_5 + ensure + ConstantSpecs::ModuleA.send(:remove_const, :CS_CONST201) + ConstantSpecs::ClassB.send(:remove_const, :CS_CONST201) + ConstantSpecs::ParentB.send(:remove_const, :CS_CONST201) + ConstantSpecs::ContainerB.send(:remove_const, :CS_CONST201) + ConstantSpecs::ContainerB::ChildB.send(:remove_const, :CS_CONST201) end it "searches a module included in the immediate class before the superclass" do @@ -286,6 +314,9 @@ describe "Constant resolution within methods" do ConstantSpecs::ContainerB::ChildB.const202.should == :const202_1 ConstantSpecs::ContainerB::ChildB.new.const202.should == :const202_1 + ensure + ConstantSpecs::ParentB.send(:remove_const, :CS_CONST202) + ConstantSpecs::ContainerB::ChildB.send(:remove_const, :CS_CONST202) end it "searches the superclass before a module included in the superclass" do @@ -294,6 +325,9 @@ describe "Constant resolution within methods" do ConstantSpecs::ContainerB::ChildB.const203.should == :const203_1 ConstantSpecs::ContainerB::ChildB.new.const203.should == :const203_1 + ensure + ConstantSpecs::ParentB.send(:remove_const, :CS_CONST203) + ConstantSpecs::ModuleE.send(:remove_const, :CS_CONST203) end it "searches a module included in the superclass" do @@ -302,6 +336,9 @@ describe "Constant resolution within methods" do ConstantSpecs::ContainerB::ChildB.const204.should == :const204_1 ConstantSpecs::ContainerB::ChildB.new.const204.should == :const204_1 + ensure + ConstantSpecs::ModuleA.send(:remove_const, :CS_CONST204) + ConstantSpecs::ModuleE.send(:remove_const, :CS_CONST204) end it "searches the superclass chain" do @@ -309,6 +346,8 @@ describe "Constant resolution within methods" do ConstantSpecs::ContainerB::ChildB.const205.should == :const205 ConstantSpecs::ContainerB::ChildB.new.const205.should == :const205 + ensure + ConstantSpecs::ModuleA.send(:remove_const, :CS_CONST205) end it "searches the lexical scope of the method not the receiver's immediate class" do @@ -320,6 +359,9 @@ describe "Constant resolution within methods" do end ConstantSpecs::ContainerB::ChildB.const206.should == :const206_1 + ensure + ConstantSpecs::ContainerB::ChildB.send(:remove_const, :CS_CONST206) + ConstantSpecs::ContainerB::ChildB.singleton_class.send(:remove_const, :CS_CONST206) end it "searches the lexical scope of a singleton method" do @@ -327,12 +369,17 @@ describe "Constant resolution within methods" do ConstantSpecs::ClassB::CS_CONST207 = :const207_2 ConstantSpecs::CS_CONST208.const207.should == :const207_1 + ensure + ConstantSpecs.send(:remove_const, :CS_CONST207) + ConstantSpecs::ClassB.send(:remove_const, :CS_CONST207) end it "does not search the lexical scope of the caller" do ConstantSpecs::ClassB::CS_CONST209 = :const209 - -> { ConstantSpecs::ClassB.const209 }.should raise_error(NameError) + -> { ConstantSpecs::ClassB.const209 }.should.raise(NameError) + ensure + ConstantSpecs::ClassB.send(:remove_const, :CS_CONST209) end it "searches the lexical scope of a block" do @@ -340,6 +387,9 @@ describe "Constant resolution within methods" do ConstantSpecs::ParentB::CS_CONST210 = :const210_2 ConstantSpecs::ClassB.const210.should == :const210_1 + ensure + ConstantSpecs::ClassB.send(:remove_const, :CS_CONST210) + ConstantSpecs::ParentB.send(:remove_const, :CS_CONST210) end it "searches Object as a lexical scope only if Object is explicitly opened" do @@ -350,6 +400,11 @@ describe "Constant resolution within methods" do Object::CS_CONST212 = :const212_2 ConstantSpecs::ParentB::CS_CONST212 = :const212_1 ConstantSpecs::ContainerB::ChildB.const212.should == :const212_1 + ensure + Object.send(:remove_const, :CS_CONST211) + ConstantSpecs::ParentB.send(:remove_const, :CS_CONST211) + Object.send(:remove_const, :CS_CONST212) + ConstantSpecs::ParentB.send(:remove_const, :CS_CONST212) end it "returns the updated value when a constant is reassigned" do @@ -362,6 +417,8 @@ describe "Constant resolution within methods" do }.should complain(/already initialized constant/) ConstantSpecs::ContainerB::ChildB.const213.should == :const213_2 ConstantSpecs::ContainerB::ChildB.new.const213.should == :const213_2 + ensure + ConstantSpecs::ParentB.send(:remove_const, :CS_CONST213) end it "does not search the lexical scope of qualifying modules" do @@ -369,64 +426,20 @@ describe "Constant resolution within methods" do -> do ConstantSpecs::ContainerB::ChildB.const214 - end.should raise_error(NameError) + end.should.raise(NameError) + ensure + ConstantSpecs::ContainerB.send(:remove_const, :CS_CONST214) end end it "raises a NameError if no constant is defined in the search path" do - -> { ConstantSpecs::ParentA.constx }.should raise_error(NameError) + -> { ConstantSpecs::ParentA.constx }.should.raise(NameError) end it "sends #const_missing to the original class or module scope" do ConstantSpecs::ClassA.constx.should == :CS_CONSTX ConstantSpecs::ClassA.new.constx.should == :CS_CONSTX end - - describe "with ||=" do - it "assigns a scoped constant if previously undefined" do - ConstantSpecs.should_not have_constant(:OpAssignUndefined) - module ConstantSpecs - OpAssignUndefined ||= 42 - end - ConstantSpecs::OpAssignUndefined.should == 42 - ConstantSpecs::OpAssignUndefinedOutside ||= 42 - ConstantSpecs::OpAssignUndefinedOutside.should == 42 - ConstantSpecs.send(:remove_const, :OpAssignUndefined) - ConstantSpecs.send(:remove_const, :OpAssignUndefinedOutside) - end - - it "assigns a global constant if previously undefined" do - OpAssignGlobalUndefined ||= 42 - ::OpAssignGlobalUndefinedExplicitScope ||= 42 - OpAssignGlobalUndefined.should == 42 - ::OpAssignGlobalUndefinedExplicitScope.should == 42 - Object.send :remove_const, :OpAssignGlobalUndefined - Object.send :remove_const, :OpAssignGlobalUndefinedExplicitScope - end - - end - - describe "with &&=" do - it "re-assigns a scoped constant if already true" do - module ConstantSpecs - OpAssignTrue = true - end - suppress_warning do - ConstantSpecs::OpAssignTrue &&= 1 - end - ConstantSpecs::OpAssignTrue.should == 1 - ConstantSpecs.send :remove_const, :OpAssignTrue - end - - it "leaves scoped constant if not true" do - module ConstantSpecs - OpAssignFalse = false - end - ConstantSpecs::OpAssignFalse &&= 1 - ConstantSpecs::OpAssignFalse.should == false - ConstantSpecs.send :remove_const, :OpAssignFalse - end - end end describe "Constant resolution within a singleton class (class << obj)" do @@ -443,7 +456,7 @@ describe "Constant resolution within a singleton class (class << obj)" do it "uses its own namespace for nested modules" do a = ConstantSpecs::CS_SINGLETON3[0].x b = ConstantSpecs::CS_SINGLETON3[1].x - a.should_not equal(b) + a.should_not.equal?(b) end it "allows nested modules to have proper resolution" do @@ -456,12 +469,12 @@ end describe "top-level constant lookup" do context "on a class" do it "does not search Object after searching other scopes" do - -> { String::Hash }.should raise_error(NameError) + -> { String::Hash }.should.raise(NameError) end end it "searches Object unsuccessfully when searches on a module" do - -> { Enumerable::Hash }.should raise_error(NameError) + -> { Enumerable::Hash }.should.raise(NameError) end end @@ -475,39 +488,37 @@ describe "Module#private_constant marked constants" do mod.const_set :Foo, false }.should complain(/already initialized constant/) - -> {mod::Foo}.should raise_error(NameError) + -> {mod::Foo}.should.raise(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 it "cannot be accessed from outside the module" do -> do ConstantVisibility::PrivConstModule::PRIVATE_CONSTANT_MODULE - end.should raise_error(NameError) + end.should.raise(NameError) end it "cannot be reopened as a module from scope where constant would be private" do -> do module ConstantVisibility::ModuleContainer::PrivateModule; end - end.should raise_error(NameError) + end.should.raise(NameError) end it "cannot be reopened as a class from scope where constant would be private" do -> do class ConstantVisibility::ModuleContainer::PrivateClass; end - end.should raise_error(NameError) + end.should.raise(NameError) end it "can be reopened as a module where constant is not private" do @@ -518,6 +529,10 @@ describe "Module#private_constant marked constants" do PrivateModule::X.should == 1 end + ensure + module ::ConstantVisibility::ModuleContainer + PrivateModule.send(:remove_const, :X) + end end it "can be reopened as a class where constant is not private" do @@ -528,6 +543,10 @@ describe "Module#private_constant marked constants" do PrivateClass::X.should == 1 end + ensure + module ::ConstantVisibility::ModuleContainer + PrivateClass.send(:remove_const, :X) + end end it "is not defined? with A::B form" do @@ -535,7 +554,7 @@ describe "Module#private_constant marked constants" do end it "can be accessed from the module itself" do - ConstantVisibility::PrivConstModule.private_constant_from_self.should be_true + ConstantVisibility::PrivConstModule.private_constant_from_self.should == true end it "is defined? from the module itself" do @@ -543,7 +562,7 @@ describe "Module#private_constant marked constants" do end it "can be accessed from lexical scope" do - ConstantVisibility::PrivConstModule::Nested.private_constant_from_scope.should be_true + ConstantVisibility::PrivConstModule::Nested.private_constant_from_scope.should == true end it "is defined? from lexical scope" do @@ -551,11 +570,24 @@ describe "Module#private_constant marked constants" do end it "can be accessed from classes that include the module" do - ConstantVisibility::PrivConstModuleChild.new.private_constant_from_include.should be_true + ConstantVisibility::ClassIncludingPrivConstModule.new.private_constant_from_include.should == true + end + + it "can be accessed from modules that include the module" do + ConstantVisibility::ModuleIncludingPrivConstModule.private_constant_from_include.should == true + end + + it "raises a NameError when accessed directly from modules that include the module" do + -> do + ConstantVisibility::ModuleIncludingPrivConstModule.private_constant_self_from_include + end.should.raise(NameError) + -> do + ConstantVisibility::ModuleIncludingPrivConstModule.private_constant_named_from_include + end.should.raise(NameError) end it "is defined? from classes that include the module" do - ConstantVisibility::PrivConstModuleChild.new.defined_from_include.should == "constant" + ConstantVisibility::ClassIncludingPrivConstModule.new.defined_from_include.should == "constant" end end @@ -563,19 +595,19 @@ describe "Module#private_constant marked constants" do it "cannot be accessed from outside the class" do -> do ConstantVisibility::PrivConstClass::PRIVATE_CONSTANT_CLASS - end.should raise_error(NameError) + end.should.raise(NameError) end it "cannot be reopened as a module" do -> do module ConstantVisibility::ClassContainer::PrivateModule; end - end.should raise_error(NameError) + end.should.raise(NameError) end it "cannot be reopened as a class" do -> do class ConstantVisibility::ClassContainer::PrivateClass; end - end.should raise_error(NameError) + end.should.raise(NameError) end it "can be reopened as a module where constant is not private" do @@ -586,6 +618,10 @@ describe "Module#private_constant marked constants" do PrivateModule::X.should == 1 end + ensure + class ::ConstantVisibility::ClassContainer + PrivateModule.send(:remove_const, :X) + end end it "can be reopened as a class where constant is not private" do @@ -596,6 +632,10 @@ describe "Module#private_constant marked constants" do PrivateClass::X.should == 1 end + ensure + class ::ConstantVisibility::ClassContainer + PrivateClass.send(:remove_const, :X) + end end it "is not defined? with A::B form" do @@ -603,7 +643,7 @@ describe "Module#private_constant marked constants" do end it "can be accessed from the class itself" do - ConstantVisibility::PrivConstClass.private_constant_from_self.should be_true + ConstantVisibility::PrivConstClass.private_constant_from_self.should == true end it "is defined? from the class itself" do @@ -611,7 +651,7 @@ describe "Module#private_constant marked constants" do end it "can be accessed from lexical scope" do - ConstantVisibility::PrivConstClass::Nested.private_constant_from_scope.should be_true + ConstantVisibility::PrivConstClass::Nested.private_constant_from_scope.should == true end it "is defined? from lexical scope" do @@ -619,7 +659,7 @@ describe "Module#private_constant marked constants" do end it "can be accessed from subclasses" do - ConstantVisibility::PrivConstClassChild.new.private_constant_from_subclass.should be_true + ConstantVisibility::PrivConstClassChild.new.private_constant_from_subclass.should == true end it "is defined? from subclasses" do @@ -631,7 +671,7 @@ describe "Module#private_constant marked constants" do it "cannot be accessed using ::Const form" do -> do ::PRIVATE_CONSTANT_IN_OBJECT - end.should raise_error(NameError) + end.should.raise(NameError) end it "is not defined? using ::Const form" do @@ -651,14 +691,14 @@ describe "Module#private_constant marked constants" do it "has :receiver and :name attributes" do -> do ConstantVisibility::PrivConstClass::PRIVATE_CONSTANT_CLASS - end.should raise_error(NameError) {|e| + end.should.raise(NameError) {|e| e.receiver.should == ConstantVisibility::PrivConstClass e.name.should == :PRIVATE_CONSTANT_CLASS } -> do ConstantVisibility::PrivConstModule::PRIVATE_CONSTANT_MODULE - end.should raise_error(NameError) {|e| + end.should.raise(NameError) {|e| e.receiver.should == ConstantVisibility::PrivConstModule e.name.should == :PRIVATE_CONSTANT_MODULE } @@ -667,14 +707,14 @@ describe "Module#private_constant marked constants" do it "has the defined class as the :name attribute" do -> do ConstantVisibility::PrivConstClassChild::PRIVATE_CONSTANT_CLASS - end.should raise_error(NameError) {|e| + end.should.raise(NameError) {|e| e.receiver.should == ConstantVisibility::PrivConstClass e.name.should == :PRIVATE_CONSTANT_CLASS } -> do - ConstantVisibility::PrivConstModuleChild::PRIVATE_CONSTANT_MODULE - end.should raise_error(NameError) {|e| + ConstantVisibility::ClassIncludingPrivConstModule::PRIVATE_CONSTANT_MODULE + end.should.raise(NameError) {|e| e.receiver.should == ConstantVisibility::PrivConstModule e.name.should == :PRIVATE_CONSTANT_MODULE } @@ -743,23 +783,27 @@ describe 'Allowed characters' do it 'does not allow not ASCII characters that cannot be upcased or lowercased at the beginning' do -> do Module.new.const_set("થBB", 1) - end.should raise_error(NameError, /wrong constant name/) + end.should.raise(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(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..82c89a0d08 100644 --- a/spec/ruby/language/def_spec.rb +++ b/spec/ruby/language/def_spec.rb @@ -14,11 +14,11 @@ end describe "Defining a method at the top-level" do it "defines it on Object with private visibility by default" do - Object.should have_private_instance_method(:some_toplevel_method, false) + Object.private_instance_methods(false).should.include?(:some_toplevel_method) end it "defines it on Object with public visibility after calling public" do - Object.should have_public_instance_method(:public_toplevel_method, false) + Object.public_instance_methods(false).should.include?(:public_toplevel_method) end end @@ -28,7 +28,7 @@ describe "Defining an 'initialize' method" do def initialize end end - DefInitializeSpec.should have_private_instance_method(:initialize, false) + DefInitializeSpec.private_instance_methods(false).should.include?(:initialize) end end @@ -38,7 +38,7 @@ describe "Defining an 'initialize_copy' method" do def initialize_copy end end - DefInitializeCopySpec.should have_private_instance_method(:initialize_copy, false) + DefInitializeCopySpec.private_instance_methods(false).should.include?(:initialize_copy) end end @@ -48,7 +48,7 @@ describe "Defining an 'initialize_dup' method" do def initialize_dup end end - DefInitializeDupSpec.should have_private_instance_method(:initialize_dup, false) + DefInitializeDupSpec.private_instance_methods(false).should.include?(:initialize_dup) end end @@ -58,7 +58,7 @@ describe "Defining an 'initialize_clone' method" do def initialize_clone end end - DefInitializeCloneSpec.should have_private_instance_method(:initialize_clone, false) + DefInitializeCloneSpec.private_instance_methods(false).should.include?(:initialize_clone) end end @@ -68,7 +68,7 @@ describe "Defining a 'respond_to_missing?' method" do def respond_to_missing? end end - DefRespondToMissingPSpec.should have_private_instance_method(:respond_to_missing?, false) + DefRespondToMissingPSpec.private_instance_methods(false).should.include?(:respond_to_missing?) end end @@ -82,12 +82,12 @@ end describe "An instance method" do it "raises an error with too few arguments" do def foo(a, b); end - -> { foo 1 }.should raise_error(ArgumentError, 'wrong number of arguments (given 1, expected 2)') + -> { foo 1 }.should.raise(ArgumentError, 'wrong number of arguments (given 1, expected 2)') end it "raises an error with too many arguments" do def foo(a); end - -> { foo 1, 2 }.should raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 1)') + -> { foo 1, 2 }.should.raise(ArgumentError, 'wrong number of arguments (given 2, expected 1)') end it "raises FrozenError with the correct class name" do @@ -96,8 +96,9 @@ describe "An instance method" do self.freeze def foo; end end - }.should raise_error(FrozenError) { |e| - e.message.should.start_with? "can't modify frozen module" + }.should.raise(FrozenError) { |e| + msg_class = ruby_version_is("4.0") ? "Module" : "module" + e.message.should == "can't modify frozen #{msg_class}: #{e.receiver}" } -> { @@ -105,8 +106,9 @@ describe "An instance method" do self.freeze def foo; end end - }.should raise_error(FrozenError){ |e| - e.message.should.start_with? "can't modify frozen class" + }.should.raise(FrozenError){ |e| + msg_class = ruby_version_is("4.0") ? "Class" : "class" + e.message.should == "can't modify frozen #{msg_class}: #{e.receiver}" } end end @@ -133,12 +135,12 @@ describe "An instance method definition with a splat" do end it "allows only a single * argument" do - -> { eval 'def foo(a, *b, *c); end' }.should raise_error(SyntaxError) + -> { eval 'def foo(a, *b, *c); end' }.should.raise(SyntaxError) end it "requires the presence of any arguments that precede the *" do def foo(a, b, *c); end - -> { foo 1 }.should raise_error(ArgumentError, 'wrong number of arguments (given 1, expected 2+)') + -> { foo 1 }.should.raise(ArgumentError, 'wrong number of arguments (given 1, expected 2+)') end end @@ -171,7 +173,7 @@ describe "An instance method with a default argument" do def foo(a, b = 2) [a,b] end - -> { foo }.should raise_error(ArgumentError, 'wrong number of arguments (given 0, expected 1..2)') + -> { foo }.should.raise(ArgumentError, 'wrong number of arguments (given 0, expected 1..2)') foo(1).should == [1, 2] end @@ -179,7 +181,7 @@ describe "An instance method with a default argument" do def foo(a, b = 2, *c) [a,b,c] end - -> { foo }.should raise_error(ArgumentError, 'wrong number of arguments (given 0, expected 1+)') + -> { foo }.should.raise(ArgumentError, 'wrong number of arguments (given 0, expected 1+)') foo(1).should == [1,2,[]] end @@ -197,31 +199,24 @@ 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 + ruby_version_is ""..."3.4" do + it "raises a SyntaxError if using the argument in its default value" do -> { eval "def foo(bar = bar) bar end" - }.should complain(/circular argument reference/) - foo.should == nil - foo(2).should == 2 + }.should.raise(SyntaxError) 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 + ruby_version_is "3.4" do + it "is nil if using the argument in its default value" do -> { eval "def foo(bar = bar) bar - end" - }.should raise_error(SyntaxError) + end + foo" + }.call.should == nil end end @@ -255,7 +250,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 @@ -284,26 +279,27 @@ describe "A singleton method definition" do it "raises FrozenError if frozen" do obj = Object.new obj.freeze - -> { def obj.foo; end }.should raise_error(FrozenError) + -> { def obj.foo; end }.should.raise(FrozenError) end it "raises FrozenError with the correct class name" do obj = Object.new obj.freeze - -> { def obj.foo; end }.should raise_error(FrozenError){ |e| - e.message.should.start_with? "can't modify frozen object" - } + msg_class = ruby_version_is("4.0") ? "Object" : "object" + -> { def obj.foo; end }.should.raise(FrozenError, "can't modify frozen #{msg_class}: #{obj}") + obj = Object.new c = obj.singleton_class - -> { def c.foo; end }.should raise_error(FrozenError){ |e| - e.message.should.start_with? "can't modify frozen Class" - } + c.singleton_class.freeze + -> { def c.foo; end }.should.raise(FrozenError, "can't modify frozen Class: #{c}") + + c = Class.new + c.freeze + -> { def c.foo; end }.should.raise(FrozenError, "can't modify frozen Class: #{c}") m = Module.new m.freeze - -> { def m.foo; end }.should raise_error(FrozenError){ |e| - e.message.should.start_with? "can't modify frozen Module" - } + -> { def m.foo; end }.should.raise(FrozenError, "can't modify frozen Module: #{m}") end end @@ -438,7 +434,7 @@ describe "A method definition inside a metaclass scope" do end DefSpecSingleton.a_class_method.should == DefSpecSingleton - -> { Object.a_class_method }.should raise_error(NoMethodError) + -> { Object.a_class_method }.should.raise(NoMethodError) end it "can create a singleton method" do @@ -448,7 +444,7 @@ describe "A method definition inside a metaclass scope" do end obj.a_singleton_method.should == obj - -> { Object.new.a_singleton_method }.should raise_error(NoMethodError) + -> { Object.new.a_singleton_method }.should.raise(NoMethodError) end it "raises FrozenError if frozen" do @@ -456,7 +452,7 @@ describe "A method definition inside a metaclass scope" do obj.freeze class << obj - -> { def foo; end }.should raise_error(FrozenError) + -> { def foo; end }.should.raise(FrozenError) end end end @@ -477,7 +473,7 @@ describe "A nested method definition" do other = DefSpecNested.new other.an_instance_method.should == other - DefSpecNested.should have_instance_method(:an_instance_method) + DefSpecNested.should.method_defined?(:an_instance_method, false) end it "creates a class method when evaluated in a class method" do @@ -492,11 +488,11 @@ describe "A nested method definition" do end end - -> { DefSpecNested.a_class_method }.should raise_error(NoMethodError) + -> { DefSpecNested.a_class_method }.should.raise(NoMethodError) DefSpecNested.create_class_method.should == DefSpecNested DefSpecNested.a_class_method.should == DefSpecNested - -> { Object.a_class_method }.should raise_error(NoMethodError) - -> { DefSpecNested.new.a_class_method }.should raise_error(NoMethodError) + -> { Object.a_class_method }.should.raise(NoMethodError) + -> { DefSpecNested.new.a_class_method }.should.raise(NoMethodError) end it "creates a singleton method when evaluated in the metaclass of an instance" do @@ -514,7 +510,7 @@ describe "A nested method definition" do obj.a_singleton_method.should == obj other = DefSpecNested.new - -> { other.a_singleton_method }.should raise_error(NoMethodError) + -> { other.a_singleton_method }.should.raise(NoMethodError) end it "creates a method in the surrounding context when evaluated in a def expr.method" do @@ -526,11 +522,13 @@ describe "A nested method definition" do end DefSpecNested::TARGET.defs_method - DefSpecNested.should have_instance_method :inherited_method - DefSpecNested::TARGET.should_not have_method :inherited_method + DefSpecNested.should.method_defined?(:inherited_method, false) + DefSpecNested::TARGET.should_not.respond_to? :inherited_method obj = DefSpecNested.new obj.inherited_method.should == obj + ensure + DefSpecNested.send(:remove_const, :TARGET) end # See http://yugui.jp/articles/846#label-3 @@ -547,11 +545,13 @@ describe "A nested method definition" do obj = DefSpecNested::OBJ obj.create_method_in_instance_eval - obj.should have_method :arg_method - obj.should have_method :body_method + obj.should.respond_to? :arg_method + obj.should.respond_to? :body_method - DefSpecNested.should_not have_instance_method :arg_method - DefSpecNested.should_not have_instance_method :body_method + DefSpecNested.should_not.method_defined? :arg_method + DefSpecNested.should_not.method_defined? :body_method + ensure + DefSpecNested.send(:remove_const, :OBJ) end it "creates an instance method inside Class.new" do @@ -569,7 +569,7 @@ describe "A nested method definition" do cls.new.new_def.should == 1 - -> { Object.new.new_def }.should raise_error(NoMethodError) + -> { Object.new.new_def }.should.raise(NoMethodError) end end @@ -585,18 +585,18 @@ describe "A method definition always resets the visibility to public for nested end obj = cls.new - -> { obj.do_def }.should raise_error(NoMethodError, /private/) + -> { obj.do_def }.should.raise(NoMethodError, /private/) obj.send :do_def obj.new_def.should == 1 cls.new.new_def.should == 1 - -> { Object.new.new_def }.should raise_error(NoMethodError) + -> { Object.new.new_def }.should.raise(NoMethodError) end it "at the toplevel" do obj = Object.new - -> { obj.toplevel_define_other_method }.should raise_error(NoMethodError, /private/) + -> { obj.toplevel_define_other_method }.should.raise(NoMethodError, /private/) toplevel_define_other_method nested_method_in_toplevel_method.should == 42 @@ -613,7 +613,7 @@ describe "A method definition inside an instance_eval" do obj.an_instance_eval_method.should == obj other = Object.new - -> { other.an_instance_eval_method }.should raise_error(NoMethodError) + -> { other.an_instance_eval_method }.should.raise(NoMethodError) end it "creates a singleton method when evaluated inside a metaclass" do @@ -626,7 +626,7 @@ describe "A method definition inside an instance_eval" do obj.a_metaclass_eval_method.should == obj other = Object.new - -> { other.a_metaclass_eval_method }.should raise_error(NoMethodError) + -> { other.a_metaclass_eval_method }.should.raise(NoMethodError) end it "creates a class method when the receiver is a class" do @@ -635,7 +635,7 @@ describe "A method definition inside an instance_eval" do end DefSpecNested.an_instance_eval_class_method.should == DefSpecNested - -> { Object.an_instance_eval_class_method }.should raise_error(NoMethodError) + -> { Object.an_instance_eval_class_method }.should.raise(NoMethodError) end it "creates a class method when the receiver is an anonymous class" do @@ -647,7 +647,7 @@ describe "A method definition inside an instance_eval" do end m.klass_method.should == :test - -> { Object.klass_method }.should raise_error(NoMethodError) + -> { Object.klass_method }.should.raise(NoMethodError) end it "creates a class method when instance_eval is within class" do @@ -660,7 +660,7 @@ describe "A method definition inside an instance_eval" do end m.klass_method.should == :test - -> { Object.klass_method }.should raise_error(NoMethodError) + -> { Object.klass_method }.should.raise(NoMethodError) end end @@ -673,7 +673,7 @@ describe "A method definition inside an instance_exec" do end DefSpecNested.an_instance_exec_class_method.should == 1 - -> { Object.an_instance_exec_class_method }.should raise_error(NoMethodError) + -> { Object.an_instance_exec_class_method }.should.raise(NoMethodError) end it "creates a class method when the receiver is an anonymous class" do @@ -687,7 +687,7 @@ describe "A method definition inside an instance_exec" do end m.klass_method.should == 1 - -> { Object.klass_method }.should raise_error(NoMethodError) + -> { Object.klass_method }.should.raise(NoMethodError) end it "creates a class method when instance_exec is within class" do @@ -702,7 +702,7 @@ describe "A method definition inside an instance_exec" do end m.klass_method.should == 2 - -> { Object.klass_method }.should raise_error(NoMethodError) + -> { Object.klass_method }.should.raise(NoMethodError) end end @@ -722,7 +722,7 @@ describe "A method definition in an eval" do other = DefSpecNested.new other.an_eval_instance_method.should == other - -> { Object.new.an_eval_instance_method }.should raise_error(NoMethodError) + -> { Object.new.an_eval_instance_method }.should.raise(NoMethodError) end it "creates a class method" do @@ -738,8 +738,8 @@ describe "A method definition in an eval" do DefSpecNestedB.eval_class_method.should == DefSpecNestedB DefSpecNestedB.an_eval_class_method.should == DefSpecNestedB - -> { Object.an_eval_class_method }.should raise_error(NoMethodError) - -> { DefSpecNestedB.new.an_eval_class_method}.should raise_error(NoMethodError) + -> { Object.an_eval_class_method }.should.raise(NoMethodError) + -> { DefSpecNestedB.new.an_eval_class_method}.should.raise(NoMethodError) end it "creates a singleton method" do @@ -757,7 +757,7 @@ describe "A method definition in an eval" do obj.an_eval_singleton_method.should == obj other = DefSpecNested.new - -> { other.an_eval_singleton_method }.should raise_error(NoMethodError) + -> { other.an_eval_singleton_method }.should.raise(NoMethodError) end end @@ -768,8 +768,8 @@ describe "a method definition that sets more than one default parameter all to t it "assigns them all the same object by default" do foo.should == [{},{},{}] a, b, c = foo - a.should eql(b) - a.should eql(c) + a.should.eql?(b) + a.should.eql?(c) end it "allows the first argument to be given, and sets the rest to null" do @@ -779,11 +779,11 @@ describe "a method definition that sets more than one default parameter all to t it "assigns the parameters different objects across different default calls" do a, _b, _c = foo d, _e, _f = foo - a.should_not equal(d) + a.should_not.equal?(d) end it "only allows overriding the default value of the first such parameter in each set" do - -> { foo(1,2) }.should raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 0..1)') + -> { foo(1,2) }.should.raise(ArgumentError, 'wrong number of arguments (given 2, expected 0..1)') end def bar(a=b=c=1,d=2) @@ -794,7 +794,7 @@ describe "a method definition that sets more than one default parameter all to t bar.should == [1,1,1,2] bar(3).should == [3,nil,nil,2] bar(3,4).should == [3,nil,nil,4] - -> { bar(3,4,5) }.should raise_error(ArgumentError, 'wrong number of arguments (given 3, expected 0..2)') + -> { bar(3,4,5) }.should.raise(ArgumentError, 'wrong number of arguments (given 3, expected 0..2)') end end @@ -809,7 +809,7 @@ describe "The def keyword" do }.call end - DefSpecsLambdaVisibility.should have_private_instance_method("some_method") + DefSpecsLambdaVisibility.private_instance_methods(false).should.include?(:some_method) end end end diff --git a/spec/ruby/language/defined_spec.rb b/spec/ruby/language/defined_spec.rb index f2da8a9293..3fd611d09e 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,13 +48,29 @@ 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 - defined?(defined_specs_undefined_method).should be_nil + defined?(defined_specs_undefined_method).should == nil end it "returns 'method' if the method is defined and private" do @@ -69,23 +90,23 @@ describe "The defined? keyword when called with a method name" do end it "returns nil if the method is private" do - defined?(Object.print).should be_nil + defined?(Object.print).should == nil end it "returns nil if the method is protected" do - defined?(DefinedSpecs::Basic.new.protected_method).should be_nil + defined?(DefinedSpecs::Basic.new.protected_method).should == nil end it "returns nil if the method is not defined" do - defined?(Kernel.defined_specs_undefined_method).should be_nil + defined?(Kernel.defined_specs_undefined_method).should == nil end it "returns nil if the class is not defined" do - defined?(DefinedSpecsUndefined.puts).should be_nil + defined?(DefinedSpecsUndefined.puts).should == nil end it "returns nil if the subclass is not defined" do - defined?(DefinedSpecs::Undefined.puts).should be_nil + defined?(DefinedSpecs::Undefined.puts).should == nil end end @@ -95,13 +116,18 @@ 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 + defined?(obj.an_undefined_method).should == nil end it "returns nil if the variable does not exist" do - defined?(nonexistent_local_variable.some_method).should be_nil + defined?(nonexistent_local_variable.some_method).should == nil end it "calls #respond_to_missing?" do @@ -119,11 +145,11 @@ describe "The defined? keyword when called with a method name" do it "returns nil if the method is not defined" do @defined_specs_obj = DefinedSpecs::Basic.new - defined?(@defined_specs_obj.an_undefined_method).should be_nil + defined?(@defined_specs_obj.an_undefined_method).should == nil end it "returns nil if the variable does not exist" do - defined?(@nonexistent_instance_variable.some_method).should be_nil + defined?(@nonexistent_instance_variable.some_method).should == nil end end @@ -135,22 +161,22 @@ describe "The defined? keyword when called with a method name" do it "returns nil if the method is not defined" do $defined_specs_obj = DefinedSpecs::Basic.new - defined?($defined_specs_obj.an_undefined_method).should be_nil + defined?($defined_specs_obj.an_undefined_method).should == nil end it "returns nil if the variable does not exist" do - defined?($nonexistent_global_variable.some_method).should be_nil + defined?($nonexistent_global_variable.some_method).should == nil end end describe "having a method call as a receiver" do it "returns nil if evaluating the receiver raises an exception" do - defined?(DefinedSpecs.exception_method / 2).should be_nil + defined?(DefinedSpecs.exception_method / 2).should == nil ScratchPad.recorded.should == :defined_specs_exception end it "returns nil if the method is not defined on the object the receiver returns" do - defined?(DefinedSpecs.side_effects / 2).should be_nil + defined?(DefinedSpecs.side_effects / 2).should == nil ScratchPad.recorded.should == :defined_specs_side_effects end @@ -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 @@ -243,15 +403,15 @@ describe "The defined? keyword for an expression" do end it "returns nil for an expression with == and an undefined method" do - defined?(defined_specs_undefined_method == 2).should be_nil + defined?(defined_specs_undefined_method == 2).should == nil end it "returns nil for an expression with != and an undefined method" do - defined?(defined_specs_undefined_method != 2).should be_nil + defined?(defined_specs_undefined_method != 2).should == nil end it "returns nil for an expression with !~ and an undefined method" do - defined?(defined_specs_undefined_method !~ 2).should be_nil + defined?(defined_specs_undefined_method !~ 2).should == nil end it "returns 'method' for an expression with '=='" do @@ -271,25 +431,25 @@ describe "The defined? keyword for an expression" do describe "with logical connectives" do it "returns nil for an expression with '!' and an undefined method" do - defined?(!defined_specs_undefined_method).should be_nil + defined?(!defined_specs_undefined_method).should == nil end it "returns nil for an expression with '!' and an unset class variable" do @result = eval("class singleton_class::A; defined?(!@@doesnt_exist) end", binding, __FILE__, __LINE__) - @result.should be_nil + @result.should == nil end it "returns nil for an expression with 'not' and an undefined method" do - defined?(not defined_specs_undefined_method).should be_nil + defined?(not defined_specs_undefined_method).should == nil end it "returns nil for an expression with 'not' and an unset class variable" do @result = eval("class singleton_class::A; defined?(not @@doesnt_exist) end", binding, __FILE__, __LINE__) - @result.should be_nil + @result.should == nil end it "does not propagate an exception raised by a method in a 'not' expression" do - defined?(not DefinedSpecs.exception_method).should be_nil + defined?(not DefinedSpecs.exception_method).should == nil ScratchPad.recorded.should == :defined_specs_exception end @@ -328,11 +488,11 @@ describe "The defined? keyword for an expression" do end it "returns nil for an expression with '!' and an unset global variable" do - defined?(!$defined_specs_undefined_global_variable).should be_nil + defined?(!$defined_specs_undefined_global_variable).should == nil end it "returns nil for an expression with '!' and an unset instance variable" do - defined?(!@defined_specs_undefined_instance_variable).should be_nil + defined?(!@defined_specs_undefined_instance_variable).should == nil end it "returns 'method' for a 'not' expression with a method" do @@ -345,11 +505,11 @@ describe "The defined? keyword for an expression" do end it "returns nil for an expression with 'not' and an unset global variable" do - defined?(not $defined_specs_undefined_global_variable).should be_nil + defined?(not $defined_specs_undefined_global_variable).should == nil end it "returns nil for an expression with 'not' and an unset instance variable" do - defined?(not @defined_specs_undefined_instance_variable).should be_nil + defined?(not @defined_specs_undefined_instance_variable).should == nil end it "returns 'expression' for an expression with '&&/and' and an undefined method" do @@ -364,12 +524,12 @@ describe "The defined? keyword for an expression" do it "does not call a method in an '&&' expression and returns 'expression'" do defined?(DefinedSpecs.side_effects && true).should == "expression" - ScratchPad.recorded.should be_nil + ScratchPad.recorded.should == nil end it "does not call a method in an 'and' expression and returns 'expression'" do defined?(DefinedSpecs.side_effects and true).should == "expression" - ScratchPad.recorded.should be_nil + ScratchPad.recorded.should == nil end it "returns 'expression' for an expression with '||/or' and an undefined method" do @@ -384,12 +544,12 @@ describe "The defined? keyword for an expression" do it "does not call a method in an '||' expression and returns 'expression'" do defined?(DefinedSpecs.side_effects || true).should == "expression" - ScratchPad.recorded.should be_nil + ScratchPad.recorded.should == nil end it "does not call a method in an 'or' expression and returns 'expression'" do defined?(DefinedSpecs.side_effects or true).should == "expression" - ScratchPad.recorded.should be_nil + ScratchPad.recorded.should == nil end end @@ -412,7 +572,7 @@ describe "The defined? keyword for an expression" do it "does not call the method in the String" do defined?("garble #{DefinedSpecs.dynamic_string}").should == "expression" - ScratchPad.recorded.should be_nil + ScratchPad.recorded.should == nil end end @@ -431,7 +591,7 @@ describe "The defined? keyword for an expression" do it "does not call the method in the Regexp" do defined?(/garble #{DefinedSpecs.dynamic_string}/).should == "expression" - ScratchPad.recorded.should be_nil + ScratchPad.recorded.should == nil end end @@ -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 @@ -478,15 +640,17 @@ describe "The defined? keyword for variables" do end it "returns nil for an instance variable that has not been read" do - DefinedSpecs::Basic.new.instance_variable_undefined.should be_nil + DefinedSpecs::Basic.new.instance_variable_undefined.should == nil end it "returns nil for an instance variable that has been read but not assigned to" do - DefinedSpecs::Basic.new.instance_variable_read.should be_nil + DefinedSpecs::Basic.new.instance_variable_read.should == nil 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 @@ -494,15 +658,17 @@ describe "The defined? keyword for variables" do end it "returns nil for a global variable that has not been read" do - DefinedSpecs::Basic.new.global_variable_undefined.should be_nil + DefinedSpecs::Basic.new.global_variable_undefined.should == nil end it "returns nil for a global variable that has been read but not assigned to" do - DefinedSpecs::Basic.new.global_variable_read.should be_nil + DefinedSpecs::Basic.new.global_variable_read.should == nil 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 @@ -528,27 +694,27 @@ describe "The defined? keyword for variables" do end it "returns nil for $&" do - defined?($&).should be_nil + defined?($&).should == nil end it "returns nil for $`" do - defined?($`).should be_nil + defined?($`).should == nil end it "returns nil for $'" do - defined?($').should be_nil + defined?($').should == nil end it "returns nil for $+" do - defined?($+).should be_nil + defined?($+).should == nil end it "returns nil for any last match global" do - defined?($1).should be_nil - defined?($4).should be_nil - defined?($7).should be_nil - defined?($10).should be_nil - defined?($200).should be_nil + defined?($1).should == nil + defined?($4).should == nil + defined?($7).should == nil + defined?($10).should == nil + defined?($200).should == nil end end @@ -583,10 +749,10 @@ describe "The defined? keyword for variables" do end it "returns nil for non-captures" do - defined?($4).should be_nil - defined?($7).should be_nil - defined?($10).should be_nil - defined?($200).should be_nil + defined?($4).should == nil + defined?($7).should == nil + defined?($10).should == nil + defined?($200).should == nil end end @@ -600,27 +766,27 @@ describe "The defined? keyword for variables" do end it "returns nil for $&" do - defined?($&).should be_nil + defined?($&).should == nil end it "returns nil for $`" do - defined?($`).should be_nil + defined?($`).should == nil end it "returns nil for $'" do - defined?($').should be_nil + defined?($').should == nil end it "returns nil for $+" do - defined?($+).should be_nil + defined?($+).should == nil end it "returns nil for any last match global" do - defined?($1).should be_nil - defined?($4).should be_nil - defined?($7).should be_nil - defined?($10).should be_nil - defined?($200).should be_nil + defined?($1).should == nil + defined?($4).should == nil + defined?($7).should == nil + defined?($10).should == nil + defined?($200).should == nil end end @@ -655,10 +821,10 @@ describe "The defined? keyword for variables" do end it "returns nil for non-captures" do - defined?($4).should be_nil - defined?($7).should be_nil - defined?($10).should be_nil - defined?($200).should be_nil + defined?($4).should == nil + defined?($7).should == nil + defined?($10).should == nil + defined?($200).should == nil end end it "returns 'global-variable' for a global variable that has been assigned" do @@ -666,7 +832,7 @@ describe "The defined? keyword for variables" do end it "returns nil for a class variable that has not been read" do - DefinedSpecs::Basic.new.class_variable_undefined.should be_nil + DefinedSpecs::Basic.new.class_variable_undefined.should == nil end # There is no spec for a class variable that is read before being assigned @@ -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,16 +853,18 @@ 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 - defined?(DefinedSpecsUndefined).should be_nil + defined?(DefinedSpecsUndefined).should == nil end it "does not call Object.const_missing if the constant is not defined" do Object.should_not_receive(:const_missing) - defined?(DefinedSpecsUndefined).should be_nil + defined?(DefinedSpecsUndefined).should == nil end it "returns 'constant' for an included module" do @@ -712,12 +882,12 @@ describe "The defined? keyword for a top-level constant" do end it "returns nil if the constant is not defined" do - defined?(::DefinedSpecsUndefined).should be_nil + defined?(::DefinedSpecsUndefined).should == nil end it "does not call Object.const_missing if the constant is not defined" do Object.should_not_receive(:const_missing) - defined?(::DefinedSpecsUndefined).should be_nil + defined?(::DefinedSpecsUndefined).should == nil end end @@ -727,33 +897,37 @@ describe "The defined? keyword for a scoped constant" do end it "returns nil when the scoped constant is not defined" do - defined?(DefinedSpecs::Undefined).should be_nil + defined?(DefinedSpecs::Undefined).should == nil + end + + it "returns nil when the constant is not defined and the outer module implements .const_missing" do + defined?(DefinedSpecs::ModuleWithConstMissing::Undefined).should == nil end it "does not call .const_missing if the constant is not defined" do DefinedSpecs.should_not_receive(:const_missing) - defined?(DefinedSpecs::UnknownChild).should be_nil + defined?(DefinedSpecs::UnknownChild).should == nil end it "returns nil when an undefined constant is scoped to a defined constant" do - defined?(DefinedSpecs::Child::Undefined).should be_nil + defined?(DefinedSpecs::Child::Undefined).should == nil end it "returns nil when a constant is scoped to an undefined constant" do Object.should_not_receive(:const_missing) - defined?(Undefined::Object).should be_nil + defined?(Undefined::Object).should == nil end it "returns nil when the undefined constant is scoped to an undefined constant" do - defined?(DefinedSpecs::Undefined::Undefined).should be_nil + defined?(DefinedSpecs::Undefined::Undefined).should == nil end it "returns nil when a constant is defined on top-level but not on the module" do - defined?(DefinedSpecs::String).should be_nil + defined?(DefinedSpecs::String).should == nil end it "returns nil when a constant is defined on top-level but not on the class" do - defined?(DefinedSpecs::Basic::String).should be_nil + defined?(DefinedSpecs::Basic::String).should == nil end it "returns 'constant' if the scoped-scoped constant is defined" do @@ -767,15 +941,15 @@ describe "The defined? keyword for a top-level scoped constant" do end it "returns nil when the scoped constant is not defined" do - defined?(::DefinedSpecs::Undefined).should be_nil + defined?(::DefinedSpecs::Undefined).should == nil end it "returns nil when an undefined constant is scoped to a defined constant" do - defined?(::DefinedSpecs::Child::Undefined).should be_nil + defined?(::DefinedSpecs::Child::Undefined).should == nil end it "returns nil when the undefined constant is scoped to an undefined constant" do - defined?(::DefinedSpecs::Undefined::Undefined).should be_nil + defined?(::DefinedSpecs::Undefined::Undefined).should == nil end it "returns 'constant' if the scoped-scoped constant is defined" do @@ -785,7 +959,7 @@ end describe "The defined? keyword for a self-send method call scoped constant" do it "returns nil if the constant is not defined in the scope of the method's value" do - defined?(defined_specs_method::Undefined).should be_nil + defined?(defined_specs_method::Undefined).should == nil end it "returns 'constant' if the constant is defined in the scope of the method's value" do @@ -793,11 +967,11 @@ describe "The defined? keyword for a self-send method call scoped constant" do end it "returns nil if the last constant is not defined in the scope chain" do - defined?(defined_specs_method::Basic::Undefined).should be_nil + defined?(defined_specs_method::Basic::Undefined).should == nil end it "returns nil if the middle constant is not defined in the scope chain" do - defined?(defined_specs_method::Undefined::Undefined).should be_nil + defined?(defined_specs_method::Undefined::Undefined).should == nil end it "returns 'constant' if all the constants in the scope chain are defined" do @@ -807,7 +981,7 @@ end describe "The defined? keyword for a receiver method call scoped constant" do it "returns nil if the constant is not defined in the scope of the method's value" do - defined?(defined_specs_receiver.defined_method::Undefined).should be_nil + defined?(defined_specs_receiver.defined_method::Undefined).should == nil end it "returns 'constant' if the constant is defined in the scope of the method's value" do @@ -815,11 +989,11 @@ describe "The defined? keyword for a receiver method call scoped constant" do end it "returns nil if the last constant is not defined in the scope chain" do - defined?(defined_specs_receiver.defined_method::Basic::Undefined).should be_nil + defined?(defined_specs_receiver.defined_method::Basic::Undefined).should == nil end it "returns nil if the middle constant is not defined in the scope chain" do - defined?(defined_specs_receiver.defined_method::Undefined::Undefined).should be_nil + defined?(defined_specs_receiver.defined_method::Undefined::Undefined).should == nil end it "returns 'constant' if all the constants in the scope chain are defined" do @@ -829,7 +1003,7 @@ end describe "The defined? keyword for a module method call scoped constant" do it "returns nil if the constant is not defined in the scope of the method's value" do - defined?(DefinedSpecs.defined_method::Undefined).should be_nil + defined?(DefinedSpecs.defined_method::Undefined).should == nil end it "returns 'constant' if the constant scoped by the method's value is defined" do @@ -837,11 +1011,11 @@ describe "The defined? keyword for a module method call scoped constant" do end it "returns nil if the last constant in the scope chain is not defined" do - defined?(DefinedSpecs.defined_method::Basic::Undefined).should be_nil + defined?(DefinedSpecs.defined_method::Basic::Undefined).should == nil end it "returns nil if the middle constant in the scope chain is not defined" do - defined?(DefinedSpecs.defined_method::Undefined::Undefined).should be_nil + defined?(DefinedSpecs.defined_method::Undefined::Undefined).should == nil end it "returns 'constant' if all the constants in the scope chain are defined" do @@ -849,11 +1023,11 @@ describe "The defined? keyword for a module method call scoped constant" do end it "returns nil if the outer scope constant in the receiver is not defined" do - defined?(Undefined::DefinedSpecs.defined_method::Basic).should be_nil + defined?(Undefined::DefinedSpecs.defined_method::Basic).should == nil end it "returns nil if the scoped constant in the receiver is not defined" do - defined?(DefinedSpecs::Undefined.defined_method::Basic).should be_nil + defined?(DefinedSpecs::Undefined.defined_method::Basic).should == nil end it "returns 'constant' if all the constants in the receiver are defined" do @@ -874,7 +1048,7 @@ describe "The defined? keyword for a variable scoped constant" do it "returns nil if the instance scoped constant is not defined" do @defined_specs_obj = DefinedSpecs::Basic - defined?(@defined_specs_obj::Undefined).should be_nil + defined?(@defined_specs_obj::Undefined).should == nil end it "returns 'constant' if the constant is defined in the scope of the instance variable" do @@ -884,7 +1058,7 @@ describe "The defined? keyword for a variable scoped constant" do it "returns nil if the global scoped constant is not defined" do $defined_specs_obj = DefinedSpecs::Basic - defined?($defined_specs_obj::Undefined).should be_nil + defined?($defined_specs_obj::Undefined).should == nil end it "returns 'constant' if the constant is defined in the scope of the global variable" do @@ -896,7 +1070,7 @@ describe "The defined? keyword for a variable scoped constant" do eval(<<-END, binding, __FILE__, __LINE__) class singleton_class::A @@defined_specs_obj = DefinedSpecs::Basic - defined?(@@defined_specs_obj::Undefined).should be_nil + defined?(@@defined_specs_obj::Undefined).should == nil end END end @@ -912,7 +1086,7 @@ describe "The defined? keyword for a variable scoped constant" do it "returns nil if the local scoped constant is not defined" do defined_specs_obj = DefinedSpecs::Basic - defined?(defined_specs_obj::Undefined).should be_nil + defined?(defined_specs_obj::Undefined).should == nil end it "returns 'constant' if the constant is defined in the scope of the local variable" do @@ -933,46 +1107,62 @@ end describe "The defined? keyword for yield" do it "returns nil if no block is passed to a method not taking a block parameter" do - DefinedSpecs::Basic.new.no_yield_block.should be_nil + DefinedSpecs::Basic.new.no_yield_block.should == nil end it "returns nil if no block is passed to a method taking a block parameter" do - DefinedSpecs::Basic.new.no_yield_block_parameter.should be_nil + DefinedSpecs::Basic.new.no_yield_block_parameter.should == nil 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 it "returns nil when a superclass undef's the method" do - DefinedSpecs::ClassWithoutMethod.new.test.should be_nil + DefinedSpecs::ClassWithoutMethod.new.test.should == nil end describe "for a method taking no arguments" do it "returns nil when no superclass method exists" do - DefinedSpecs::Super.new.no_super_method_no_args.should be_nil + DefinedSpecs::Super.new.no_super_method_no_args.should == nil end it "returns nil from a block when no superclass method exists" do - DefinedSpecs::Super.new.no_super_method_block_no_args.should be_nil + DefinedSpecs::Super.new.no_super_method_block_no_args.should == nil end it "returns nil from a #define_method when no superclass method exists" do - DefinedSpecs::Super.new.no_super_define_method_no_args.should be_nil + DefinedSpecs::Super.new.no_super_define_method_no_args.should == nil end it "returns nil from a block in a #define_method when no superclass method exists" do - DefinedSpecs::Super.new.no_super_define_method_block_no_args.should be_nil + DefinedSpecs::Super.new.no_super_define_method_block_no_args.should == nil 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 @@ -994,19 +1184,19 @@ describe "The defined? keyword for super" do describe "for a method taking arguments" do it "returns nil when no superclass method exists" do - DefinedSpecs::Super.new.no_super_method_args.should be_nil + DefinedSpecs::Super.new.no_super_method_args.should == nil end it "returns nil from a block when no superclass method exists" do - DefinedSpecs::Super.new.no_super_method_block_args.should be_nil + DefinedSpecs::Super.new.no_super_method_block_args.should == nil end it "returns nil from a #define_method when no superclass method exists" do - DefinedSpecs::Super.new.no_super_define_method_args.should be_nil + DefinedSpecs::Super.new.no_super_define_method_args.should == nil end it "returns nil from a block in a #define_method when no superclass method exists" do - DefinedSpecs::Super.new.no_super_define_method_block_args.should be_nil + DefinedSpecs::Super.new.no_super_define_method_block_args.should == nil end it "returns 'super' when a superclass method exists" do @@ -1041,7 +1231,7 @@ describe "The defined? keyword for instance variables" do end it "returns nil if not assigned" do - defined?(@unassigned_ivar).should be_nil + defined?(@unassigned_ivar).should == nil end end diff --git a/spec/ruby/language/delegation_spec.rb b/spec/ruby/language/delegation_spec.rb index ac7b511f65..3d917993f5 100644 --- a/spec/ruby/language/delegation_spec.rb +++ b/spec/ruby/language/delegation_spec.rb @@ -1,41 +1,162 @@ 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) +# Forwarding anonymous parameters +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}, nil] + end + + it "delegates a block literal" 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 "delegates a block argument" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate(...) + target(...) + end + RUBY + + block = proc {} + a.new.delegate(1, b: 2, &block).should == [[1], {b: 2}, block] + end + + it "delegates with additional arguments" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate(...) + target(:first, :second, ...) + end + RUBY + a.new.delegate(1, b: 2).should == [[:first, :second, 1], {b: 2}, nil] + 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 + end + + a.new.delegate(1, b: 2).should == Range.new([[], {}, nil], 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}, nil] + end + + it "delegates a block literal" 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 + + it "delegates a block argument" 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}] + block = proc {} + a.new.delegate(1, b: 2, &block).should == [[1], {b: 2}, block] + end +end + +describe "delegation with def(*)" do + it "delegates rest" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate(*) + target(*) end + RUBY - it "delegates block" do - a = Class.new(DelegationSpecs::Target) - a.class_eval(<<-RUBY) - def delegate_block(...) - target_block(...) - end - RUBY + a.new.delegate(0, 1).should == [[0, 1], {}, nil] + end + + context "within a block that accepts anonymous rest within a method that accepts anonymous rest" do + it "does not allow delegating rest" do + -> { + eval "def m(*); proc { |*| n(*) } end" + }.should.raise(SyntaxError, /anonymous rest parameter is also used within block/) + end + end +end - a.new.delegate_block(1, b: 2) { |x| x }.should == [{b: 2}, [1]] +describe "delegation with def(**)" do + it "delegates kwargs" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate(**) + target(**) end + RUBY + + a.new.delegate(a: 1) { |x| x }.should == [[], {a: 1}, nil] + end + + context "within a block that accepts anonymous kwargs within a method that accepts anonymous kwargs" do + it "does not allow delegating kwargs" do + -> { + eval "def m(**); proc { |**| n(**) } end" + }.should.raise(SyntaxError, /anonymous keyword rest parameter is also used within block/) + end + end +end + +describe "delegation with def(&)" do + it "delegates an anonymous block parameter" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate(&) + target(&) + end + RUBY + + block = proc {} + a.new.delegate(&block).should == [[], {}, block] + 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 ... - end - RUBY - end - - a.new.delegate(1, b: 2).should == Range.new([[], {}], nil, true) + context "within a block that accepts anonymous block within a method that accepts anonymous block" do + it "does not allow delegating a block" do + -> { + eval "def m(&); proc { |&| n(&) } end" + }.should.raise(SyntaxError, /anonymous block parameter is also used within block/) end end end diff --git a/spec/ruby/language/encoding_spec.rb b/spec/ruby/language/encoding_spec.rb index 5430c9cb98..116f53a77d 100644 --- a/spec/ruby/language/encoding_spec.rb +++ b/spec/ruby/language/encoding_spec.rb @@ -5,7 +5,7 @@ require_relative 'fixtures/coding_utf_8' describe "The __ENCODING__ pseudo-variable" do it "is an instance of Encoding" do - __ENCODING__.should be_kind_of(Encoding) + __ENCODING__.should.is_a?(Encoding) end it "is US-ASCII by default" do @@ -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 @@ -31,6 +31,6 @@ describe "The __ENCODING__ pseudo-variable" do end it "raises a SyntaxError if assigned to" do - -> { eval("__ENCODING__ = 1") }.should raise_error(SyntaxError) + -> { eval("__ENCODING__ = 1") }.should.raise(SyntaxError) end end diff --git a/spec/ruby/language/ensure_spec.rb b/spec/ruby/language/ensure_spec.rb index e94c523e82..04ff0305ab 100644 --- a/spec/ruby/language/ensure_spec.rb +++ b/spec/ruby/language/ensure_spec.rb @@ -6,7 +6,7 @@ describe "An ensure block inside a begin block" do ScratchPad.record [] end - it "is executed when an exception is raised in it's corresponding begin block" do + it "is executed when an exception is raised in its corresponding begin block" do -> { begin ScratchPad << :begin @@ -14,12 +14,12 @@ describe "An ensure block inside a begin block" do ensure ScratchPad << :ensure end - }.should raise_error(EnsureSpec::Error) + }.should.raise(EnsureSpec::Error) ScratchPad.recorded.should == [:begin, :ensure] end - it "is executed when an exception is raised and rescued in it's corresponding begin block" do + it "is executed when an exception is raised and rescued in its corresponding begin block" do begin ScratchPad << :begin raise "An exception occurred!" @@ -32,7 +32,7 @@ describe "An ensure block inside a begin block" do ScratchPad.recorded.should == [:begin, :rescue, :ensure] end - it "is executed even when a symbol is thrown in it's corresponding begin block" do + it "is executed even when a symbol is thrown in its corresponding begin block" do catch(:symbol) do begin ScratchPad << :begin @@ -47,7 +47,7 @@ describe "An ensure block inside a begin block" do ScratchPad.recorded.should == [:begin, :ensure] end - it "is executed when nothing is raised or thrown in it's corresponding begin block" do + it "is executed when nothing is raised or thrown in its corresponding begin block" do begin ScratchPad << :begin rescue @@ -74,9 +74,9 @@ describe "An ensure block inside a begin block" do ensure raise "from ensure" end - }.should raise_error(RuntimeError, "from ensure") do |e| + }.should.raise(RuntimeError, "from ensure") { |e| e.cause.message.should == "from block" - end + } end end @@ -108,7 +108,7 @@ describe "An ensure block inside a method" do end it "is executed when an exception is raised in the method" do - -> { @obj.raise_in_method_with_ensure }.should raise_error(EnsureSpec::Error) + -> { @obj.raise_in_method_with_ensure }.should.raise(EnsureSpec::Error) @obj.executed.should == [:method, :ensure] end @@ -149,13 +149,13 @@ describe "An ensure block inside a method" do it "overrides exception raised in rescue if raises exception itself" do -> { @obj.raise_in_rescue_and_raise_in_ensure - }.should raise_error(RuntimeError, "raised in ensure") + }.should.raise(RuntimeError, "raised in ensure") end it "suppresses exception raised in method if raises exception itself" do -> { @obj.raise_in_method_and_raise_in_ensure - }.should raise_error(RuntimeError, "raised in ensure") + }.should.raise(RuntimeError, "raised in ensure") end end @@ -174,7 +174,7 @@ describe "An ensure block inside a class" do ScratchPad << :ensure end ruby - }.should raise_error(EnsureSpec::Error) + }.should.raise(EnsureSpec::Error) ScratchPad.recorded.should == [:class, :ensure] end @@ -247,7 +247,7 @@ describe "An ensure block inside {} block" do ensure } ruby - }.should raise_error(SyntaxError) + }.should.raise(SyntaxError) end end @@ -256,7 +256,7 @@ describe "An ensure block inside 'do end' block" do ScratchPad.record [] end - it "is executed when an exception is raised in it's corresponding begin block" do + it "is executed when an exception is raised in its corresponding begin block" do -> { eval(<<-ruby).call lambda do @@ -266,12 +266,12 @@ describe "An ensure block inside 'do end' block" do ScratchPad << :ensure end ruby - }.should raise_error(EnsureSpec::Error) + }.should.raise(EnsureSpec::Error) ScratchPad.recorded.should == [:begin, :ensure] end - it "is executed when an exception is raised and rescued in it's corresponding begin block" do + it "is executed when an exception is raised and rescued in its corresponding begin block" do eval(<<-ruby).call lambda do ScratchPad << :begin @@ -286,7 +286,7 @@ describe "An ensure block inside 'do end' block" do ScratchPad.recorded.should == [:begin, :rescue, :ensure] end - it "is executed even when a symbol is thrown in it's corresponding begin block" do + it "is executed even when a symbol is thrown in its corresponding begin block" do catch(:symbol) do eval(<<-ruby).call lambda do @@ -303,7 +303,7 @@ describe "An ensure block inside 'do end' block" do ScratchPad.recorded.should == [:begin, :ensure] end - it "is executed when nothing is raised or thrown in it's corresponding begin block" do + it "is executed when nothing is raised or thrown in its corresponding begin block" do eval(<<-ruby).call lambda do ScratchPad << :begin @@ -328,4 +328,19 @@ 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[0].should =~ /#{__FILE__}:#{line-3}:in 'foo'/ + foo[1].should =~ /#{__FILE__}:#{line+2}:in 'block/ + end + end end diff --git a/spec/ruby/language/execution_spec.rb b/spec/ruby/language/execution_spec.rb index 4e0310946d..51bcde62e8 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}` # rubocop:disable Lint/LiteralInInterpolation + 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}} # rubocop:disable Lint/LiteralInInterpolation + 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..dd89ce2385 100644 --- a/spec/ruby/language/file_spec.rb +++ b/spec/ruby/language/file_spec.rb @@ -4,26 +4,18 @@ require_relative 'shared/__FILE__' describe "The __FILE__ pseudo-variable" do it "raises a SyntaxError if assigned to" do - -> { eval("__FILE__ = 1") }.should raise_error(SyntaxError) + -> { eval("__FILE__ = 1") }.should.raise(SyntaxError) end - it "equals (eval) inside an eval" do - eval("__FILE__").should == "(eval)" + it "equals (eval at __FILE__:__LINE__) inside an eval" do + eval("__FILE__").should == "(eval at #{__FILE__}:#{__LINE__})" end end -describe "The __FILE__ pseudo-variable" do - it_behaves_like :language___FILE__, :require, CodeLoadingSpecs::Method.new -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/class_with_class_variable.rb b/spec/ruby/language/fixtures/class_with_class_variable.rb new file mode 100644 index 0000000000..0b07f16d30 --- /dev/null +++ b/spec/ruby/language/fixtures/class_with_class_variable.rb @@ -0,0 +1,9 @@ +module StringSpecs + class ClassWithClassVariable + @@a = "xxx" + + def foo + "#@@a" + end + end +end diff --git a/spec/ruby/language/fixtures/constant_visibility.rb b/spec/ruby/language/fixtures/constant_visibility.rb index 022554430e..af38b2d8f2 100644 --- a/spec/ruby/language/fixtures/constant_visibility.rb +++ b/spec/ruby/language/fixtures/constant_visibility.rb @@ -65,7 +65,7 @@ module ConstantVisibility end end - class PrivConstModuleChild + class ClassIncludingPrivConstModule include PrivConstModule def private_constant_from_include @@ -77,6 +77,22 @@ module ConstantVisibility end end + module ModuleIncludingPrivConstModule + include PrivConstModule + + def self.private_constant_from_include + PRIVATE_CONSTANT_MODULE + end + + def self.private_constant_self_from_include + self::PRIVATE_CONSTANT_MODULE + end + + def self.private_constant_named_from_include + PrivConstModule::PRIVATE_CONSTANT_MODULE + end + end + class PrivConstClassChild < PrivConstClass def private_constant_from_subclass PRIVATE_CONSTANT_CLASS diff --git a/spec/ruby/language/fixtures/defined.rb b/spec/ruby/language/fixtures/defined.rb index 8b6004c19f..3761cfa5bd 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 @@ -282,6 +285,12 @@ module DefinedSpecs end end + module ModuleWithConstMissing + def self.const_missing(const) + const + end + end + class SuperWithIntermediateModules include IntermediateModule1 include IntermediateModule2 diff --git a/spec/ruby/language/fixtures/delegation.rb b/spec/ruby/language/fixtures/delegation.rb index 527d928390..da2b024791 100644 --- a/spec/ruby/language/fixtures/delegation.rb +++ b/spec/ruby/language/fixtures/delegation.rb @@ -1,7 +1,7 @@ module DelegationSpecs class Target - def target(*args, **kwargs) - [args, kwargs] + def target(*args, **kwargs, &block) + [args, kwargs, block] end def target_block(*args, **kwargs) diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_across_files.rb b/spec/ruby/language/fixtures/freeze_magic_comment_across_files.rb index 3aed2f29b6..f3ef666a3c 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_across_files.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_across_files.rb @@ -2,4 +2,5 @@ require_relative 'freeze_magic_comment_required' -p "abc".object_id == $second_literal_id +p "abc".equal?($second_literal) +$second_literal = nil diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_across_files_diff_enc.rb b/spec/ruby/language/fixtures/freeze_magic_comment_across_files_diff_enc.rb index 53ef959970..e9ca35e7c8 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_across_files_diff_enc.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_across_files_diff_enc.rb @@ -2,4 +2,5 @@ require_relative 'freeze_magic_comment_required_diff_enc' -p "abc".object_id != $second_literal_id +p !"abc".equal?($second_literal) +$second_literal = nil diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_across_files_no_comment.rb b/spec/ruby/language/fixtures/freeze_magic_comment_across_files_no_comment.rb index fc6cd5bf82..c9eaab46a2 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_across_files_no_comment.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_across_files_no_comment.rb @@ -2,4 +2,5 @@ require_relative 'freeze_magic_comment_required_no_comment' -p "abc".object_id != $second_literal_id +p !"abc".equal?($second_literal) +$second_literal = nil diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_one_literal.rb b/spec/ruby/language/fixtures/freeze_magic_comment_one_literal.rb index d35905b332..c175b2b7a2 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_one_literal.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_one_literal.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true -ids = Array.new(2) { "abc".object_id } -p ids.first == ids.last +objs = Array.new(2) { "abc" } +p objs.first.equal?(objs.last) diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_required.rb b/spec/ruby/language/fixtures/freeze_magic_comment_required.rb index a4ff4459b1..f75acb2ce3 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_required.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_required.rb @@ -1,3 +1,3 @@ # frozen_string_literal: true -$second_literal_id = "abc".object_id +$second_literal = "abc" 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..739e96e99a 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_required_no_comment.rb b/spec/ruby/language/fixtures/freeze_magic_comment_required_no_comment.rb index e09232a5f4..6fbe175b42 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_required_no_comment.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_required_no_comment.rb @@ -1 +1 @@ -$second_literal_id = "abc".object_id +$second_literal = "abc" 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/module.rb b/spec/ruby/language/fixtures/module.rb index 33d323846e..75eee77791 100644 --- a/spec/ruby/language/fixtures/module.rb +++ b/spec/ruby/language/fixtures/module.rb @@ -12,13 +12,4 @@ module ModuleSpecs module Anonymous end - - module IncludedInObject - module IncludedModuleSpecs - end - end -end - -class Object - include ModuleSpecs::IncludedInObject end diff --git a/spec/ruby/language/fixtures/private.rb b/spec/ruby/language/fixtures/private.rb index 96f73cea3f..da3e0a97f9 100644 --- a/spec/ruby/language/fixtures/private.rb +++ b/spec/ruby/language/fixtures/private.rb @@ -43,17 +43,17 @@ module Private end end - class E - include D - end - - class G - def foo - "foo" - end - end - - class H < A - private :foo - end + class E + include D + end + + class G + def foo + "foo" + end + end + + class H < A + private :foo + end end 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/return.rb b/spec/ruby/language/fixtures/return.rb index 0414c356e8..f6b143f3fa 100644 --- a/spec/ruby/language/fixtures/return.rb +++ b/spec/ruby/language/fixtures/return.rb @@ -101,18 +101,14 @@ module ReturnSpecs # return value will go into val before we run the ensure. # # If lamb's return keeps unwinding incorrectly, val will still - # have it's old value. + # have its old value. # # We can therefore use val to figure out what happened. begin val = foo() ensure - if val != :good - return :bad - end + return val end - - return val end end diff --git a/spec/ruby/language/fixtures/send.rb b/spec/ruby/language/fixtures/send.rb index 918241e171..4787abee5c 100644 --- a/spec/ruby/language/fixtures/send.rb +++ b/spec/ruby/language/fixtures/send.rb @@ -43,9 +43,9 @@ module LangSendSpecs attr_writer :foo private :foo= - def call_self_foo_equals(value) - self.foo = value - end + def call_self_foo_equals(value) + self.foo = value + end def call_self_foo_equals_masgn(value) a, self.foo = 1, value @@ -81,6 +81,16 @@ module LangSendSpecs end end + class RawToProc + def initialize(to_proc) + @to_proc = to_proc + end + + def to_proc + @to_proc + end + end + class ToAry def initialize(obj) @obj = obj diff --git a/spec/ruby/language/fixtures/squiggly_heredoc.rb b/spec/ruby/language/fixtures/squiggly_heredoc.rb index d223966b93..984a629e5b 100644 --- a/spec/ruby/language/fixtures/squiggly_heredoc.rb +++ b/spec/ruby/language/fixtures/squiggly_heredoc.rb @@ -29,6 +29,14 @@ module SquigglyHeredocSpecs HERE end + def self.backslash + <<~HERE + a + b\ + c + HERE + end + def self.least_indented_on_the_first_line <<~HERE a diff --git a/spec/ruby/language/fixtures/super.rb b/spec/ruby/language/fixtures/super.rb index 218f1e4970..b6d4218b03 100644 --- a/spec/ruby/language/fixtures/super.rb +++ b/spec/ruby/language/fixtures/super.rb @@ -266,7 +266,7 @@ module SuperSpecs # Use this so that we can see collect all supers that we see. # One bug that arises is that we call Alias2#name from Alias2#name - # as it's superclass. In that case, either we get a runaway recursion + # as its superclass. In that case, either we get a runaway recursion # super OR we get the return value being [:alias2, :alias2, :alias1] # rather than [:alias2, :alias1]. # @@ -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/for_spec.rb b/spec/ruby/language/for_spec.rb index 0ad5ea88af..b0f3aef405 100644 --- a/spec/ruby/language/for_spec.rb +++ b/spec/ruby/language/for_spec.rb @@ -19,6 +19,27 @@ describe "The for expression" do end end + it "iterates over a list of arrays and destructures with an empty splat" do + for i, * in [[1,2]] + i.should == 1 + end + end + + it "iterates over a list of arrays and destructures with a splat" do + for i, *j in [[1,2]] + i.should == 1 + j.should == [2] + end + end + + it "iterates over a list of arrays and destructures with a splat and additional targets" do + for i, *j, k in [[1,2,3,4]] + i.should == 1 + j.should == [2,3] + k.should == 4 + end + end + it "iterates over an Hash passing each key-value pair to the block" do k = 0 l = 0 @@ -81,6 +102,85 @@ describe "The for expression" do end end + it "allows a global variable as an iterator name" do + old_global_var = $var + m = [1,2,3] + n = 0 + for $var in m + n += 1 + end + $var.should == 3 + n.should == 3 + $var = old_global_var + end + + it "allows an attribute as an iterator name" do + class OFor + attr_accessor :target + end + + ofor = OFor.new + m = [1,2,3] + n = 0 + for ofor.target in m + n += 1 + end + ofor.target.should == 3 + n.should == 3 + end + + it "allows an attribute with safe navigation as an iterator name" do + class OFor + attr_accessor :target + end + + ofor = OFor.new + m = [1,2,3] + n = 0 + eval <<~RUBY + for ofor&.target in m + n += 1 + end + RUBY + ofor.target.should == 3 + n.should == 3 + end + + it "allows an attribute with safe navigation on a nil base as an iterator name" do + ofor = nil + m = [1,2,3] + n = 0 + eval <<~RUBY + for ofor&.target in m + n += 1 + end + RUBY + ofor.should == nil + n.should == 3 + end + + it "allows an array index writer as an iterator name" do + arr = [:a, :b, :c] + m = [1,2,3] + n = 0 + for arr[1] in m + n += 1 + end + arr.should == [:a, 3, :c] + n.should == 3 + end + + it "allows a hash index writer as an iterator name" do + hash = { a: 10, b: 20, c: 30 } + m = [1,2,3] + n = 0 + for hash[:b] in m + n += 1 + end + hash.should == { a: 10, b: 3, c: 30 } + n.should == 3 + end + # 1.9 behaviour verified by nobu in # http://redmine.ruby-lang.org/issues/show/2053 it "yields only as many values as there are arguments" do @@ -115,7 +215,15 @@ describe "The for expression" do j.should == 6 end - it "executes code in containing variable scope" do + it "declares iteration variables in the surrounding variable scope" do + for a, b in [[1,2]] + end + + a.should == 1 + b.should == 2 + end + + it "declares variables in the body in the surrounding variable scope" do for i in 1..2 a = 123 end @@ -123,7 +231,7 @@ describe "The for expression" do a.should == 123 end - it "executes code in containing variable scope with 'do'" do + it "declares variables in the body in the surrounding variable scope with 'do'" do for i in 1..2 do a = 123 end @@ -131,6 +239,96 @@ describe "The for expression" do a.should == 123 end + it "declares variables inside a block as normal" do + for i in 1..2 do + proc { + inside_proc = 42 + }.call + end + local_variables.should == [:i] + end + + it "declares variables inside a lambda as normal" do + for i in 1..2 do + -> { + inside_proc = 42 + }.call + end + local_variables.should == [:i] + end + + it "can be nested" do + for a in [6] + for b in [7] + c = a * b + end + end + local_variables.sort.should == [:a, :b, :c] + c.should == 42 + end + + it "can be nested with blocks in between" do + # This is an edge case spec for Ruby implementations which have + # their own runtime scope per for loop body (like YARV and TruffleRuby) + for a in [1] + a1 = a + a1.should == a + for b in [2] + b1 = b + a1.should == a + b1.should == b + proc { + inside_proc = 42 + + a1.should == a + b1.should == b + inside_proc.should == 42 + + for c in [3].map { |enum_var| + a1.should == a + b1.should == b + inside_proc.should == 42 + enum_var + } + c1 = c + + a1.should == a + b1.should == b + c1.should == c + inside_proc.should == 42 + + for d in [4] + d1 = d + + a1.should == a + b1.should == b + c1.should == c + d1.should == d + inside_proc.should == 42 + end + end + local_variables.sort.should == [:a, :a1, :b, :b1, :c, :c1, :d, :d1, :inside_proc] + }.call + end + end + local_variables.sort.should == [:a, :a1, :b, :b1] + end + + it "can be nested with forward arguments" do + def bar(*args) + args + end + + def foo(...) + for a in [1] + r = bar(...) + end + r + end + + foo(2, 3).should == [2, 3] + end + it "does not try to access variables outside the method" do ForSpecs::ForInClassMethod.foo.should == [:bar, :baz] ForSpecs::ForInClassMethod::READER.call.should == :same_variable_set_outside diff --git a/spec/ruby/language/hash_spec.rb b/spec/ruby/language/hash_spec.rb index 630c9d2364..7a4a2e37c9 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" @@ -48,6 +48,24 @@ describe "Hash literal" do }.should complain(/key :foo is duplicated|duplicated key/) @h.keys.size.should == 1 @h.should == {foo: :foo} + -> { + @h = eval "{%q{a} => :bar, %q{a} => :foo}" + }.should complain(/key "a" is duplicated|duplicated key/) + @h.keys.size.should == 1 + @h.should == {%q{a} => :foo} + -> { + @h = eval "{1000 => :bar, 1000 => :foo}" + }.should complain(/key 1000 is duplicated|duplicated key/) + @h.keys.size.should == 1 + @h.should == {1000 => :foo} + end + + 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 it "accepts a hanging comma" do @@ -57,13 +75,37 @@ describe "Hash literal" do end it "recognizes '=' at the end of the key" do - eval("{:a==>1}").should == {:"a=" => 1} - eval("{:a= =>1}").should == {:"a=" => 1} - eval("{:a= => 1}").should == {:"a=" => 1} + {:a==>1}.should == {:"a=" => 1} + {:a= =>1}.should == {:"a=" => 1} + {:a= => 1}.should == {:"a=" => 1} end it "with '==>' in the middle raises SyntaxError" do - -> { eval("{:a ==> 1}") }.should raise_error(SyntaxError) + -> { eval("{:a ==> 1}") }.should.raise(SyntaxError) + end + + it "recognizes '!' at the end of the key" do + {:a! =>1}.should == {:"a!" => 1} + {:a! => 1}.should == {:"a!" => 1} + + {a!:1}.should == {:"a!" => 1} + {a!: 1}.should == {:"a!" => 1} + end + + it "raises a SyntaxError if there is no space between `!` and `=>`" do + -> { eval("{:a!=> 1}") }.should.raise(SyntaxError) + end + + it "recognizes '?' at the end of the key" do + {:a? =>1}.should == {:"a?" => 1} + {:a? => 1}.should == {:"a?" => 1} + + {a?:1}.should == {:"a?" => 1} + {a?: 1}.should == {:"a?" => 1} + end + + it "raises a SyntaxError if there is no space between `?` and `=>`" do + -> { eval("{:a?=> 1}") }.should.raise(SyntaxError) end it "constructs a new hash with the given elements" do @@ -85,7 +127,7 @@ describe "Hash literal" do it "accepts mixed 'key: value', 'key => value' and '\"key\"': value' syntax" do h = {:a => 1, :b => 2, "c" => 3, :d => 4} - eval('{a: 1, :b => 2, "c" => 3, "d": 4}').should == h + {a: 1, :b => 2, "c" => 3, "d": 4}.should == h end it "expands an '**{}' element into the containing Hash literal initialization" do @@ -107,11 +149,55 @@ 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 + ruby_version_is ""..."3.4" do + it "does not expand nil using ** into {} and raises TypeError" do + h = nil + -> { {a: 1, **h} }.should.raise(TypeError, "no implicit conversion of nil into Hash") + + -> { {a: 1, **nil} }.should.raise(TypeError, "no implicit conversion of nil into Hash") + end + end + + ruby_version_is "3.4" do + it "expands nil using ** into {}" do + h = nil + {**h}.should == {} + {a: 1, **h}.should == {a: 1} + + {**nil}.should == {} + {a: 1, **nil}.should == {a: 1} + end + + it "expands nil using ** into {} and provides a copy to the callable" do + ScratchPad.record [] + insert = -> key, **kw do + kw[key] = 1 + ScratchPad << kw + end + insert.call(:foo, **nil) + insert.call(:bar, **nil) + ScratchPad.recorded.should == [{ foo: 1 }, { bar: 1 }] + end + end + + 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 @@ -128,25 +214,22 @@ 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 obj = mock("hash splat") obj.should_receive(:to_hash).and_return(obj) - -> { {**obj} }.should raise_error(TypeError) + -> { {**obj} }.should.raise(TypeError) + end + + it "raises a TypeError if the object does not respond to #to_hash" do + obj = 42 + -> { {**obj} }.should.raise(TypeError) + -> { {a: 1, **obj} }.should.raise(TypeError) end it "does not change encoding of literal string keys during creation" do @@ -160,4 +243,91 @@ 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(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(SyntaxError, /invalid symbol/) + ScratchPad.recorded.should == [] + end + end +end + +describe "The ** operator" do + it "makes a copy when calling a method taking a keyword rest argument" do + def m(**h) + h.delete(:one); h + end + + h = { one: 1, two: 2 } + m(**h).should == { two: 2 } + m(**h).should_not.equal?(h) + h.should == { one: 1, two: 2 } + end + + it "makes a copy when calling a method taking a positional Hash" do + def m(h) + h.delete(:one); h + end + + h = { one: 1, two: 2 } + m(**h).should == { two: 2 } + m(**h).should_not.equal?(h) + h.should == { one: 1, two: 2 } + end + + describe "hash with omitted value" do + it "accepts short notation 'key' for 'key: value' syntax" do + a, b, c = 1, 2, 3 + h = {a:} + {a: 1}.should == h + h = {a:, b:, c:} + {a: 1, b: 2, c: 3}.should == h + end + + it "ignores hanging comma on short notation" do + a, b, c = 1, 2, 3 + h = {a:, b:, c:,} + {a: 1, b: 2, c: 3}.should == h + end + + it "accepts mixed syntax" do + a, e = 1, 5 + h = {a:, b: 2, "c" => 3, :d => 4, e:} + {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 + + it "raises a SyntaxError when the hash key ends with `!`" do + -> { eval("{a!:}") }.should.raise(SyntaxError, /identifier a! is not valid to get/) + end + + it "raises a SyntaxError when the hash key ends with `?`" do + -> { eval("{a?:}") }.should.raise(SyntaxError, /identifier a\? is not valid to get/) + end + end end diff --git a/spec/ruby/language/heredoc_spec.rb b/spec/ruby/language/heredoc_spec.rb index 95df8457e4..535f18cba8 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(SyntaxError) end it "allows HEREDOC with <<~'identifier', allowing to indent identifier and content" do @@ -100,6 +90,11 @@ HERE SquigglyHeredocSpecs.singlequoted.should == "singlequoted \#{\"interpolated\"}\n" end + it "allows HEREDOC with <<~'identifier', no interpolation, with backslash" do + require_relative 'fixtures/squiggly_heredoc' + SquigglyHeredocSpecs.backslash.should == "a\nbc\n" + end + it "selects the least-indented line and removes its indentation from all the lines" do require_relative 'fixtures/squiggly_heredoc' SquigglyHeredocSpecs.least_indented_on_the_first_line.should == "a\n b\n c\n" @@ -111,4 +106,14 @@ HERE SquigglyHeredocSpecs.least_indented_on_the_first_line_single.should == "a\n b\n c\n" SquigglyHeredocSpecs.least_indented_on_the_last_line_single.should == " a\n b\nc\n" end + + it "reports line numbers inside HEREDOC with method call" do + -> { + <<-HERE.chomp + a + b + #{c} + HERE + }.should.raise(NameError) { |e| e.backtrace[0].should.start_with?("#{__FILE__}:#{__LINE__ - 2}") } + end end diff --git a/spec/ruby/language/if_spec.rb b/spec/ruby/language/if_spec.rb index d1d95c1607..53fcb853d5 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(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/it_parameter_spec.rb b/spec/ruby/language/it_parameter_spec.rb new file mode 100644 index 0000000000..6ba67cbb4f --- /dev/null +++ b/spec/ruby/language/it_parameter_spec.rb @@ -0,0 +1,108 @@ +require_relative '../spec_helper' + +ruby_version_is "3.4" do + eval <<-RUBY # use eval to avoid warnings on Ruby 3.3 + describe "The `it` parameter" do + it "provides it in a block" do + -> { it }.call("a").should == "a" + proc { it }.call("a").should == "a" + lambda { it }.call("a").should == "a" + ["a"].map { it }.should == ["a"] + end + + it "assigns nil to not passed parameters" do + proc { it }.call().should == nil + end + + it "can be used in both outer and nested blocks at the same time" do + -> { it + -> { it * it }.call(2) }.call(3).should == 7 + end + + it "can be reassigned to act as a local variable" do + proc { tmp = it; it = tmp * 2; it }.call(21).should == 42 + end + + it "is a regular local variable if there is already a 'it' local variable" do + it = 0 + proc { it }.call("a").should == 0 + end + + it "is a regular local variable if there is a method `it` defined" do + o = Object.new + def o.it + 21 + end + + o.instance_eval("proc { it * 2 }").call(1).should == 2 + end + + it "is not shadowed by an reassignment in a block" do + a = nil + proc { a = it; it = 42 }.call(0) + a.should == 0 # if `it` were shadowed its value would be nil + end + + it "raises SyntaxError when block parameters are specified explicitly" do + -> { eval("-> () { it }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + -> { eval("-> (x) { it }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + + -> { eval("proc { || it }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + -> { eval("proc { |x| it }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + + -> { eval("lambda { || it }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + -> { eval("lambda { |x| it }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + + -> { eval("['a'].map { || it }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + -> { eval("['a'].map { |x| it }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + end + + it "cannot be mixed with numbered parameters" do + -> { + eval("proc { it + _1 }") + }.should.raise(SyntaxError, /numbered parameters are not allowed when 'it' is already used|'it' is already used in/) + + -> { + eval("proc { _1 + it }") + }.should.raise(SyntaxError, /numbered parameter is already used in|'it' is not allowed when a numbered parameter is already used/) + end + + it "affects block arity" do + -> {}.arity.should == 0 + -> { it }.arity.should == 1 + end + + it "affects block parameters" do + -> { it }.parameters.should == [[:req]] + + ruby_version_is ""..."4.0" do + proc { it }.parameters.should == [[:opt, nil]] + end + ruby_version_is "4.0" do + proc { it }.parameters.should == [[:opt]] + end + end + + it "does not affect binding local variables" do + -> { it; binding.local_variables }.call("a").should == [] + end + + it "does not work in methods" do + obj = Object.new + def obj.foo; it; end + + -> { obj.foo("a") }.should.raise(ArgumentError, /wrong number of arguments/) + end + + context "given multiple arguments" do + it "provides it in a block and assigns the first argument for a block" do + proc { it }.call("a", "b").should == "a" + end + + it "raises ArgumentError for a proc" do + -> { -> { it }.call("a", "b") }.should.raise(ArgumentError, "wrong number of arguments (given 2, expected 1)") + -> { lambda { it }.call("a", "b") }.should.raise(ArgumentError, "wrong number of arguments (given 2, expected 1)") + end + end + end + RUBY +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..38edc24414 --- /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(ArgumentError, 'unknown keyword: :kw2') + -> { m(kw: 1, true => false) }.should.raise(ArgumentError, 'unknown keyword: true') + -> { m(kw: 1, a: 1, b: 2, c: 3) }.should.raise(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(ArgumentError, /missing keyword: :c/) + -> { m() }.should.raise(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(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 "marked as ruby2_keywords_hash" do + it "is not copied when passed as a positional argument" do + h = Hash.ruby2_keywords_hash(a:1) + + def bar(a) + a + end + + h2 = bar(h) + h2.should.equal?(h) + Hash.ruby2_keywords_hash?(h).should == true + end + end + + context "**" 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 + + 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 + + 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 + + m(a:, b:).should == [1, 2] + end + end + + 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 + + context "in define_method(name, &proc)" do + # This tests that a free-standing proc used in define_method and converted to ruby2_keywords adopts that logic. + # See jruby/jruby#8119 for a case where aggressive JIT optimization broke later ruby2_keywords changes. + it "works with ruby2_keywords" do + m = Class.new do + def bar(a, foo: nil) + [a, foo] + end + + # define_method and ruby2_keywords using send to avoid peephole optimizations + def self.setup + pr = make_proc + send :define_method, :foo, &pr + send :ruby2_keywords, :foo + end + + # create proc in isolated method to force jit compilation on some implementations + def self.make_proc + proc { |a, *args| bar(a, *args) } + end + end + + m.setup + + m.new.foo(1, foo:2).should == [1, 2] + end + end +end diff --git a/spec/ruby/language/lambda_spec.rb b/spec/ruby/language/lambda_spec.rb index 1c9acba39c..2a2953bd97 100644 --- a/spec/ruby/language/lambda_spec.rb +++ b/spec/ruby/language/lambda_spec.rb @@ -11,25 +11,23 @@ describe "A lambda literal -> () { }" do end end - klass.new.create_lambda.should be_an_instance_of(Proc) + klass.new.create_lambda.should.instance_of?(Proc) end it "does not execute the block" do - -> { fail }.should be_an_instance_of(Proc) + -> { fail }.should.instance_of?(Proc) end it "returns a lambda" do - -> { }.lambda?.should be_true + -> { }.lambda?.should == 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.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.instance_of?(Proc) end it "has its own scope for local variables" do @@ -50,10 +48,10 @@ describe "A lambda literal -> () { }" do @d = -> do end ruby - @a.().should be_nil - @b.().should be_nil - @c.().should be_nil - @d.().should be_nil + @a.().should == nil + @b.().should == nil + @c.().should == nil + @d.().should == nil end end @@ -93,9 +91,9 @@ describe "A lambda literal -> () { }" do @a = -> (*) { } ruby - @a.().should be_nil - @a.(1).should be_nil - @a.(1, 2, 3).should be_nil + @a.().should == nil + @a.(1).should == nil + @a.(1, 2, 3).should == nil end evaluate <<-ruby do @@ -111,7 +109,7 @@ describe "A lambda literal -> () { }" do @a = -> (a:) { a } ruby - -> { @a.() }.should raise_error(ArgumentError) + -> { @a.() }.should.raise(ArgumentError) @a.(a: 1).should == 1 end @@ -127,9 +125,9 @@ describe "A lambda literal -> () { }" do @a = -> (**) { } ruby - @a.().should be_nil - @a.(a: 1, b: 2).should be_nil - -> { @a.(1) }.should raise_error(ArgumentError) + @a.().should == nil + @a.(a: 1, b: 2).should == nil + -> { @a.(1) }.should.raise(ArgumentError) end evaluate <<-ruby do @@ -144,8 +142,8 @@ describe "A lambda literal -> () { }" do @a = -> (&b) { b } ruby - @a.().should be_nil - @a.() { }.should be_an_instance_of(Proc) + @a.().should == nil + @a.() { }.should.instance_of?(Proc) end evaluate <<-ruby do @@ -153,8 +151,8 @@ describe "A lambda literal -> () { }" do ruby @a.(1, 2).should == [1, 2] - -> { @a.() }.should raise_error(ArgumentError) - -> { @a.(1) }.should raise_error(ArgumentError) + -> { @a.() }.should.raise(ArgumentError) + -> { @a.(1) }.should.raise(ArgumentError) end evaluate <<-ruby do @@ -179,43 +177,25 @@ 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 @a = -> (*, &b) { b } ruby - @a.().should be_nil - @a.(1, 2, 3, 4).should be_nil - @a.(&(l = ->{})).should equal(l) + @a.().should == nil + @a.(1, 2, 3, 4).should == nil + @a.(&(l = ->{})).should.equal?(l) end evaluate <<-ruby do @@ -283,37 +263,20 @@ 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 + ruby_version_is ""..."3.4" do + it "raises a SyntaxError if using the argument in its default value" 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 + eval "-> (a=a) { a }" + }.should.raise(SyntaxError) end 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 + ruby_version_is "3.4" do + it "is nil if using the argument in its default value" do -> { - @proc = eval "-> (a=a) { a }" - }.should raise_error(SyntaxError) + eval "-> (a=a) { a }.call" + }.call.should == nil end end @@ -323,6 +286,24 @@ describe "A lambda literal -> () { }" do end end end + + evaluate <<-ruby do + @a = -> (**nil) { :ok } + ruby + + @a.call().should == :ok + -> { @a.call(a: 1) }.should.raise(ArgumentError, 'no keywords accepted') + -> { @a.call(**{a: 1}) }.should.raise(ArgumentError, 'no keywords accepted') + -> { @a.call("a" => 1) }.should.raise(ArgumentError, 'no keywords accepted') + end + + evaluate <<-ruby do + @a = -> (a, **nil) { a } + ruby + + @a.call({a: 1}).should == {a: 1} + -> { @a.call(a: 1) }.should.raise(ArgumentError, 'no keywords accepted') + end end describe "A lambda expression 'lambda { ... }'" do @@ -336,23 +317,25 @@ describe "A lambda expression 'lambda { ... }'" do lambda { } end - obj.define.should equal(obj) + obj.define.should.equal?(obj) end it "does not execute the block" do - lambda { fail }.should be_an_instance_of(Proc) + lambda { fail }.should.instance_of?(Proc) end it "returns a lambda" do - lambda { }.lambda?.should be_true + lambda { }.lambda?.should == true end it "requires a block" do - lambda { lambda }.should raise_error(ArgumentError) + suppress_warning do + lambda { lambda }.should.raise(ArgumentError) + end end it "may include a rescue clause" do - eval('lambda do raise ArgumentError; rescue ArgumentError; 7; end').should be_an_instance_of(Proc) + eval('lambda do raise ArgumentError; rescue ArgumentError; 7; end').should.instance_of?(Proc) end context "with an implicit block" do @@ -360,24 +343,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 - -> { - 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 + it "raises ArgumentError" do + implicit_lambda = nil + suppress_warning do -> { meth { 1 } - }.should raise_error(ArgumentError, /tried to create Proc object without a block/) + }.should.raise(ArgumentError, /tried to create Proc object without a block/) end end end @@ -388,8 +359,8 @@ describe "A lambda expression 'lambda { ... }'" do @b = lambda { || } ruby - @a.().should be_nil - @b.().should be_nil + @a.().should == nil + @b.().should == nil end end @@ -406,8 +377,8 @@ describe "A lambda expression 'lambda { ... }'" do @a = lambda { |a| a } ruby - lambda { m(&@a) }.should raise_error(ArgumentError) - lambda { m(1, 2, &@a) }.should raise_error(ArgumentError) + lambda { m(&@a) }.should.raise(ArgumentError) + lambda { m(1, 2, &@a) }.should.raise(ArgumentError) end evaluate <<-ruby do @@ -417,8 +388,8 @@ describe "A lambda expression 'lambda { ... }'" do @a.(1).should == 1 @a.([1, 2]).should == [1, 2] - lambda { @a.() }.should raise_error(ArgumentError) - lambda { @a.(1, 2) }.should raise_error(ArgumentError) + lambda { @a.() }.should.raise(ArgumentError) + lambda { @a.(1, 2) }.should.raise(ArgumentError) end evaluate <<-ruby do @@ -431,7 +402,7 @@ describe "A lambda expression 'lambda { ... }'" do m(1, &@a).should == 1 m([1, 2], &@a).should == [1, 2] - lambda { m2(&@a) }.should raise_error(ArgumentError) + lambda { m2(&@a) }.should.raise(ArgumentError) end evaluate <<-ruby do @@ -462,9 +433,9 @@ describe "A lambda expression 'lambda { ... }'" do @a = lambda { |*| } ruby - @a.().should be_nil - @a.(1).should be_nil - @a.(1, 2, 3).should be_nil + @a.().should == nil + @a.(1).should == nil + @a.(1, 2, 3).should == nil end evaluate <<-ruby do @@ -480,7 +451,7 @@ describe "A lambda expression 'lambda { ... }'" do @a = lambda { |a:| a } ruby - lambda { @a.() }.should raise_error(ArgumentError) + lambda { @a.() }.should.raise(ArgumentError) @a.(a: 1).should == 1 end @@ -496,9 +467,9 @@ describe "A lambda expression 'lambda { ... }'" do @a = lambda { |**| } ruby - @a.().should be_nil - @a.(a: 1, b: 2).should be_nil - lambda { @a.(1) }.should raise_error(ArgumentError) + @a.().should == nil + @a.(a: 1, b: 2).should == nil + lambda { @a.(1) }.should.raise(ArgumentError) end evaluate <<-ruby do @@ -513,8 +484,8 @@ describe "A lambda expression 'lambda { ... }'" do @a = lambda { |&b| b } ruby - @a.().should be_nil - @a.() { }.should be_an_instance_of(Proc) + @a.().should == nil + @a.() { }.should.instance_of?(Proc) end evaluate <<-ruby do @@ -546,43 +517,25 @@ 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 @a = lambda { |*, &b| b } ruby - @a.().should be_nil - @a.(1, 2, 3, 4).should be_nil - @a.(&(l = ->{})).should equal(l) + @a.().should == nil + @a.(1, 2, 3, 4).should == nil + @a.(&(l = ->{})).should.equal?(l) end evaluate <<-ruby do @@ -648,5 +601,23 @@ describe "A lambda expression 'lambda { ... }'" do result = @a.(1, 2, e: 3, g: 4, h: 5, i: 6, &(l = ->{})) result.should == [1, 1, [], 2, 3, 2, 4, { h: 5, i: 6 }, l] end + + evaluate <<-ruby do + @a = lambda { |**nil| :ok } + ruby + + @a.call().should == :ok + -> { @a.call(a: 1) }.should.raise(ArgumentError, 'no keywords accepted') + -> { @a.call(**{a: 1}) }.should.raise(ArgumentError, 'no keywords accepted') + -> { @a.call("a" => 1) }.should.raise(ArgumentError, 'no keywords accepted') + end + + evaluate <<-ruby do + @a = lambda { |a, **nil| a } + ruby + + @a.call({a: 1}).should == {a: 1} + -> { @a.call(a: 1) }.should.raise(ArgumentError, 'no keywords accepted') + end end end diff --git a/spec/ruby/language/line_spec.rb b/spec/ruby/language/line_spec.rb index fcadaa71d7..2864798079 100644 --- a/spec/ruby/language/line_spec.rb +++ b/spec/ruby/language/line_spec.rb @@ -4,7 +4,7 @@ require_relative 'shared/__LINE__' describe "The __LINE__ pseudo-variable" do it "raises a SyntaxError if assigned to" do - -> { eval("__LINE__ = 1") }.should raise_error(SyntaxError) + -> { eval("__LINE__ = 1") }.should.raise(SyntaxError) end before :each do diff --git a/spec/ruby/language/loop_spec.rb b/spec/ruby/language/loop_spec.rb index fd17b53910..9b12765a5f 100644 --- a/spec/ruby/language/loop_spec.rb +++ b/spec/ruby/language/loop_spec.rb @@ -15,7 +15,7 @@ describe "The loop expression" do inner_loop = 123 break end - -> { inner_loop }.should raise_error(NameError) + -> { inner_loop }.should.raise(NameError) end it "returns the value passed to break if interrupted by break" do diff --git a/spec/ruby/language/magic_comment_spec.rb b/spec/ruby/language/magic_comment_spec.rb index f2bf3a08e5..af9c9dbfd0 100644 --- a/spec/ruby/language/magic_comment_spec.rb +++ b/spec/ruby/language/magic_comment_spec.rb @@ -45,7 +45,8 @@ end describe "Magic comments" do describe "in stdin" do - it_behaves_like :magic_comments, :locale, -> file { + default = (platform_is :windows and ruby_version_is "4.0") ? :UTF8 : :locale + it_behaves_like :magic_comments, default, -> file { print_at_exit = fixture(__FILE__, "print_magic_comment_result_at_exit.rb") ruby_exe(nil, args: "< #{fixture(__FILE__, file)}", options: "-r#{print_at_exit}") } diff --git a/spec/ruby/language/match_spec.rb b/spec/ruby/language/match_spec.rb index ebf677cabc..096ebee022 100644 --- a/spec/ruby/language/match_spec.rb +++ b/spec/ruby/language/match_spec.rb @@ -46,6 +46,14 @@ describe "The =~ operator with named captures" do matched.should == "foo" unmatched.should == nil end + + it "sets existing local variables if declared in a higher scope" do + a = 42 + 1.times do + /(?<a>foo)/ =~ @string + end + a.should == "foo" + end end describe "on syntax of 'string_literal' =~ /regexp/" do diff --git a/spec/ruby/language/metaclass_spec.rb b/spec/ruby/language/metaclass_spec.rb index fc83067977..3bee823a75 100644 --- a/spec/ruby/language/metaclass_spec.rb +++ b/spec/ruby/language/metaclass_spec.rb @@ -16,17 +16,17 @@ describe "self in a metaclass body (class << obj)" do end it "raises a TypeError for numbers" do - -> { class << 1; self; end }.should raise_error(TypeError) + -> { class << 1; self; end }.should.raise(TypeError) end it "raises a TypeError for symbols" do - -> { class << :symbol; self; end }.should raise_error(TypeError) + -> { class << :symbol; self; end }.should.raise(TypeError) end it "is a singleton Class instance" do cls = class << mock('x'); self; end cls.is_a?(Class).should == true - cls.should_not equal(Object) + cls.should_not.equal?(Object) end end @@ -57,20 +57,20 @@ describe "A constant on a metaclass" do end it "is not defined on the object's class" do - @object.class.const_defined?(:CONST).should be_false + @object.class.const_defined?(:CONST).should == false end it "is not defined in the metaclass opener's scope" do class << @object CONST end - -> { CONST }.should raise_error(NameError) + -> { CONST }.should.raise(NameError) end it "cannot be accessed via object::CONST" do -> do @object::CONST - end.should raise_error(TypeError) + end.should.raise(TypeError) end it "raises a NameError for anonymous_module::CONST" do @@ -81,16 +81,16 @@ describe "A constant on a metaclass" do -> do @object::CONST - end.should raise_error(NameError) + end.should.raise(NameError) end it "appears in the metaclass constant list" do constants = class << @object; constants; end - constants.should include(:CONST) + constants.should.include?(:CONST) end it "does not appear in the object's class constant list" do - @object.class.constants.should_not include(:CONST) + @object.class.constants.should_not.include?(:CONST) end it "is not preserved when the object is duped" do @@ -98,14 +98,14 @@ describe "A constant on a metaclass" do -> do class << @object; CONST; end - end.should raise_error(NameError) + end.should.raise(NameError) end it "is preserved when the object is cloned" do @object = @object.clone class << @object - CONST.should_not be_nil + CONST.should_not == nil end end end diff --git a/spec/ruby/language/method_spec.rb b/spec/ruby/language/method_spec.rb index 462a182b3d..324bd6cea5 100644 --- a/spec/ruby/language/method_spec.rb +++ b/spec/ruby/language/method_spec.rb @@ -19,7 +19,7 @@ describe "A method send" do x = mock("splat argument") x.should_not_receive(:to_ary) - m(*x).should equal(x) + m(*x).should.equal?(x) end it "calls #to_a" do @@ -40,7 +40,7 @@ describe "A method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - -> { m(*x) }.should raise_error(TypeError) + -> { m(*x) }.should.raise(TypeError) end end @@ -74,7 +74,7 @@ describe "A method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - -> { m(*x, 2, 3) }.should raise_error(TypeError) + -> { m(*x, 2, 3) }.should.raise(TypeError) end end @@ -108,13 +108,13 @@ describe "A method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - -> { m(1, *x, 2, 3) }.should raise_error(TypeError) + -> { m(1, *x, 2, 3) }.should.raise(TypeError) end it "copies the splatted array" do args = [3, 4] m(1, 2, *args, 4, 5).should == [1, 2, [3, 4], 4, 5] - m(1, 2, *args, 4, 5)[2].should_not equal(args) + m(1, 2, *args, 4, 5)[2].should_not.equal?(args) end it "allows an array being splatted to be modified by another argument" do @@ -153,7 +153,7 @@ describe "A method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - -> { m(1, 2, *x) }.should raise_error(TypeError) + -> { m(1, 2, *x) }.should.raise(TypeError) end end @@ -217,7 +217,7 @@ describe "An element assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - -> { @o[*x] = 1 }.should raise_error(TypeError) + -> { @o[*x] = 1 }.should.raise(TypeError) end end @@ -255,7 +255,7 @@ describe "An element assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - -> { @o[*x, 2, 3] = 4 }.should raise_error(TypeError) + -> { @o[*x, 2, 3] = 4 }.should.raise(TypeError) end end @@ -293,7 +293,7 @@ describe "An element assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - -> { @o[1, 2, *x, 3] = 4 }.should raise_error(TypeError) + -> { @o[1, 2, *x, 3] = 4 }.should.raise(TypeError) end end @@ -331,7 +331,7 @@ describe "An element assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - -> { @o[1, 2, 3, *x] = 4 }.should raise_error(TypeError) + -> { @o[1, 2, 3, *x] = 4 }.should.raise(TypeError) end end end @@ -368,7 +368,7 @@ describe "An attribute assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - -> { @o.send :m=, *x, 1 }.should raise_error(TypeError) + -> { @o.send :m=, *x, 1 }.should.raise(TypeError) end end @@ -403,7 +403,7 @@ describe "An attribute assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - -> { @o.send :m=, *x, 2, 3, 4 }.should raise_error(TypeError) + -> { @o.send :m=, *x, 2, 3, 4 }.should.raise(TypeError) end end @@ -438,7 +438,7 @@ describe "An attribute assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - -> { @o.send :m=, 1, 2, *x, 3, 4 }.should raise_error(TypeError) + -> { @o.send :m=, 1, 2, *x, 3, 4 }.should.raise(TypeError) end end @@ -473,7 +473,7 @@ describe "An attribute assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - -> { @o.send :m=, 1, 2, 3, *x, 4 }.should raise_error(TypeError) + -> { @o.send :m=, 1, 2, 3, *x, 4 }.should.raise(TypeError) end end end @@ -487,7 +487,7 @@ describe "A method" do end ruby - m.should be_nil + m.should == nil end evaluate <<-ruby do @@ -495,7 +495,7 @@ describe "A method" do end ruby - m.should be_nil + m.should == nil end end @@ -504,7 +504,7 @@ describe "A method" do def m(a) a end ruby - m((args = 1, 2, 3)).should equal(args) + m((args = 1, 2, 3)).should.equal?(args) end evaluate <<-ruby do @@ -535,18 +535,18 @@ describe "A method" do def m() end ruby - m().should be_nil - m(*[]).should be_nil - m(**{}).should be_nil + m().should == nil + m(*[]).should == nil + m(**{}).should == nil end evaluate <<-ruby do def m(*) end ruby - m().should be_nil - m(1).should be_nil - m(1, 2, 3).should be_nil + m().should == nil + m(1).should == nil + m(1, 2, 3).should == nil end evaluate <<-ruby do @@ -564,14 +564,21 @@ describe "A method" do def m(a:) a end ruby - -> { m() }.should raise_error(ArgumentError) + -> { m() }.should.raise(ArgumentError) m(a: 1).should == 1 suppress_keyword_warning do - -> { m("a" => 1, a: 1) }.should raise_error(ArgumentError) + -> { m("a" => 1, a: 1) }.should.raise(ArgumentError) end end evaluate <<-ruby do + def m(a:, **kw) [a, kw] end + ruby + + -> { m(b: 1) }.should.raise(ArgumentError) + end + + evaluate <<-ruby do def m(a: 1) a end ruby @@ -583,9 +590,9 @@ describe "A method" do def m(**) end ruby - m().should be_nil - m(a: 1, b: 2).should be_nil - -> { m(1) }.should raise_error(ArgumentError) + m().should == nil + m(a: 1, b: 2).should == nil + -> { m(1) }.should.raise(ArgumentError) end evaluate <<-ruby do @@ -599,23 +606,21 @@ describe "A method" do suppress_warning { eval "m(**{a: 1, b: 2}, **{a: 4, c: 7})" }.should == { a: 4, b: 2, c: 7 } - -> { m(2) }.should raise_error(ArgumentError) + -> { m(2) }.should.raise(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 def m(&b) b end ruby - m { }.should be_an_instance_of(Proc) + m { }.should.instance_of?(Proc) end evaluate <<-ruby do @@ -645,9 +650,9 @@ describe "A method" do def m((*), (*)) end ruby - m(2, 3).should be_nil - m([2, 3, 4], [5, 6]).should be_nil - -> { m a: 1 }.should raise_error(ArgumentError) + m(2, 3).should == nil + m([2, 3, 4], [5, 6]).should == nil + -> { m a: 1 }.should.raise(ArgumentError) end evaluate <<-ruby do @@ -740,72 +745,35 @@ describe "A method" do m(1, b: 2).should == [1, 2] suppress_keyword_warning do - -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) + -> { m("a" => 1, b: 2) }.should.raise(ArgumentError) 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(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(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(ArgumentError) end evaluate <<-ruby do @@ -880,8 +848,8 @@ describe "A method" do def m(a=1, (*b), (*c)) [a, b, c] end ruby - -> { m() }.should raise_error(ArgumentError) - -> { m(2) }.should raise_error(ArgumentError) + -> { m() }.should.raise(ArgumentError) + -> { m(2) }.should.raise(ArgumentError) m(2, 3).should == [1, [2], [3]] m(2, [3, 4], [5, 6]).should == [2, [3, 4], [5, 6]] end @@ -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(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(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,456 +949,13 @@ 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 - m().should be_nil - m(1, 2, 3, 4).should be_nil - m(&(l = ->{})).should equal(l) + m().should == nil + m(1, 2, 3, 4).should == nil + m(&(l = ->{})).should.equal?(l) end evaluate <<-ruby do @@ -1488,7 +973,7 @@ describe "A method" do m(a: 1, b: 2).should == [1, 2] suppress_keyword_warning do - -> { m("a" => 1, a: 1, b: 2) }.should raise_error(ArgumentError) + -> { m("a" => 1, a: 1, b: 2) }.should.raise(ArgumentError) end end @@ -1499,48 +984,26 @@ describe "A method" do m(a: 1).should == [1, 1] m(a: 1, b: 2).should == [1, 2] suppress_keyword_warning do - -> { m("a" => 1, a: 1, b: 2) }.should raise_error(ArgumentError) + -> { m("a" => 1, a: 1, b: 2) }.should.raise(ArgumentError) 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,124 @@ 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 - - m({a: 1}).should == {a: 1} - m({"a" => 1}).should == {"a" => 1} + evaluate <<-ruby do + def m(**nil); :ok; end; + ruby - -> { m(a: 1) }.should raise_error(ArgumentError) - -> { m(**{a: 1}) }.should raise_error(ArgumentError) - -> { m("a" => 1) }.should raise_error(ArgumentError) - end + m().should == :ok + -> { m(a: 1) }.should.raise(ArgumentError, 'no keywords accepted') + -> { m(**{a: 1}) }.should.raise(ArgumentError, 'no keywords accepted') + -> { m("a" => 1) }.should.raise(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 + evaluate <<-ruby do + def m(a, **nil); a end; + ruby - result = m(1, 2) - result.should == [1, nil, nil, 2, nil, {}] + m({a: 1}).should == {a: 1} + m({"a" => 1}).should == {"a" => 1} + + -> { m(a: 1) }.should.raise(ArgumentError, 'no keywords accepted') + -> { m(**{a: 1}) }.should.raise(ArgumentError, 'no keywords accepted') + -> { m("a" => 1) }.should.raise(ArgumentError, 'no keywords accepted') + end - 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 + 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 - ruby_version_is '3.0' do + ruby_version_is "4.1" do evaluate <<-ruby do - def m(a, b = nil, c = nil, d, e: nil, **f) - [a, b, c, d, e, f] - end + def m(a, &nil); a 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, {}] + m(1).should == 1 - result = m(1, {foo: :bar}) - result.should == [1, nil, nil, {foo: :bar}, nil, {}] + -> { m(1) {} }.should.raise(ArgumentError, 'no block accepted') + -> { m(1, &proc {}) }.should.raise(ArgumentError, 'no block accepted') end end end - 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 + 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 = {} - m(**h).should == [] - end + h = {} + m(**h).should == [] 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 + h = {} - -> do - m(**h).should == {} - end.should complain(/warning: Passing the keyword argument as the last hash parameter is deprecated/) - end + -> do + m(**h).should == {} + end.should.raise(ArgumentError) 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 "raises ArgumentError if passing hash as keyword arguments" do + evaluate <<-ruby do + def m(a: nil); a; end + ruby - options = {a: 1}.freeze - -> do - suppress_warning do - m(options).should == 1 - end - end.should_not raise_error - options.should == {a: 1} - end + options = {a: 1}.freeze + -> do + m(options) + end.should.raise(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 = {} + it "assigns the last Hash to the last optional argument if the Hash contains non-Symbol keys and is not passed as keywords" do + def m(a = nil, b = {}, v: false) + [a, b, v] + end - -> do - m(**h).should == {} - end.should raise_error(ArgumentError) - end + h = { "key" => "value" } + m(:a, h).should == [:a, h, false] + m(:a, h, v: true).should == [:a, h, true] + m(v: true).should == [nil, {}, true] + end +end + +context "when passing **nil into a method that accepts keyword arguments" do + ruby_version_is ""..."3.4" do + it "raises TypeError" do + def m(**kw) kw; end + + h = nil + -> { m(a: 1, **h) }.should.raise(TypeError, "no implicit conversion of nil into Hash") + -> { m(a: 1, **nil) }.should.raise(TypeError, "no implicit conversion of nil into Hash") end + end - context "raises ArgumentError if passing hash as keyword arguments" do - evaluate <<-ruby do - def m(a: nil); a; end - ruby + ruby_version_is "3.4" do + it "expands nil using ** into {}" do + def m(**kw) kw; end - options = {a: 1}.freeze - -> do - m(options) - end.should raise_error(ArgumentError) - end + h = nil + m(**h).should == {} + m(a: 1, **h).should == {a: 1} + + m(**nil).should == {} + m(a: 1, **nil).should == {a: 1} end end end @@ -1778,22 +1240,48 @@ 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 + + it "supports multiple statements" do + eval("m (1; 2)").should == [2] + 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)") - }.should raise_error(SyntaxError) + }.should.raise(SyntaxError) -> { eval("m (1, 2, 3)") - }.should raise_error(SyntaxError) + }.should.raise(SyntaxError) end end @@ -1850,15 +1338,332 @@ 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 - a = b = m 1 - a.should == 1 - b.should == 1 + 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(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(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 + +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 = call(a:) + h.should == {a: 1} + arr.should == [] + + arr, h = call(a:, b:, c:) + h.should == {a: 1, b: 2, c: 3} + arr.should == [] + + arr, h = 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 + def greet(person) = "Hi, ".dup.concat person + + greet("Homer").should == "Hi, Homer" + end +end + +describe "warning about not used block argument" do + ruby_version_is "3.4" do + it "warns when passing a block argument to a method that never uses it" do + def m_that_does_not_use_block + 42 + end + + -> { + m_that_does_not_use_block { } + }.should complain( + /#{__FILE__}:#{__LINE__ - 2}: warning: the block passed to 'm_that_does_not_use_block' defined at #{__FILE__}:#{__LINE__ - 7} may be ignored/, + verbose: true) + end + + it "does not warn when passing a block argument to a method that declares a block parameter" do + def m_with_block_parameter(&block) + 42 + end + + -> { m_with_block_parameter { } }.should_not complain(verbose: true) + end + + it "does not warn when passing a block argument to a method that declares an anonymous block parameter" do + def m_with_anonymous_block_parameter(&) + 42 + end + + -> { m_with_anonymous_block_parameter { } }.should_not complain(verbose: true) + end + + it "does not warn when passing a block argument to a method that yields an implicit block parameter" do + def m_with_yield + yield 42 + end + + -> { m_with_yield { } }.should_not complain(verbose: true) + end + + it "warns when passing a block argument to a method that calls #block_given?" do + def m_with_block_given + block_given? + end + + -> { + m_with_block_given { } + }.should complain( + /#{__FILE__}:#{__LINE__ - 2}: warning: the block passed to 'm_with_block_given' defined at #{__FILE__}:#{__LINE__ - 7} may be ignored/, + verbose: true) + end + + it "does not warn when passing a block argument to a method that calls super" do + parent = Class.new do + def m + end + end + + child = Class.new(parent) do + def m + super + end + end + + obj = child.new + -> { obj.m { } }.should_not complain(verbose: true) + end + + it "does not warn when passing a block argument to a method that calls super(...)" do + parent = Class.new do + def m(a) + end + end + + child = Class.new(parent) do + def m(...) + super(...) + end + end + + obj = child.new + -> { obj.m(42) { } }.should_not complain(verbose: true) + end + + it "does not warn when called #initialize()" do + klass = Class.new do + def initialize + end + end + + -> { klass.new {} }.should_not complain(verbose: true) + end + + it "does not warn when passing a block argument to a method that calls super()" do + parent = Class.new do + def m + end + end + + child = Class.new(parent) do + def m + super() + end + end + + obj = child.new + -> { obj.m { } }.should_not complain(verbose: true) + end + + it "warns only once per call site" do + def m_that_does_not_use_block + 42 + end + + def call_m_that_does_not_use_block + m_that_does_not_use_block {} + end + + -> { + m_that_does_not_use_block { } + }.should complain(/the block passed to 'm_that_does_not_use_block' defined at .+ may be ignored/, verbose: true) + + -> { + m_that_does_not_use_block { } + }.should_not complain(verbose: true) + end + + it "can be disabled with :strict_unused_block warning category" do + def m_that_does_not_use_block + 42 + end + + # ensure that warning is emitted + -> { m_that_does_not_use_block { } }.should complain(verbose: true) + + warn_strict_unused_block = Warning[:strict_unused_block] + Warning[:strict_unused_block] = false + begin + -> { m_that_does_not_use_block { } }.should_not complain(verbose: true) + ensure + Warning[:strict_unused_block] = warn_strict_unused_block + end + end + + it "can be enabled with :strict_unused_block = true warning category in not verbose mode" do + def m_that_does_not_use_block + 42 + end + + warn_strict_unused_block = Warning[:strict_unused_block] + Warning[:strict_unused_block] = true + begin + -> { + m_that_does_not_use_block { } + }.should complain(/the block passed to 'm_that_does_not_use_block' defined at .+ may be ignored/) + ensure + Warning[:strict_unused_block] = warn_strict_unused_block + end end end end diff --git a/spec/ruby/language/module_spec.rb b/spec/ruby/language/module_spec.rb index cbc7149359..2f22e383d5 100644 --- a/spec/ruby/language/module_spec.rb +++ b/spec/ruby/language/module_spec.rb @@ -4,85 +4,105 @@ require_relative 'fixtures/module' describe "The module keyword" do it "creates a new module without semicolon" do module ModuleSpecsKeywordWithoutSemicolon end - ModuleSpecsKeywordWithoutSemicolon.should be_an_instance_of(Module) + ModuleSpecsKeywordWithoutSemicolon.should.instance_of?(Module) end it "creates a new module with a non-qualified constant name" do module ModuleSpecsToplevel; end - ModuleSpecsToplevel.should be_an_instance_of(Module) + ModuleSpecsToplevel.should.instance_of?(Module) end it "creates a new module with a qualified constant name" do module ModuleSpecs::Nested; end - ModuleSpecs::Nested.should be_an_instance_of(Module) + ModuleSpecs::Nested.should.instance_of?(Module) end it "creates a new module with a variable qualified constant name" do m = Module.new module m::N; end - m::N.should be_an_instance_of(Module) + m::N.should.instance_of?(Module) end it "reopens an existing module" do module ModuleSpecs; Reopened = true; end - 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 + ModuleSpecs::Reopened.should == true + ensure + ModuleSpecs.send(:remove_const, :Reopened) + end + + it "does not reopen a module included in Object" do + ruby_exe(<<~RUBY).should == "false" + module IncludedInObject + module IncludedModule; end + end + class Object + include IncludedInObject + end + module IncludedModule; end + print IncludedInObject::IncludedModule == Object::IncludedModule + RUBY + end + + it "does not reopen a module included in non-Object modules" do + ruby_exe(<<~RUBY).should == "false/false" + module Included + module IncludedModule; end + end + module M + include Included + module IncludedModule; end + end + class C + include Included + module IncludedModule; end + end + print Included::IncludedModule == M::IncludedModule, "/", + Included::IncludedModule == C::IncludedModule + RUBY end it "raises a TypeError if the constant is a Class" do -> do module ModuleSpecs::Modules::Klass; end - end.should raise_error(TypeError) + end.should.raise(TypeError) end it "raises a TypeError if the constant is a String" do - -> { module ModuleSpecs::Modules::A; end }.should raise_error(TypeError) + -> { module ModuleSpecs::Modules::A; end }.should.raise(TypeError) end - it "raises a TypeError if the constant is a Fixnum" do - -> { module ModuleSpecs::Modules::B; end }.should raise_error(TypeError) + it "raises a TypeError if the constant is an Integer" do + -> { module ModuleSpecs::Modules::B; end }.should.raise(TypeError) end it "raises a TypeError if the constant is nil" do - -> { module ModuleSpecs::Modules::C; end }.should raise_error(TypeError) + -> { module ModuleSpecs::Modules::C; end }.should.raise(TypeError) end it "raises a TypeError if the constant is true" do - -> { module ModuleSpecs::Modules::D; end }.should raise_error(TypeError) + -> { module ModuleSpecs::Modules::D; end }.should.raise(TypeError) end it "raises a TypeError if the constant is false" do - -> { module ModuleSpecs::Modules::D; end }.should raise_error(TypeError) + -> { module ModuleSpecs::Modules::D; end }.should.raise(TypeError) end end describe "Assigning an anonymous module to a constant" do it "sets the name of the module" do mod = Module.new - mod.name.should be_nil + mod.name.should == nil ::ModuleSpecs_CS1 = mod mod.name.should == "ModuleSpecs_CS1" + ensure + Object.send(:remove_const, :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 @@ -97,5 +117,7 @@ describe "Assigning an anonymous module to a constant" do b.name.should == "ModuleSpecs_CS2::B" c.name.should == "ModuleSpecs_CS2::B::C" d.name.should == "ModuleSpecs_CS2::D" + ensure + Object.send(:remove_const, :ModuleSpecs_CS2) end end diff --git a/spec/ruby/language/next_spec.rb b/spec/ruby/language/next_spec.rb index 6fbfc4a54d..eac151eeb3 100644 --- a/spec/ruby/language/next_spec.rb +++ b/spec/ruby/language/next_spec.rb @@ -21,7 +21,7 @@ describe "The next statement from within the block" do end it "causes block to return nil if invoked with an empty expression" do - -> { next (); 456 }.call.should be_nil + -> { next (); 456 }.call.should == nil end it "returns the argument passed" do @@ -111,7 +111,7 @@ describe "The next statement" do it "is invalid and raises a SyntaxError" do -> { eval("def m; next; end") - }.should raise_error(SyntaxError) + }.should.raise(SyntaxError) end end end diff --git a/spec/ruby/language/not_spec.rb b/spec/ruby/language/not_spec.rb index 052af9b256..23411a8e1d 100644 --- a/spec/ruby/language/not_spec.rb +++ b/spec/ruby/language/not_spec.rb @@ -2,50 +2,50 @@ require_relative '../spec_helper' describe "The not keyword" do it "negates a `true' value" do - (not true).should be_false - (not 'true').should be_false + (not true).should == false + (not 'true').should == false end it "negates a `false' value" do - (not false).should be_true - (not nil).should be_true + (not false).should == true + (not nil).should == true end it "accepts an argument" do - not(true).should be_false + not(true).should == false end it "returns false if the argument is true" do - (not(true)).should be_false + (not(true)).should == false end it "returns true if the argument is false" do - (not(false)).should be_true + (not(false)).should == true end it "returns true if the argument is nil" do - (not(nil)).should be_true + (not(nil)).should == true end end describe "The `!' keyword" do it "negates a `true' value" do - (!true).should be_false - (!'true').should be_false + (!true).should == false + (!'true').should == false end it "negates a `false' value" do - (!false).should be_true - (!nil).should be_true + (!false).should == true + (!nil).should == true end it "doubled turns a truthful object into `true'" do - (!!true).should be_true - (!!'true').should be_true + (!!true).should == true + (!!'true').should == true end it "doubled turns a not truthful object into `false'" do - (!!false).should be_false - (!!nil).should be_false + (!!false).should == false + (!!nil).should == false end end diff --git a/spec/ruby/language/numbered_parameters_spec.rb b/spec/ruby/language/numbered_parameters_spec.rb index b05c373a68..23e89a14be 100644 --- a/spec/ruby/language/numbered_parameters_spec.rb +++ b/spec/ruby/language/numbered_parameters_spec.rb @@ -1,106 +1,113 @@ 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(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.+ outer block here/m) - end + it "can not be used in both outer and nested blocks at the same time" do + -> { + eval("-> { _1; -> { _2 } }") + }.should.raise(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(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 + it "errors when numbered parameter is overwritten with local variable" do + -> { + eval("_1 = 0") + }.should.raise(SyntaxError, /_1 is reserved for numbered parameter/) + end + + it "raises SyntaxError when block parameters are specified explicitly" do + -> { eval("-> () { _1 }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + -> { eval("-> (x) { _1 }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + + -> { eval("proc { || _1 }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + -> { eval("proc { |x| _1 }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + + -> { eval("lambda { || _1 }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + -> { eval("lambda { |x| _1 }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + + -> { eval("['a'].map { || _1 }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + -> { eval("['a'].map { |x| _1 }") }.should.raise(SyntaxError, /ordinary parameter is defined/) + end + + describe "assigning to a numbered parameter" do + it "raises SyntaxError" do + -> { eval("proc { _1 = 0 }") }.should.raise(SyntaxError, /_1 is reserved for numbered parameter/) end + 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 "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 - -> { eval("proc { || _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) - -> { eval("proc { |x| _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + it "affects block parameters" do + -> { _1 }.parameters.should == [[:req, :_1]] + -> { _2 }.parameters.should == [[:req, :_1], [:req, :_2]] - -> { eval("lambda { || _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) - -> { eval("lambda { |x| _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + proc { _1 }.parameters.should == [[:opt, :_1]] + proc { _2 }.parameters.should == [[:opt, :_1], [:opt, :_2]] + 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/) + ruby_version_is ""..."4.0" do + 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 + 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 + ruby_version_is "4.0" do + it "does not affect binding local variables" do + -> { _1; binding.local_variables }.call("a").should == [] + -> { _2; binding.local_variables }.call("a", "b").should == [] end + end - it "does not work in methods" do - obj = Object.new - def obj.foo; _1 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 + -> { obj.foo("a") }.should.raise(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..ed0e49c048 100644 --- a/spec/ruby/language/numbers_spec.rb +++ b/spec/ruby/language/numbers_spec.rb @@ -11,7 +11,7 @@ describe "A number literal" do end it "cannot have a leading underscore" do - -> { eval("_4_2") }.should raise_error(NameError) + -> { eval("_4_2") }.should.raise(NameError) end it "can have a decimal point" do @@ -20,8 +20,8 @@ describe "A number literal" do it "must have a digit before the decimal point" do 0.75.should == 0.75 - -> { eval(".75") }.should raise_error(SyntaxError) - -> { eval("-.75") }.should raise_error(SyntaxError) + -> { eval(".75") }.should.raise(SyntaxError) + -> { eval("-.75") }.should.raise(SyntaxError) end it "can have an exponent" do @@ -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 3d9e0dbb65..ebb5d36351 100644 --- a/spec/ruby/language/optional_assignments_spec.rb +++ b/spec/ruby/language/optional_assignments_spec.rb @@ -1,4 +1,5 @@ require_relative '../spec_helper' +require_relative '../fixtures/constants' describe 'Optional variable assignments' do describe 'using ||=' do @@ -56,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 @@ -102,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 @@ -121,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 - def b=(x) - @b = x - :v + it 'returns the assigned value, not the result of the []= method with ||=' do + (@b[:k] ||= 12).should == 12 + end + + 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 - private :b= + (ScratchPad << :evaluated; @a)[:k] ||= 2 + + 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 @@ -190,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 @@ -235,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 @@ -296,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 @@ -335,7 +603,7 @@ describe 'Optional variable assignments' do end it 'with &&= assignments will fail with non-existent constants' do - -> { Object::A &&= 10 }.should raise_error(NameError) + -> { Object::A &&= 10 }.should.raise(NameError) end it 'with operator assignments' do @@ -347,7 +615,128 @@ describe 'Optional variable assignments' do end it 'with operator assignments will fail with non-existent constants' do - -> { Object::A += 10 }.should raise_error(NameError) + -> { Object::A += 10 }.should.raise(NameError) + end + end +end + +describe 'Optional constant assignment' do + describe 'with ||=' do + it "assigns a scoped constant if previously undefined" do + ConstantSpecs.should_not.const_defined?(:OpAssignUndefined) + module ConstantSpecs + OpAssignUndefined ||= 42 + end + ConstantSpecs::OpAssignUndefined.should == 42 + ConstantSpecs::OpAssignUndefinedOutside ||= 42 + ConstantSpecs::OpAssignUndefinedOutside.should == 42 + ConstantSpecs.send(:remove_const, :OpAssignUndefined) + ConstantSpecs.send(:remove_const, :OpAssignUndefinedOutside) + end + + it "assigns a global constant if previously undefined" do + OpAssignGlobalUndefined ||= 42 + ::OpAssignGlobalUndefinedExplicitScope ||= 42 + OpAssignGlobalUndefined.should == 42 + ::OpAssignGlobalUndefinedExplicitScope.should == 42 + Object.send :remove_const, :OpAssignGlobalUndefined + Object.send :remove_const, :OpAssignGlobalUndefinedExplicitScope + end + + it 'correctly defines non-existing constants' do + ConstantSpecs::ClassA::OR_ASSIGNED_CONSTANT1 ||= :assigned + ConstantSpecs::ClassA::OR_ASSIGNED_CONSTANT1.should == :assigned + end + + it 'correctly overwrites nil constants' do + suppress_warning do # already initialized constant + ConstantSpecs::ClassA::NIL_OR_ASSIGNED_CONSTANT1 = nil + ConstantSpecs::ClassA::NIL_OR_ASSIGNED_CONSTANT1 ||= :assigned + ConstantSpecs::ClassA::NIL_OR_ASSIGNED_CONSTANT1.should == :assigned + end + end + + it 'causes side-effects of the module part to be applied only once (for undefined constant)' do + x = 0 + (x += 1; ConstantSpecs::ClassA)::OR_ASSIGNED_CONSTANT2 ||= :assigned + x.should == 1 + ConstantSpecs::ClassA::OR_ASSIGNED_CONSTANT2.should == :assigned + end + + 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 + (x += 1; ConstantSpecs::ClassA)::NIL_OR_ASSIGNED_CONSTANT2 ||= :assigned + x.should == 1 + ConstantSpecs::ClassA::NIL_OR_ASSIGNED_CONSTANT2.should == :assigned + end + end + + it 'does not evaluate the right-hand side if the module part raises an exception (for undefined constant)' do + x = 0 + y = 0 + + -> { + (x += 1; raise Exception; ConstantSpecs::ClassA)::OR_ASSIGNED_CONSTANT3 ||= (y += 1; :assigned) + }.should.raise(Exception) + + x.should == 1 + y.should == 0 + defined?(ConstantSpecs::ClassA::OR_ASSIGNED_CONSTANT3).should == nil + end + + it 'does not evaluate the right-hand side if the module part raises an exception (for nil constant)' do + ConstantSpecs::ClassA::NIL_OR_ASSIGNED_CONSTANT3 = nil + x = 0 + y = 0 + + -> { + (x += 1; raise Exception; ConstantSpecs::ClassA)::NIL_OR_ASSIGNED_CONSTANT3 ||= (y += 1; :assigned) + }.should.raise(Exception) + + x.should == 1 + y.should == 0 + ConstantSpecs::ClassA::NIL_OR_ASSIGNED_CONSTANT3.should == nil + ensure + ConstantSpecs::ClassA.send(:remove_const, :NIL_OR_ASSIGNED_CONSTANT3) + end + end + + describe "with &&=" do + it "re-assigns a scoped constant if already true" do + module ConstantSpecs + OpAssignTrue = true + end + suppress_warning do + ConstantSpecs::OpAssignTrue &&= 1 + end + ConstantSpecs::OpAssignTrue.should == 1 + ConstantSpecs.send :remove_const, :OpAssignTrue + end + + it "leaves scoped constant if not true" do + module ConstantSpecs + OpAssignFalse = false + end + ConstantSpecs::OpAssignFalse &&= 1 + 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/or_spec.rb b/spec/ruby/language/or_spec.rb index fb75e788f1..8ae577a142 100644 --- a/spec/ruby/language/or_spec.rb +++ b/spec/ruby/language/or_spec.rb @@ -26,24 +26,24 @@ describe "The || operator" do end it "treats empty expressions as nil" do - (() || true).should be_true - (() || false).should be_false - (true || ()).should be_true - (false || ()).should be_nil - (() || ()).should be_nil + (() || true).should == true + (() || false).should == false + (true || ()).should == true + (false || ()).should == nil + (() || ()).should == nil end it "has a higher precedence than 'break' in 'break true || false'" do # see also 'break true or false' below - -> { break false || true }.call.should be_true + -> { break false || true }.call.should == true end it "has a higher precedence than 'next' in 'next true || false'" do - -> { next false || true }.call.should be_true + -> { next false || true }.call.should == true end it "has a higher precedence than 'return' in 'return true || false'" do - -> { return false || true }.call.should be_true + -> { return false || true }.call.should == true end end @@ -68,23 +68,23 @@ describe "The or operator" do end it "treats empty expressions as nil" do - (() or true).should be_true - (() or false).should be_false - (true or ()).should be_true - (false or ()).should be_nil - (() or ()).should be_nil + (() or true).should == true + (() or false).should == false + (true or ()).should == true + (false or ()).should == nil + (() or ()).should == nil end it "has a lower precedence than 'break' in 'break true or false'" do # see also 'break true || false' above - -> { eval "break true or false" }.should raise_error(SyntaxError, /void value expression/) + -> { eval "break true or false" }.should.raise(SyntaxError, /void value expression/) end it "has a lower precedence than 'next' in 'next true or false'" do - -> { eval "next true or false" }.should raise_error(SyntaxError, /void value expression/) + -> { eval "next true or false" }.should.raise(SyntaxError, /void value expression/) end it "has a lower precedence than 'return' in 'return true or false'" do - -> { eval "return true or false" }.should raise_error(SyntaxError, /void value expression/) + -> { eval "return true or false" }.should.raise(SyntaxError, /void value expression/) end end diff --git a/spec/ruby/language/pattern_matching_spec.rb b/spec/ruby/language/pattern_matching_spec.rb index e82b0f5b3c..a24500c9fd 100644 --- a/spec/ruby/language/pattern_matching_spec.rb +++ b/spec/ruby/language/pattern_matching_spec.rb @@ -1,1061 +1,1310 @@ 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 - - before do - ScratchPad.record [] - 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] - [0, 1] => [a, b] - [a, b] - RUBY - end +describe "Pattern matching" do + before :each do + ScratchPad.record [] + end + + describe "Rightward assignment (`=>`) that can be standalone assoc operator that" do + it "deconstructs value" do + suppress_warning do + [0, 1] => [a, b] + [a, b].should == [0, 1] 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 + it "deconstructs value and properly scopes variables" do + suppress_warning do + a = nil + 1.times { + [0, 1] => [a, b] + } + [a, defined?(b)].should == [0, nil] + end 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 + it "can work with keywords" do + { a: 0, b: 1 } => { a:, b: } + [a, b].should == [0, 1] + end + end + + describe "One-line pattern matching" do + it "can be used to check if a pattern matches for Array-like entities" do + ([0, 1] in [a, b]).should == true + ([0, 1] in [a, b, c]).should == false end - describe "warning" do - before do - ruby_version_is ""..."3.0" do - @src = 'case 0; in a; end' + it "can be used to check if a pattern matches for Hash-like entities" do + ({ a: 0, b: 1 } in { a:, b: }).should == true + ({ a: 0, b: 1 } in { a:, b:, c: }).should == false + end + end + + describe "find pattern" do + it "captures preceding elements to the pattern" do + case [0, 1, 2, 3] + in [*pre, 2, 3] + pre + else + false + end.should == [0, 1] + end + + it "captures following elements to the pattern" do + case [0, 1, 2, 3] + in [0, 1, *post] + post + else + false + end.should == [2, 3] + end + + it "captures both preceding and following elements to the pattern" do + case [0, 1, 2, 3, 4] + in [*pre, 2, *post] + [pre, post] + else + false + end.should == [[0, 1], [3, 4]] + end + + it "can capture the entirety of the pattern" do + case [0, 1, 2, 3, 4] + in [*everything] + everything + else + false + end.should == [0, 1, 2, 3, 4] + end + + it "will match an empty Array-like structure" do + case [] + in [*everything] + everything + else + false + end.should == [] + end + + it "can be nested" do + case [0, [2, 4, 6], [3, 9, 27], [4, 16, 64]] + in [*pre, [*, 9, a], *post] + [pre, post, a] + else + false + end.should == [[0, [2, 4, 6]], [[4, 16, 64]], 27] + end + + it "can be nested with an array pattern" do + case [0, [2, 4, 6], [3, 9, 27], [4, 16, 64]] + in [_, _, [*, 9, *], *post] + post + else + false + end.should == [[4, 16, 64]] + end + + it "can be nested within a hash pattern" do + case {a: [3, 9, 27]} + in {a: [*, 9, *post]} + post + else + false + end.should == [27] + end + + it "can nest hash and array patterns" do + case [0, {a: 42, b: [0, 1]}, {a: 42, b: [1, 2]}] + in [*, {a:, b: [1, c]}, *] + [a, c] + else + false + end.should == [42, 2] + end + end + + it "extends case expression with case/in construction" do + case [0, 1] + in [0] + :foo + in [0, 1] + :bar + end.should == :bar + end + + it "allows using then operator" do + case [0, 1] + in [0] then :foo + in [0, 1] then :bar + end.should == :bar + 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 + @src = '[0, 1] => [a, b]' + end + + it "does not warn about pattern matching is experimental feature" do + -> { eval @src }.should_not complain + end + end + end + + it "binds variables" do + case [0, 1] + in [0, a] + a + end.should == 1 + end + + it "cannot mix in and when operators" do + -> { + eval <<~RUBY + case [] + when 1 == 1 + in [] end + RUBY + }.should.raise(SyntaxError, /syntax error, unexpected `in'|\(eval\):3: syntax error, unexpected keyword_in|unexpected 'in'/) - ruby_version_is "3.0" do - @src = '1 => a' + -> { + eval <<~RUBY + case [] + in [] + when 1 == 1 end + RUBY + }.should.raise(SyntaxError, /syntax error, unexpected `when'|\(eval\):3: syntax error, unexpected keyword_when|unexpected 'when'/) + end + + it "checks patterns until the first matching" do + case [0, 1] + in [0] + :foo + in [0, 1] + :bar + in [0, 1] + :baz + end.should == :bar + end + + it "executes else clause if no pattern matches" do + case [0, 1] + in [0] + true + else + false + end.should == false + end + + it "raises NoMatchingPatternError if no pattern matches and no else clause" do + -> { + case [0, 1] + in [0] end + }.should.raise(NoMatchingPatternError, /\[0, 1\]/) - 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) + error_pattern = ruby_version_is("3.4") ? /\{a: 0, b: 1\}/ : /\{:a=>0, :b=>1\}/ + -> { + case {a: 0, b: 1} + in a: 1, b: 1 end - end + }.should.raise(NoMatchingPatternError, error_pattern) + end - it "binds variables" do - eval(<<~RUBY).should == 1 - case [0, 1] - in [0, a] - a + it "raises NoMatchingPatternError if no pattern matches and evaluates the expression only once" do + evals = 0 + -> { + case (evals += 1; [0, 1]) + in [0] + end + }.should.raise(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(SyntaxError, /unexpected|expected a delimiter after the patterns of an `in` clause/) + end + + it "evaluates the case expression once for multiple patterns, caching the result" do + case (ScratchPad << :foo; 1) + in 0 + false + in 1 + true + end.should == true + + ScratchPad.recorded.should == [:foo] + end + + describe "guards" do + it "supports if guard" do + case 0 + in 0 if false + true + else + false + end.should == false + + case 0 + in 0 if true + true + else + false + end.should == true 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 "supports unless guard" do + case 0 + in 0 unless true + true + else + false + end.should == false + + case 0 + in 0 unless false + true + else + false + end.should == true + end - -> { - eval <<~RUBY - case [] - in [] - when 1 == 1 - end - RUBY - }.should raise_error(SyntaxError, /syntax error, unexpected `when'/) + it "makes bound variables visible in guard" do + case [0, 1] + in [a, 1] if a >= 0 + true + end.should == true 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 + it "does not evaluate guard if pattern does not match" do + case 0 + in 1 if (ScratchPad << :foo) || true + else + end + + ScratchPad.recorded.should == [] + end + + it "takes guards into account when there are several matching patterns" do + case 0 + in 0 if false + :foo + in 0 if true + :bar + end.should == :bar + end + + it "executes else clause if no guarded pattern matches" do + case 0 + in 0 if false + true + else + false + end.should == false end - it "executes else clause if no pattern matches" do - eval(<<~RUBY).should == false + it "raises NoMatchingPatternError if no guarded pattern matches and no else clause" do + -> { case [0, 1] - in [0] - true - else - false + in [0, 1] if false end - RUBY + }.should.raise(NoMatchingPatternError, /\[0, 1\]/) end + 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\]/) + describe "value pattern" do + it "matches an object such that pattern === object" do + case 0 + in 0 + true + end.should == true + + case 0 + in ( + -1..1) + true + end.should == true + + case 0 + in Integer + true + end.should == true + + case "0" + in /0/ + true + end.should == true + + case "0" + in -> s { s == "0" } + true + end.should == true 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/) + it "allows string literal with interpolation" do + x = "x" + + case "x" + in "#{x + ""}" + true + end.should == true end + end - describe "guards" do - it "supports if guard" do - eval(<<~RUBY).should == false - case 0 - in 0 if false - true - else - false - end - RUBY + describe "variable pattern" do + it "matches a value and binds variable name to this value" do + case 0 + in a + a + end.should == 0 + end - eval(<<~RUBY).should == true - case 0 - in 0 if true - true - else - false - end - RUBY + it "makes bounded variable visible outside a case statement scope" do + case 0 + in a end - it "supports unless guard" do - eval(<<~RUBY).should == false - case 0 - in 0 unless true - true - else - false - end - RUBY + a.should == 0 + end - eval(<<~RUBY).should == true - case 0 - in 0 unless false - true - else - false - end - RUBY + it "create local variables even if a pattern doesn't match" do + case 0 + in a + in b + in c 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 + [a, b, c].should == [0, nil, nil] + end + + it "allow using _ name to drop values" do + case [0, 1] + in [a, _] + a + end.should == 0 + end - it "does not evaluate guard if pattern does not match" do + it "supports using _ in a pattern several times" do + case [0, 1, 2] + in [0, _, _] + true + end.should == true + end + + it "supports using any name with _ at the beginning in a pattern several times" do + case [0, 1, 2] + in [0, _x, _x] + true + end.should == true + + case {a: 0, b: 1, c: 2} + in {a: 0, b: _x, c: _x} + true + end.should == true + 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(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 + case 0 + in ^a + true + end.should == true + end + + it "allows applying ^ operator to bound variables" do + case [1, 1] + in [n, ^n] + n + end.should == 1 + + case [1, 2] + in [n, ^n] + true + else + false + end.should == false + end - it "executes else clause if no guarded pattern matches" do - eval(<<~RUBY).should == false - case 0 - in 0 if false - true + 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(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 + case 0 + in 0 | 1 | 2 + true + end.should == true 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(SyntaxError) + end - eval(<<~RUBY).should == true - case 0 - in (-1..1) - true - end - RUBY + it "support underscore prefixed variables in alternation" do + case [0, 1] + in [1, _] + false + in [0, 0] | [0, _a] + true + end.should == true + end - eval(<<~RUBY).should == true - case 0 - in Integer - true - end - RUBY + it "can be used as a nested pattern" do + case [[1], ["2"]] + in [[0] | nil, _] + false + in [[1], [1]] + false + in [[1], [2 | "2"]] + true + end.should == true + + case [1, 2] + in [0, _] | {a: 0} + false + in {a: 1, b: 2} | [1, 2] + true + end.should == true + end + end - eval(<<~RUBY).should == true - case "0" - in /0/ - true - end - RUBY + describe "AS pattern" do + it "binds a variable to a value if pattern matches" do + case 0 + in Integer => n + n + end.should == 0 + end - eval(<<~RUBY).should == true - case "0" - in ->(s) { s == "0" } - true - end - RUBY - end + it "can be used as a nested pattern" do + case [1, [2, 3]] + in [1, Array => ary] + ary + end.should == [2, 3] + end + end - it "allows string literal with interpolation" do - x = "x" + describe "Array pattern" do + it "supports form Constant(pat, pat, ...)" do + case [0, 1, 2] + in Array(0, 1, 2) + true + end.should == true + end - eval(<<~RUBY).should == true - case "x" - in "#{x + ""}" - true - end - RUBY - end + it "supports form Constant[pat, pat, ...]" do + case [0, 1, 2] + in Array[0, 1, 2] + true + end.should == true 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 [pat, pat, ...]" do + case [0, 1, 2] + in [0, 1, 2] + true + end.should == true + 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 + case [0, 1, 2] + in 0, 1, 2 + true + end.should == true + + case [0, 1, 2] + in 0, a, 2 + a + end.should == 1 + + case [0, 1, 2] + in 0, *rest + rest + end.should == [1, 2] + end - a - RUBY + 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 "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 + case obj + in [Integer, Integer] + true + end.should == true + end - [a, b, c] - RUBY - end + it "calls #deconstruct once for multiple patterns, caching the result" do + obj = Object.new - it "allow using _ name to drop values" do - eval(<<~RUBY).should == 0 - case [0, 1] - in [a, _] - a - end - RUBY + def obj.deconstruct + ScratchPad << :deconstruct + [0, 1] end - it "supports using _ in a pattern several times" do - eval(<<~RUBY).should == 2 - case [0, 1, 2] - in [0, _, _] - _ - end - RUBY - end + case obj + in [1, 2] + false + in [0, 1] + true + end.should == true - it "supports using any name with _ at the beginning in a pattern several times" do - eval(<<~RUBY).should == 2 - case [0, 1, 2] - in [0, _x, _x] - _x - end - RUBY + ScratchPad.recorded.should == [:deconstruct] + end - eval(<<~RUBY).should == 2 - case {a: 0, b: 1, c: 2} - in {a: 0, b: _x, c: _x} - _x - end - RUBY - end + it "calls #deconstruct even on objects that are already an array" do + obj = [1, 2] - 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/) + def obj.deconstruct + ScratchPad << :deconstruct + [3, 4] end - it "supports existing variables in a pattern specified with ^ operator" do - a = 0 + case obj + in [3, 4] + true + else + false + end.should == true - 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 + case [0, 1, 2] + in String[0, 1, 2] + true + else + false + end.should == false + 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) + + case obj + in String[1] + true + else + false + end.should == false + 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) + + case obj + in Object[] + true + else + false + end.should == false 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 + it "raises TypeError if #deconstruct method does not return array" do + obj = Object.new - 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/) + def obj.deconstruct + "" 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 + -> { + case obj + in Object[] + else + end + }.should.raise(TypeError, /deconstruct must return Array/) 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 "accepts a subclass of Array from #deconstruct" do + obj = Object.new - 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 + def obj.deconstruct + Class.new(Array).new([0, 1]) end + + case obj + in [1, 2] + false + in [0, 1] + true + end.should == true 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 "does not match object if elements of array returned by #deconstruct method does not match elements in pattern" do + obj = Object.new - it "supports form Constant[pat, pat, ...]" do - eval(<<~RUBY).should == true - case [0, 1, 2] - in Array[0, 1, 2] - true - end - RUBY + def obj.deconstruct + [1] end - it "supports form [pat, pat, ...]" do - eval(<<~RUBY).should == true - case [0, 1, 2] - in [0, 1, 2] - true - end - RUBY - end + case obj + in Object[0] + true + else + false + end.should == false + end - it "supports form pat, pat, ..." do - eval(<<~RUBY).should == true - case [0, 1, 2] - in 0, 1, 2 - true - end - RUBY + it "binds variables" do + case [0, 1, 2] + in [a, b, c] + [a, b, c] + end.should == [0, 1, 2] + end - eval(<<~RUBY).should == 1 - case [0, 1, 2] - in 0, a, 2 - a - end - RUBY + it "supports splat operator *rest" do + case [0, 1, 2] + in [0, *rest] + rest + end.should == [1, 2] + end - eval(<<~RUBY).should == [1, 2] - case [0, 1, 2] - in 0, *rest - rest - end - RUBY - end + it "does not match partially by default" do + case [0, 1, 2, 3] + in [1, 2] + true + else + false + end.should == false + 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 "does match partially from the array beginning if list + , syntax used" do + case [0, 1, 2, 3] + in [0, 1, ] + true + end.should == true - eval(<<~RUBY).should == true - case obj - in [Integer, Integer] - true - end - RUBY - end + case [0, 1, 2, 3] + in 0, 1,; + true + end.should == true + 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 "matches [] with []" do + case [] + in [] + true + end.should == true + end - it "does not match object without #deconstruct method" do - obj = Object.new + it "matches anything with *" do + case [0, 1] + in *; + true + end.should == true + end - eval(<<~RUBY).should == false - case obj - in Object[] - true - else - false - end - RUBY - end + it "can be used as a nested pattern" do + case [[1], ["2"]] + in [[0] | nil, _] + false + in [[1], [1]] + false + in [[1], [2 | "2"]] + true + end.should == true + + case [1, 2] + in [0, _] | {a: 0} + false + in {a: 1, b: 2} | [1, 2] + true + end.should == true + end + end - 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 - }.should raise_error(TypeError, /deconstruct must return Array/) - end + describe "Hash pattern" do + it "supports form Constant(id: pat, id: pat, ...)" do + case {a: 0, b: 1} + in Hash(a: 0, b: 1) + true + end.should == true + 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 "supports form Constant[id: pat, id: pat, ...]" do + case {a: 0, b: 1} + in Hash[a: 0, b: 1] + true + end.should == true + end - eval(<<~RUBY).should == false - case obj - in Object[0] - true - else - false - end - RUBY - end + it "supports form {id: pat, id: pat, ...}" do + case {a: 0, b: 1} + in {a: 0, b: 1} + true + end.should == true + 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 id: pat, id: pat, ..." do + case {a: 0, b: 1} + in a: 0, b: 1 + true + end.should == true + + case {a: 0, b: 1} + in a: a, b: b + [a, b] + end.should == [0, 1] + + case {a: 0, b: 1, c: 2} + in a: 0, **rest + rest + end.should == {b: 1, c: 2} + end - it "binds variable even if patter matches only partially" do - a = nil + it "supports a: which means a: a" do + case {a: 0, b: 1} + in Hash(a:, b:) + [a, b] + end.should == [0, 1] - eval(<<~RUBY).should == 0 - case [0, 1, 2] - in [a, 1, 3] - else - end + a = b = nil - a - RUBY - end + case {a: 0, b: 1} + in Hash[a:, b:] + [a, b] + end.should == [0, 1] - it "supports splat operator *rest" do - eval(<<~RUBY).should == [1, 2] - case [0, 1, 2] - in [0, *rest] - rest - end - RUBY - end + a = b = nil - 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 + case {a: 0, b: 1} + in {a:, b:} + [a, b] + end.should == [0, 1] - 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 + a = nil - eval(<<~RUBY).should == true - case [0, 1, 2, 3] - in 0, 1,; - true - end - RUBY - end + case {a: 0, b: 1, c: 2} + in {a:, **rest} + [a, rest] + end.should == [0, {b: 1, c: 2}] - it "matches [] with []" do - eval(<<~RUBY).should == true - case [] - in [] - true - end - RUBY - end + a = b = nil - it "matches anything with *" do - eval(<<~RUBY).should == true - case [0, 1] - in *; - true - end - RUBY - end + case {a: 0, b: 1} + in a:, b: + [a, b] + end.should == [0, 1] + end + + it "can mix key (a:) and key-value (a: b) declarations" do + case {a: 0, b: 1} + in Hash(a:, b: x) + [a, x] + end.should == [0, 1] + end + + it "supports 'string': key literal" do + case {a: 0} + in {"a": 0} + true + end.should == true 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 + it "does not support non-symbol keys" do + -> { + eval <<~RUBY + case {a: 1} + in {"a" => 1} end RUBY - end + }.should.raise(SyntaxError, /unexpected|expected a label as the key in the hash pattern/) + 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 + it "does not support string interpolation in keys" do + -> { + eval <<~'RUBY' + case {a: 1} + in {"#{x}": 1} end RUBY - end + }.should.raise(SyntaxError, /symbol literal with interpolation is not allowed|expected a label as the key in the hash pattern/) + end - it "supports form {id: pat, id: pat, ...}" do - eval(<<~RUBY).should == true - case {a: 0, b: 1} - in {a: 0, b: 1} - true + 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(SyntaxError, /duplicated key name/) + 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 "supports form id: pat, id: pat, ..." do - eval(<<~RUBY).should == true - case {a: 0, b: 1} - in a: 0, b: 1 - true - end - RUBY + case obj + in {a: 1} + true + end.should == true + end - eval(<<~RUBY).should == [0, 1] - case {a: 0, b: 1} - in a: a, b: b - [a, b] - end - RUBY + it "calls #deconstruct_keys per pattern" do + obj = Object.new - eval(<<~RUBY).should == { b: 1, c: 2 } - case {a: 0, b: 1, c: 2} - in a: 0, **rest - rest - end - RUBY + def obj.deconstruct_keys(*) + ScratchPad << :deconstruct_keys + {a: 1} 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 + case obj + in {b: 1} + false + in {a: 1} + true + end.should == true - a = b = nil - eval(<<~RUBY).should == [0, 1] - case {a: 0, b: 1} - in Hash[a:, b:] - [a, b] - end - RUBY + ScratchPad.recorded.should == [:deconstruct_keys, :deconstruct_keys] + 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 Constant === object returns false" do + case {a: 1} + in String[a: 1] + true + else + false + end.should == false + 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 + it "checks Constant === object before calling #deconstruct_keys" do + c1 = Class.new + obj = c1.new + obj.should_not_receive(:deconstruct_keys) + + case obj + in String(a: 1) + true + else + false + end.should == false + end - a = b = nil - eval(<<~RUBY).should == [0, 1] - case {a: 0, b: 1} - in a:, b: - [a, b] - 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 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 + case obj + in Object[a: 1] + true + else + false + end.should == false + end - it "supports 'string': key literal" do - eval(<<~RUBY).should == true - case {a: 0} - in {"a": 0} - true - end - RUBY - end + it "does not match object if #deconstruct_keys method does not return Hash" do + obj = Object.new - it "does not support non-symbol keys" do - -> { - eval <<~RUBY - case {a: 1} - in {"a" => 1} - end - RUBY - }.should raise_error(SyntaxError, /unexpected/) + def obj.deconstruct_keys(*) + "" end - it "does not support string interpolation in keys" do - x = "a" + -> { + case obj + in Object[a: 1] + end + }.should.raise(TypeError, /deconstruct_keys must return Hash/) + end - -> { - eval <<~'RUBY' - case {a: 1} - in {"#{x}": 1} - end - RUBY - }.should raise_error(SyntaxError, /symbol literal with interpolation is not allowed/) - end + it "does not match object if #deconstruct_keys method returns Hash with non-symbol keys" do + obj = Object.new - 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/) + def obj.deconstruct_keys(*) + {"a" => 1} 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 + case obj + in Object[a: 1] + true + else + false + end.should == false + end - eval(<<~RUBY).should == true - case obj - in {a: 1} - true - end - RUBY - 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 - 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 + def obj.deconstruct_keys(*) + {a: 1} end - it "does not match object without #deconstruct_keys method" do - obj = Object.new + case obj + in Object[a: 2] + true + else + false + end.should == false + end - eval(<<~RUBY).should == false - case obj - in Object[a: 1] - true - else - false - end - RUBY + it "passes keys specified in pattern as arguments to #deconstruct_keys method" do + obj = Object.new + + def obj.deconstruct_keys(*args) + ScratchPad << args + {a: 1, b: 2, c: 3} end - it "does not match object if #deconstruct_keys method does not return Hash" do - obj = Object.new - def obj.deconstruct_keys(*); "" end - - -> { - eval <<~RUBY - case obj - in Object[a: 1] - end - RUBY - }.should raise_error(TypeError, /deconstruct_keys must return Hash/) + case obj + in Object[a: 1, b: 2, c: 3] end - 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 + ScratchPad.recorded.sort.should == [[[:a, :b, :c]]] + end - eval(<<~RUBY).should == false - case obj - in Object[a: 1] - true - else - false - end - RUBY - end + it "passes keys specified in pattern to #deconstruct_keys method if pattern contains double splat operator **" do + obj = Object.new - 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 + def obj.deconstruct_keys(*args) + ScratchPad << args + {a: 1, b: 2, c: 3} + end - eval(<<~RUBY).should == false - case obj - in Object[a: 2] - true - else - false - end - RUBY + case obj + in Object[a: 1, b: 2, **] end - it "passes keys specified in pattern as arguments to #deconstruct_keys method" do - obj = Object.new + ScratchPad.recorded.sort.should == [[[:a, :b]]] + end - def obj.deconstruct_keys(*args) - ScratchPad << args - {a: 1, b: 2, c: 3} - end + it "passes nil to #deconstruct_keys method if pattern contains double splat operator **rest" do + obj = Object.new - eval <<~RUBY - case obj - in Object[a: 1, b: 2, c: 3] - end - RUBY + def obj.deconstruct_keys(*args) + ScratchPad << args + {a: 1, b: 2} + end - ScratchPad.recorded.should == [[[:a, :b, :c]]] + case obj + in Object[a: 1, **rest] end - it "passes keys specified in pattern to #deconstruct_keys method if pattern contains double splat operator **" do - obj = Object.new + ScratchPad.recorded.should == [[nil]] + end - def obj.deconstruct_keys(*args) - ScratchPad << args - {a: 1, b: 2, c: 3} - end + it "binds variables" do + case {a: 0, b: 1, c: 2} + in {a: x, b: y, c: z} + [x, y, z] + end.should == [0, 1, 2] + end - eval <<~RUBY - case obj - in Object[a: 1, b: 2, **] - end - RUBY + it "supports double splat operator **rest" do + case {a: 0, b: 1, c: 2} + in {a: 0, **rest} + rest + end.should == {b: 1, c: 2} + end - ScratchPad.recorded.should == [[[:a, :b]]] - end + it "treats **nil like there should not be any other keys in a matched Hash" do + case {a: 1, b: 2} + in {a: 1, b: 2, **nil} + true + end.should == true + + case {a: 1, b: 2} + in {a: 1, **nil} + true + else + false + end.should == false + end - it "passes nil to #deconstruct_keys method if pattern contains double splat operator **rest" do - obj = Object.new + it "can match partially" do + case {a: 1, b: 2} + in {a: 1} + true + end.should == true + end - def obj.deconstruct_keys(*args) - ScratchPad << args - {a: 1, b: 2} - end + it "matches {} with {}" do + case {} + in {} + true + end.should == true + end - eval <<~RUBY - case obj - in Object[a: 1, **rest] - end - RUBY + it "in {} only matches empty hashes" do + case {a: 1} + in {} + true + else + false + end.should == false + end - ScratchPad.recorded.should == [[nil]] - end + it "in {**nil} only matches empty hashes" do + case {} + in {**nil} + true + else + false + end.should == true + + case {a: 1} + in {**nil} + true + else + false + end.should == false + end + + it "matches anything with **" do + case {a: 1} + in **; + true + end.should == true + 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 "can be used as a nested pattern" do + 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 + end.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.should == true + 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 "binds variable even if pattern matches only partially" do - x = nil + result = nil + Module.new do + using refinery - eval(<<~RUBY).should == 0 - case {a: 0, b: 1} - in {a: x, b: 2} - else + result = + case [] + in [0] + true end - - x - 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 + result.should == true + end + + it "are used for #deconstruct_keys" do + refinery = Module.new do + refine Hash do + def deconstruct_keys(_) + {a: 0} end - RUBY + end 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 + result = nil + Module.new do + using refinery - eval(<<~RUBY).should == false - case {a: 1, b: 2} - in {a: 1, **nil} - true - else - false + result = + case {} + in a: 0 + true end - RUBY end - it "can match partially" do - eval(<<~RUBY).should == true - case {a: 1, b: 2} - in {a: 1} - true + result.should == true + 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 - RUBY + end end - it "matches {} with {}" do - eval(<<~RUBY).should == true + result = nil + Module.new do + using refinery + + result = case {} - in {} - true + in Array + true end - RUBY end - it "matches anything with **" do - eval(<<~RUBY).should == true - case {a: 1} - in **; - true + result.should == true + end + end + + describe "Ruby 3.1 improvements" do + it "can omit parentheses in one line pattern matching" do + [1, 2] => a, b + [a, b].should == [1, 2] + + {a: 1} => a: + a.should == 1 + end + + it "supports pinning instance variables" do + @a = /a/ + case 'abc' + in ^@a + true + end.should == true + end + + it "supports pinning class variables" do + result = nil + Module.new do + # avoid "class variable access from toplevel" runtime error with #module_eval + result = module_eval(<<~RUBY) + @@a = 0..10 + + case 2 + in ^@@a + true end RUBY end + + result.should == true end - describe "refinements" do - it "are used for #deconstruct" do - refinery = Module.new do - refine Array do - def deconstruct - [0] - end - end - end + it "supports pinning global variables" do + $a = /a/ + case 'abc' + in ^$a + true + end.should == true + end - result = nil - Module.new do - using refinery + it "supports pinning expressions" do + case 'abc' + in ^(/a/) + true + end.should == true - result = eval(<<~RUBY) - case [] - in [0] - true - end - RUBY - end + case 0 + in ^(0 + 0) + true + end.should == true + end - result.should == true - end + it "supports pinning expressions in array pattern" do + case [3] + in [^(1 + 2)] + true + end.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 + it "supports pinning expressions in hash pattern" do + case {name: '2.6', released_at: Time.new(2018, 12, 25)} + in {released_at: ^(Time.new(2010)..Time.new(2020))} + true + end.should == true + end + end - result = nil - Module.new do - using refinery + describe "value in pattern" do + it "returns true if the pattern matches" do + (1 in 1).should == true - result = eval(<<~RUBY) - case {} - in a: 0 - true - end - RUBY - end + (1 in Integer).should == true - result.should == true - end + e = nil + ([1, 2] in [1, e]).should == true + e.should == 2 - 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 + k = nil + ({k: 1} in {k:}).should == true + k.should == 1 + end - result = nil - Module.new do - using refinery + it "returns false if the pattern does not match" do + (1 in 2).should == false - result = eval(<<~RUBY) - case {} - in Array - true - end - RUBY - end + (1 in Float).should == false - result.should == true - end + ([1, 2] in [2, e]).should == false + + ({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..edb990525e 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 @@ -253,12 +251,12 @@ describe "Operators" do end it "<=> == === != =~ !~ are non-associative" do - -> { eval("1 <=> 2 <=> 3") }.should raise_error(SyntaxError) - -> { eval("1 == 2 == 3") }.should raise_error(SyntaxError) - -> { eval("1 === 2 === 3") }.should raise_error(SyntaxError) - -> { eval("1 != 2 != 3") }.should raise_error(SyntaxError) - -> { eval("1 =~ 2 =~ 3") }.should raise_error(SyntaxError) - -> { eval("1 !~ 2 !~ 3") }.should raise_error(SyntaxError) + -> { eval("1 <=> 2 <=> 3") }.should.raise(SyntaxError) + -> { eval("1 == 2 == 3") }.should.raise(SyntaxError) + -> { eval("1 === 2 === 3") }.should.raise(SyntaxError) + -> { eval("1 != 2 != 3") }.should.raise(SyntaxError) + -> { eval("1 =~ 2 =~ 3") }.should.raise(SyntaxError) + -> { eval("1 !~ 2 !~ 3") }.should.raise(SyntaxError) end it "<=> == === != =~ !~ have higher precedence than &&" do @@ -292,18 +290,18 @@ describe "Operators" do end it ".. ... are non-associative" do - -> { eval("1..2..3") }.should raise_error(SyntaxError) - -> { eval("1...2...3") }.should raise_error(SyntaxError) - end - - it ".. ... have higher precedence than ? :" do - # Use variables to avoid warnings - from = 1 - to = 2 - # These are flip-flop, not Range instances - (from..to ? 3 : 4).should == 3 - (from...to ? 3 : 4).should == 3 - end + -> { eval("1..2..3") }.should.raise(SyntaxError) + -> { eval("1...2...3") }.should.raise(SyntaxError) + end + + it ".. ... have higher precedence than ? :" do + # Use variables to avoid warnings + from = 1 + to = 2 + # These are flip-flop, not Range instances + (from..to ? 3 : 4).should == 3 + (from...to ? 3 : 4).should == 3 + end it "? : is right-associative" do (true ? 2 : 3 ? 4 : 5).should == 2 diff --git a/spec/ruby/language/predefined_spec.rb b/spec/ruby/language/predefined_spec.rb index 764c96e838..d57b924bcf 100644 --- a/spec/ruby/language/predefined_spec.rb +++ b/spec/ruby/language/predefined_spec.rb @@ -1,4 +1,5 @@ require_relative '../spec_helper' +require_relative '../core/exception/shared/set_backtrace' require 'stringio' # The following tables are excerpted from Programming Ruby: The Pragmatic Programmer's Guide' @@ -7,48 +8,44 @@ 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 it "is set to contain the MatchData object of the last match if successful" do md = /foo/.match 'foo' - $~.should be_kind_of(MatchData) - $~.should equal md + $~.should.is_a?(MatchData) + $~.should.equal? md /bar/ =~ 'bar' - $~.should be_kind_of(MatchData) - $~.should_not equal md + $~.should.is_a?(MatchData) + $~.should_not.equal? md end it "is set to nil if the last match was unsuccessful" do @@ -75,7 +72,7 @@ describe "Predefined global $~" do match2.should_not == nil $~.should == match2 - eval 'match3 = /baz/.match("baz")' + match3 = /baz/.match("baz") match3.should_not == nil $~.should == match3 @@ -90,10 +87,10 @@ describe "Predefined global $~" do $~ = nil $~.should == nil $~ = /foo/.match("foo") - $~.should be_an_instance_of(MatchData) + $~.should.instance_of?(MatchData) - -> { $~ = Object.new }.should raise_error(TypeError) - -> { $~ = 1 }.should raise_error(TypeError) + -> { $~ = Object.new }.should.raise(TypeError, 'wrong argument type Object (expected MatchData)') + -> { $~ = 1 }.should.raise(TypeError, 'wrong argument type Integer (expected MatchData)') end it "changes the value of derived capture globals when assigned" do @@ -137,8 +134,21 @@ describe "Predefined global $&" do end it "sets the encoding to the encoding of the source String" do - "abc".force_encoding(Encoding::EUC_JP) =~ /b/ - $&.encoding.should equal(Encoding::EUC_JP) + "abc".dup.force_encoding(Encoding::EUC_JP) =~ /b/ + $&.encoding.should.equal?(Encoding::EUC_JP) + end + + it "is read-only" do + -> { + eval %q{$& = ""} + }.should.raise(SyntaxError, /Can't set variable \$&/) + end + + it "is read-only when aliased" do + alias $predefined_spec_ampersand $& + -> { + $predefined_spec_ampersand = "" + }.should.raise(NameError, '$predefined_spec_ampersand is a read-only variable') end end @@ -150,13 +160,26 @@ describe "Predefined global $`" do end it "sets the encoding to the encoding of the source String" do - "abc".force_encoding(Encoding::EUC_JP) =~ /b/ - $`.encoding.should equal(Encoding::EUC_JP) + "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/ - $`.encoding.should equal(Encoding::ISO_8859_1) + "abc".dup.force_encoding(Encoding::ISO_8859_1) =~ /a/ + $`.encoding.should.equal?(Encoding::ISO_8859_1) + end + + it "is read-only" do + -> { + eval %q{$` = ""} + }.should.raise(SyntaxError, /Can't set variable \$`/) + end + + it "is read-only when aliased" do + alias $predefined_spec_backquote $` + -> { + $predefined_spec_backquote = "" + }.should.raise(NameError, '$predefined_spec_backquote is a read-only variable') end end @@ -168,13 +191,26 @@ describe "Predefined global $'" do end it "sets the encoding to the encoding of the source String" do - "abc".force_encoding(Encoding::EUC_JP) =~ /b/ - $'.encoding.should equal(Encoding::EUC_JP) + "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/ - $'.encoding.should equal(Encoding::ISO_8859_1) + "abc".dup.force_encoding(Encoding::ISO_8859_1) =~ /c/ + $'.encoding.should.equal?(Encoding::ISO_8859_1) + end + + it "is read-only" do + -> { + eval %q{$' = ""} + }.should.raise(SyntaxError, /Can't set variable \$'/) + end + + it "is read-only when aliased" do + alias $predefined_spec_single_quote $' + -> { + $predefined_spec_single_quote = "" + }.should.raise(NameError, '$predefined_spec_single_quote is a read-only variable') end end @@ -191,8 +227,21 @@ describe "Predefined global $+" do end it "sets the encoding to the encoding of the source String" do - "abc".force_encoding(Encoding::EUC_JP) =~ /(b)/ - $+.encoding.should equal(Encoding::EUC_JP) + "abc".dup.force_encoding(Encoding::EUC_JP) =~ /(b)/ + $+.encoding.should.equal?(Encoding::EUC_JP) + end + + it "is read-only" do + -> { + eval %q{$+ = ""} + }.should.raise(SyntaxError, /Can't set variable \$\+/) + end + + it "is read-only when aliased" do + alias $predefined_spec_plus $+ + -> { + $predefined_spec_plus = "" + }.should.raise(NameError, '$predefined_spec_plus is a read-only variable') end end @@ -218,8 +267,8 @@ 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)/ - $1.encoding.should equal(Encoding::EUC_JP) + "abc".dup.force_encoding(Encoding::EUC_JP) =~ /(b)/ + $1.encoding.should.equal?(Encoding::EUC_JP) end end @@ -233,20 +282,36 @@ describe "Predefined global $stdout" do end it "raises TypeError error if assigned to nil" do - -> { $stdout = nil }.should raise_error(TypeError) + -> { $stdout = nil }.should.raise(TypeError, '$stdout must have write method, NilClass given') end it "raises TypeError error if assigned to object that doesn't respond to #write" do obj = mock('object') - -> { $stdout = obj }.should raise_error(TypeError) + -> { $stdout = obj }.should.raise(TypeError) obj.stub!(:write) $stdout = obj - $stdout.should equal(obj) + $stdout.should.equal?(obj) end end describe "Predefined global $!" do + it "is Fiber-local" do + Fiber.new do + raise "hi" + rescue + Fiber.yield + end.resume + + $!.should == nil + end + + it "is read-only" do + -> { + $! = [] + }.should.raise(NameError, '$! is a read-only variable') + end + # See http://jira.codehaus.org/browse/JRUBY-5550 it "remains nil after a failed core class \"checked\" coercion against a class that defines method_missing" do $!.should == nil @@ -506,41 +571,108 @@ 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. -$. Fixnum 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 +describe "Predefined global $@" do + it "is Fiber-local" do + Fiber.new do + raise "hi" + rescue + Fiber.yield + end.resume + + $@.should == nil + end + + it "is set to a backtrace of a rescued exception" do + begin + raise + rescue + $@.should.instance_of?(Array) + $@.should == $!.backtrace + end + end + + it "is cleared when an exception is rescued" do + begin + raise + rescue + end + + $@.should == nil + end + + it "is not set when there is no current exception" do + $@.should == nil + end + + it "is set to a backtrace of a rescued exception" do + begin + raise + rescue + $@.should.instance_of?(Array) + $@.should == $!.backtrace + end + end + + it "is not read-only" do + begin + raise + rescue + $@ = [] + $@.should == [] + end + end + + it_behaves_like :exception_set_backtrace, -> backtrace { + exception = nil + begin + raise + rescue + $@ = backtrace + exception = $! + end + exception + } + + it "cannot be assigned when there is no a rescued exception" do + -> { + $@ = [] + }.should.raise(ArgumentError, '$! not set') + end +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 @@ -555,40 +687,71 @@ describe "Predefined global $/" do $VERBOSE = @verbose end - it "can be assigned a String" do - str = "abc" - $/ = str - $/.should equal(str) + ruby_version_is ""..."4.0" do + it "can be assigned a String" do + str = +"abc" + $/ = str + $/.should.equal?(str) + end + end + + ruby_version_is "4.0" do + it "makes a new frozen String from the assigned String" do + string_subclass = Class.new(String) + str = string_subclass.new("abc") + str.instance_variable_set(:@ivar, 1) + $/ = str + $/.should.frozen? + $/.should.instance_of?(String) + $/.should_not.instance_variable_defined?(:@ivar) + $/.should == str + end + + it "makes a new frozen String if it's not frozen" do + str = +"abc" + $/ = str + $/.should.frozen? + $/.should == str + end + + it "assigns the given String if it's frozen and has no instance variables" do + str = "abc".freeze + $/ = str + $/.should.equal?(str) + end end it "can be assigned nil" do $/ = nil - $/.should be_nil + $/.should == nil end it "returns the value assigned" do ($/ = "xyz").should == "xyz" end - it "changes $-0" do $/ = "xyz" - $-0.should equal($/) + $-0.should.equal?($/) 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) + -> { $/ = obj }.should.raise(TypeError, 'value of $/ must be String') end - it "raises a TypeError if assigned a Fixnum" do - -> { $/ = 1 }.should raise_error(TypeError) + it "raises a TypeError if assigned an Integer" do + -> { $/ = 1 }.should.raise(TypeError, 'value of $/ must be String') end it "raises a TypeError if assigned a boolean" do - -> { $/ = true }.should raise_error(TypeError) + -> { $/ = true }.should.raise(TypeError, 'value of $/ must be String') + end + + it "warns if assigned non-nil" do + -> { $/ = "_" }.should complain(/warning: (?:non-nil )?[`']\$\/' is deprecated/) end end @@ -605,15 +768,43 @@ describe "Predefined global $-0" do $VERBOSE = @verbose end - it "can be assigned a String" do - str = "abc" - $-0 = str - $-0.should equal(str) + ruby_version_is ""..."4.0" do + it "can be assigned a String" do + str = +"abc" + $-0 = str + $-0.should.equal?(str) + end + end + + ruby_version_is "4.0" do + it "makes a new frozen String from the assigned String" do + string_subclass = Class.new(String) + str = string_subclass.new("abc") + str.instance_variable_set(:@ivar, 1) + $-0 = str + $-0.should.frozen? + $-0.should.instance_of?(String) + $-0.should_not.instance_variable_defined?(:@ivar) + $-0.should == str + end + + it "makes a new frozen String if it's not frozen" do + str = +"abc" + $-0 = str + $-0.should.frozen? + $-0.should == str + end + + it "assigns the given String if it's frozen and has no instance variables" do + str = "abc".freeze + $-0 = str + $-0.should.equal?(str) + end end it "can be assigned nil" do $-0 = nil - $-0.should be_nil + $-0.should == nil end it "returns the value assigned" do @@ -622,22 +813,69 @@ describe "Predefined global $-0" do it "changes $/" do $-0 = "xyz" - $/.should equal($-0) + $/.should.equal?($-0) end it "does not call #to_str to convert the object to a String" do obj = mock("$-0 value") obj.should_not_receive(:to_str) - -> { $-0 = obj }.should raise_error(TypeError) + -> { $-0 = obj }.should.raise(TypeError, 'value of $-0 must be String') end - it "raises a TypeError if assigned a Fixnum" do - -> { $-0 = 1 }.should raise_error(TypeError) + it "raises a TypeError if assigned an Integer" do + -> { $-0 = 1 }.should.raise(TypeError, 'value of $-0 must be String') end it "raises a TypeError if assigned a boolean" do - -> { $-0 = true }.should raise_error(TypeError) + -> { $-0 = true }.should.raise(TypeError, 'value of $-0 must be String') + end + + it "warns if assigned non-nil" do + -> { $-0 = "_" }.should complain(/warning: (?:non-nil )?[`']\$-0' is deprecated/) + 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 == 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(TypeError, 'value of $\ must be String') + end + + it "raises a TypeError if assigned not String" do + -> { $\ = 1 }.should.raise(TypeError, 'value of $\ must be String') + -> { $\ = true }.should.raise(TypeError, 'value of $\ must be String') + end + + it "warns if assigned non-nil" do + -> { $\ = "_" }.should complain(/warning: (?:non-nil )?[`']\$\\' is deprecated/) end end @@ -647,17 +885,15 @@ describe "Predefined global $," do end it "defaults to nil" do - $,.should be_nil + $,.should == nil end it "raises TypeError if assigned a non-String" do - -> { $, = Object.new }.should raise_error(TypeError) + -> { $, = Object.new }.should.raise(TypeError, 'value of $, must be String') 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: (?:non-nil )?[`']\$,' is deprecated/) end end @@ -684,7 +920,7 @@ describe "Predefined global $." do obj = mock("bad-value") obj.should_receive(:to_int).and_return('abc') - -> { $. = obj }.should raise_error(TypeError) + -> { $. = obj }.should.raise(TypeError) end end @@ -693,10 +929,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: (?:non-nil )?[`']\$;' is deprecated/) end end @@ -730,7 +964,7 @@ describe "Predefined global $_" do match.should == "bar\n" $_.should == match - eval 'match = stdin.gets' + match = stdin.gets match.should == "baz\n" $_.should == match @@ -751,7 +985,7 @@ describe "Predefined global $_" do end Thread.pass until running - $_.should be_nil + $_.should == nil thr.join end @@ -769,54 +1003,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] -$$ Fixnum 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 @@ -824,7 +1056,7 @@ describe "Execution variable $:" do end it "does not include the current directory" do - $:.should_not include(".") + $:.should_not.include?(".") end it "is the same object as $LOAD_PATH and $-I" do @@ -834,37 +1066,55 @@ describe "Execution variable $:" do it "can be changed via <<" do $: << "foo" - $:.should include("foo") + $:.should.include?("foo") + ensure + $:.delete("foo") end it "is read-only" do -> { $: = [] - }.should raise_error(NameError) + }.should.raise(NameError, '$: is a read-only variable') -> { $LOAD_PATH = [] - }.should raise_error(NameError) + }.should.raise(NameError, '$LOAD_PATH is a read-only variable') -> { $-I = [] - }.should raise_error(NameError) + }.should.raise(NameError, '$-I is a read-only variable') + 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) + + if platform_is :windows + # See https://github.com/ruby/setup-ruby/pull/762#issuecomment-2917460440 + $:.should.find { |e| File.realdirpath(e) == RbConfig::CONFIG['sitelibdir'] } + idx = $:.index { |e| File.realdirpath(e) == RbConfig::CONFIG['sitelibdir'] } + else + $:.should.include?(RbConfig::CONFIG['sitelibdir']) + idx = $:.index(RbConfig::CONFIG['sitelibdir']) + end + + $:[idx..-1].all? { |p| p.instance_variable_defined?(:@gem_prelude_index) }.should == true + $:[0...idx].all? { |p| !p.instance_variable_defined?(:@gem_prelude_index) }.should == true end end describe "Global variable $\"" do it "is an alias for $LOADED_FEATURES" do - $".should equal $LOADED_FEATURES + $".should.equal? $LOADED_FEATURES end it "is read-only" do -> { $" = [] - }.should raise_error(NameError) + }.should.raise(NameError, '$" is a read-only variable') -> { $LOADED_FEATURES = [] - }.should raise_error(NameError) + }.should.raise(NameError, '$LOADED_FEATURES is a read-only variable') end end @@ -872,7 +1122,7 @@ describe "Global variable $<" do it "is read-only" do -> { $< = nil - }.should raise_error(NameError) + }.should.raise(NameError, '$< is a read-only variable') end end @@ -880,7 +1130,7 @@ describe "Global variable $FILENAME" do it "is read-only" do -> { $FILENAME = "-" - }.should raise_error(NameError) + }.should.raise(NameError, '$FILENAME is a read-only variable') end end @@ -888,30 +1138,30 @@ describe "Global variable $?" do it "is read-only" do -> { $? = nil - }.should raise_error(NameError) + }.should.raise(NameError, '$? is a read-only variable') end it "is thread-local" do system(ruby_cmd('exit 0')) - Thread.new { $?.should be_nil }.join + Thread.new { $?.should == nil }.join end end describe "Global variable $-a" do it "is read-only" do - -> { $-a = true }.should raise_error(NameError) + -> { $-a = true }.should.raise(NameError, '$-a is a read-only variable') end end describe "Global variable $-l" do it "is read-only" do - -> { $-l = true }.should raise_error(NameError) + -> { $-l = true }.should.raise(NameError, '$-l is a read-only variable') end end describe "Global variable $-p" do it "is read-only" do - -> { $-p = true }.should raise_error(NameError) + -> { $-p = true }.should.raise(NameError, '$-p is a read-only variable') end end @@ -926,9 +1176,9 @@ describe "Global variable $-d" do it "is an alias of $DEBUG" do $DEBUG = true - $-d.should be_true + $-d.should == true $-d = false - $DEBUG.should be_false + $DEBUG.should == false end end @@ -941,21 +1191,25 @@ describe "Global variable $VERBOSE" do $VERBOSE = @verbose end + it "is false by default" do + $VERBOSE.should == false + end + it "converts truthy values to true" do [true, 1, 0, [], ""].each do |true_value| $VERBOSE = true_value - $VERBOSE.should be_true + $VERBOSE.should == true end end it "allows false" do $VERBOSE = false - $VERBOSE.should be_false + $VERBOSE.should == false end it "allows nil without coercing to false" do $VERBOSE = nil - $VERBOSE.should be_nil + $VERBOSE.should == nil end end @@ -970,9 +1224,9 @@ describe :verbose_global_alias, shared: true do it "is an alias of $VERBOSE" do $VERBOSE = true - eval(@method).should be_true + eval(@method).should == true eval("#{@method} = false") - $VERBOSE.should be_false + $VERBOSE.should == false end end @@ -995,7 +1249,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 @@ -1009,7 +1263,7 @@ describe "Global variable $0" do it "actually sets the program name" do title = "rubyspec-dollar0-test" $0 = title - `ps -ocommand= -p#{$$}`.should include(title) + `ps -ocommand= -p#{$$}`.should.include?(title) end end @@ -1018,26 +1272,24 @@ describe "Global variable $0" do end it "raises a TypeError when not given an object that can be coerced to a String" do - -> { $0 = nil }.should raise_error(TypeError) + -> { $0 = nil }.should.raise(TypeError) 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 @@ -1056,69 +1308,88 @@ end describe "The predefined standard object nil" do it "is an instance of NilClass" do - nil.should be_kind_of(NilClass) + nil.should.is_a?(NilClass) end it "raises a SyntaxError if assigned to" do - -> { eval("nil = true") }.should raise_error(SyntaxError) + -> { eval("nil = true") }.should.raise(SyntaxError, /Can't assign to nil/) end end describe "The predefined standard object true" do it "is an instance of TrueClass" do - true.should be_kind_of(TrueClass) + true.should.is_a?(TrueClass) end it "raises a SyntaxError if assigned to" do - -> { eval("true = false") }.should raise_error(SyntaxError) + -> { eval("true = false") }.should.raise(SyntaxError, /Can't assign to true/) end end describe "The predefined standard object false" do it "is an instance of FalseClass" do - false.should be_kind_of(FalseClass) + false.should.is_a?(FalseClass) end it "raises a SyntaxError if assigned to" do - -> { eval("false = nil") }.should raise_error(SyntaxError) + -> { eval("false = nil") }.should.raise(SyntaxError, /Can't assign to false/) end end describe "The self pseudo-variable" do it "raises a SyntaxError if assigned to" do - -> { eval("self = 1") }.should raise_error(SyntaxError) + -> { eval("self = 1") }.should.raise(SyntaxError, /Can't change the value of self/) 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 +1417,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 +1431,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 nil for the internal encoding" do + STDIN.internal_encoding.should == nil + 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 despite Encoding.default_internal being changed" do + Encoding.default_internal = Encoding::IBM437 + STDIN.internal_encoding.should == nil + end end it "has the encodings set by #set_encoding" do @@ -1182,25 +1463,16 @@ 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 it "has nil for the external encoding" do - STDOUT.external_encoding.should be_nil + STDOUT.external_encoding.should == nil end it "has nil for the external encoding despite Encoding.default_external being changed" do Encoding.default_external = Encoding::ISO_8859_1 - STDOUT.external_encoding.should be_nil + STDOUT.external_encoding.should == nil end it "has the encodings set by #set_encoding" do @@ -1210,23 +1482,23 @@ describe "The predefined global constant" do end it "has nil for the internal encoding" do - STDOUT.internal_encoding.should be_nil + STDOUT.internal_encoding.should == nil end it "has nil for the internal encoding despite Encoding.default_internal being changed" do Encoding.default_internal = Encoding::IBM437 - STDOUT.internal_encoding.should be_nil + STDOUT.internal_encoding.should == nil end end describe "STDERR" do it "has nil for the external encoding" do - STDERR.external_encoding.should be_nil + STDERR.external_encoding.should == nil end it "has nil for the external encoding despite Encoding.default_external being changed" do Encoding.default_external = Encoding::ISO_8859_1 - STDERR.external_encoding.should be_nil + STDERR.external_encoding.should == nil end it "has the encodings set by #set_encoding" do @@ -1236,12 +1508,12 @@ describe "The predefined global constant" do end it "has nil for the internal encoding" do - STDERR.internal_encoding.should be_nil + STDERR.internal_encoding.should == nil end it "has nil for the internal encoding despite Encoding.default_internal being changed" do Encoding.default_internal = Encoding::IBM437 - STDERR.internal_encoding.should be_nil + STDERR.internal_encoding.should == nil end end @@ -1254,3 +1526,50 @@ describe "The predefined global constant" do end end 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('pp') + extension.should == :rb + path.should.end_with?('/pp.rb') + end + + 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 + + it "return nil if feature cannot be found" do + $LOAD_PATH.resolve_feature_path('noop').should == nil + 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/private_spec.rb b/spec/ruby/language/private_spec.rb index ddf185e6d2..94b56fee5b 100644 --- a/spec/ruby/language/private_spec.rb +++ b/spec/ruby/language/private_spec.rb @@ -4,12 +4,12 @@ require_relative 'fixtures/private' describe "The private keyword" do it "marks following methods as being private" do a = Private::A.new - a.methods.should_not include(:bar) - -> { a.bar }.should raise_error(NoMethodError) + a.methods.should_not.include?(:bar) + -> { a.bar }.should.raise(NoMethodError) b = Private::B.new - b.methods.should_not include(:bar) - -> { b.bar }.should raise_error(NoMethodError) + b.methods.should_not.include?(:bar) + -> { b.bar }.should.raise(NoMethodError) end # def expr.meth() methods are always public @@ -19,22 +19,22 @@ describe "The private keyword" do it "is overridden when a new class is opened" do c = Private::B::C.new - c.methods.should include(:baz) + c.methods.should.include?(:baz) c.baz Private::B.public_class_method1.should == 1 - -> { Private::B.private_class_method1 }.should raise_error(NoMethodError) + -> { Private::B.private_class_method1 }.should.raise(NoMethodError) end it "is no longer in effect when the class is closed" do b = Private::B.new - b.methods.should include(:foo) + b.methods.should.include?(:foo) b.foo end it "changes visibility of previously called method" do klass = Class.new do def foo - "foo" + "foo" end end f = klass.new @@ -42,7 +42,7 @@ describe "The private keyword" do klass.class_eval do private :foo end - -> { f.foo }.should raise_error(NoMethodError) + -> { f.foo }.should.raise(NoMethodError) end it "changes visibility of previously called methods with same send/call site" do @@ -56,12 +56,12 @@ describe "The private keyword" do end end end - }.should raise_error(NoMethodError) + }.should.raise(NoMethodError) end it "changes the visibility of the existing method in the subclass" do ::Private::A.new.foo.should == 'foo' - -> { ::Private::H.new.foo }.should raise_error(NoMethodError) + -> { ::Private::H.new.foo }.should.raise(NoMethodError) ::Private::H.new.send(:foo).should == 'foo' end end diff --git a/spec/ruby/language/proc_spec.rb b/spec/ruby/language/proc_spec.rb index c44e711d2b..53a21d6b85 100644 --- a/spec/ruby/language/proc_spec.rb +++ b/spec/ruby/language/proc_spec.rb @@ -22,7 +22,7 @@ describe "A Proc" do end it "raises an ArgumentError if a value is passed" do - lambda { @l.call(0) }.should raise_error(ArgumentError) + lambda { @l.call(0) }.should.raise(ArgumentError) end end @@ -36,7 +36,7 @@ describe "A Proc" do end it "raises an ArgumentError if a value is passed" do - lambda { @l.call(0) }.should raise_error(ArgumentError) + lambda { @l.call(0) }.should.raise(ArgumentError) end end @@ -57,11 +57,11 @@ describe "A Proc" do obj = mock("block yield to_ary") obj.should_not_receive(:to_ary) - @l.call(obj).should equal(obj) + @l.call(obj).should.equal?(obj) end it "raises an ArgumentError if no value is passed" do - lambda { @l.call }.should raise_error(ArgumentError) + lambda { @l.call }.should.raise(ArgumentError) end end @@ -71,11 +71,11 @@ describe "A Proc" do end it "raises an ArgumentError if passed no values" do - lambda { @l.call }.should raise_error(ArgumentError) + lambda { @l.call }.should.raise(ArgumentError) end it "raises an ArgumentError if passed one value" do - lambda { @l.call(0) }.should raise_error(ArgumentError) + lambda { @l.call(0) }.should.raise(ArgumentError) end it "assigns the values passed to the arguments" do @@ -86,7 +86,7 @@ describe "A Proc" do obj = mock("proc call to_ary") obj.should_not_receive(:to_ary) - lambda { @l.call(obj) }.should raise_error(ArgumentError) + lambda { @l.call(obj) }.should.raise(ArgumentError) end end @@ -96,7 +96,7 @@ describe "A Proc" do end it "raises an ArgumentError if passed no values" do - lambda { @l.call }.should raise_error(ArgumentError) + lambda { @l.call }.should.raise(ArgumentError) end it "does not destructure a single Array value yielded" do @@ -104,7 +104,7 @@ describe "A Proc" do end it "assigns all passed values after the first to the rest argument" do - @l.call(1, 2, 3).should == [1, [2, 3]] + @l.call(1, 2, 3).should == [1, [2, 3]] end it "does not call #to_ary to convert a single passed object to an Array" do @@ -161,17 +161,29 @@ 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 } end it "raises an ArgumentError when passed no values" do - lambda { @l.call }.should raise_error(ArgumentError) + lambda { @l.call }.should.raise(ArgumentError) end it "raises an ArgumentError when passed more than one value" do - lambda { @l.call(1, 2) }.should raise_error(ArgumentError) + lambda { @l.call(1, 2) }.should.raise(ArgumentError) end it "assigns the argument the value passed" do @@ -196,7 +208,7 @@ describe "A Proc" do end it "raises an ArgumentError when passed no values" do - lambda { @l.call }.should raise_error(ArgumentError) + lambda { @l.call }.should.raise(ArgumentError) end it "destructures a single Array value yielded" do @@ -214,7 +226,42 @@ describe "A Proc" do obj = mock("block yield to_ary invalid") obj.should_receive(:to_ary).and_return(1) - lambda { @l.call(obj) }.should raise_error(TypeError) + lambda { @l.call(obj) }.should.raise(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(ArgumentError) + end + end + + evaluate <<-ruby do + @p = proc { |**nil| :ok } + ruby + + @p.call().should == :ok + -> { @p.call(a: 1) }.should.raise(ArgumentError, 'no keywords accepted') + -> { @p.call(**{a: 1}) }.should.raise(ArgumentError, 'no keywords accepted') + -> { @p.call("a" => 1) }.should.raise(ArgumentError, 'no keywords accepted') + end + + evaluate <<-ruby do + @p = proc { |a, **nil| a } + ruby + + @p.call({a: 1}).should == {a: 1} + -> { @p.call(a: 1) }.should.raise(ArgumentError, 'no keywords accepted') 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/redo_spec.rb b/spec/ruby/language/redo_spec.rb index 57532553b3..9b14c5add1 100644 --- a/spec/ruby/language/redo_spec.rb +++ b/spec/ruby/language/redo_spec.rb @@ -60,7 +60,7 @@ describe "The redo statement" do it "is invalid and raises a SyntaxError" do -> { eval("def m; redo; end") - }.should raise_error(SyntaxError) + }.should.raise(SyntaxError) end end end diff --git a/spec/ruby/language/regexp/anchors_spec.rb b/spec/ruby/language/regexp/anchors_spec.rb index 0129e255da..8e597b65e8 100644 --- a/spec/ruby/language/regexp/anchors_spec.rb +++ b/spec/ruby/language/regexp/anchors_spec.rb @@ -7,8 +7,8 @@ describe "Regexps with anchors" do /^foo/.match("foo").to_a.should == ["foo"] /^bar/.match("foo\nbar").to_a.should == ["bar"] # Basic non-matching - /^foo/.match(" foo").should be_nil - /foo^/.match("foo\n\n\n").should be_nil + /^foo/.match(" foo").should == nil + /foo^/.match("foo\n\n\n").should == nil # A bit advanced /^^^foo/.match("foo").to_a.should == ["foo"] @@ -16,8 +16,8 @@ describe "Regexps with anchors" do (/($^)($^)/ =~ "foo\n\n").should == "foo\n".size and $~.to_a.should == ["", "", ""] # Different start of line chars - /^bar/.match("foo\rbar").should be_nil - /^bar/.match("foo\0bar").should be_nil + /^bar/.match("foo\rbar").should == nil + /^bar/.match("foo\0bar").should == nil # Trivial /^/.match("foo").to_a.should == [""] @@ -29,7 +29,7 @@ describe "Regexps with anchors" do end it "does not match ^ after trailing \\n" do - /^(?!\A)/.match("foo\n").should be_nil # There is no (empty) line after a trailing \n + /^(?!\A)/.match("foo\n").should == nil # There is no (empty) line after a trailing \n end it "supports $ (line end anchor)" do @@ -37,16 +37,16 @@ describe "Regexps with anchors" do /foo$/.match("foo").to_a.should == ["foo"] /foo$/.match("foo\nbar").to_a.should == ["foo"] # Basic non-matching - /foo$/.match("foo ").should be_nil - /$foo/.match("\n\n\nfoo").should be_nil + /foo$/.match("foo ").should == nil + /$foo/.match("\n\n\nfoo").should == nil # A bit advanced /foo$$$/.match("foo").to_a.should == ["foo"] (/[^o]$/ =~ "foo\n\n").should == ("foo\n".size - 1) and $~.to_a.should == ["\n"] # Different end of line chars - /foo$/.match("foo\r\nbar").should be_nil - /foo$/.match("foo\0bar").should be_nil + /foo$/.match("foo\r\nbar").should == nil + /foo$/.match("foo\0bar").should == nil # Trivial (/$/ =~ "foo").should == "foo".size and $~.to_a.should == [""] @@ -61,15 +61,15 @@ describe "Regexps with anchors" do # Basic matching /\Afoo/.match("foo").to_a.should == ["foo"] # Basic non-matching - /\Abar/.match("foo\nbar").should be_nil - /\Afoo/.match(" foo").should be_nil + /\Abar/.match("foo\nbar").should == nil + /\Afoo/.match(" foo").should == nil # A bit advanced /\A\A\Afoo/.match("foo").to_a.should == ["foo"] /(\A\Z)(\A\Z)/.match("").to_a.should == ["", "", ""] # Different start of line chars - /\Abar/.match("foo\0bar").should be_nil + /\Abar/.match("foo\0bar").should == nil # Grouping /(\Afoo)/.match("foo").to_a.should == ["foo", "foo"] @@ -81,8 +81,8 @@ describe "Regexps with anchors" do /foo\Z/.match("foo").to_a.should == ["foo"] /foo\Z/.match("foo\n").to_a.should == ["foo"] # Basic non-matching - /foo\Z/.match("foo\nbar").should be_nil - /foo\Z/.match("foo ").should be_nil + /foo\Z/.match("foo\nbar").should == nil + /foo\Z/.match("foo ").should == nil # A bit advanced /foo\Z\Z\Z/.match("foo\n").to_a.should == ["foo"] @@ -90,8 +90,8 @@ describe "Regexps with anchors" do (/(\z\Z)(\z\Z)/ =~ "foo\n").should == "foo\n".size and $~.to_a.should == ["", "", ""] # Different end of line chars - /foo\Z/.match("foo\0bar").should be_nil - /foo\Z/.match("foo\r\n").should be_nil + /foo\Z/.match("foo\0bar").should == nil + /foo\Z/.match("foo\r\n").should == nil # Grouping /(foo\Z)/.match("foo").to_a.should == ["foo", "foo"] @@ -102,17 +102,17 @@ describe "Regexps with anchors" do # Basic matching /foo\z/.match("foo").to_a.should == ["foo"] # Basic non-matching - /foo\z/.match("foo\nbar").should be_nil - /foo\z/.match("foo\n").should be_nil - /foo\z/.match("foo ").should be_nil + /foo\z/.match("foo\nbar").should == nil + /foo\z/.match("foo\n").should == nil + /foo\z/.match("foo ").should == nil # A bit advanced /foo\z\z\z/.match("foo").to_a.should == ["foo"] (/($\z)($\z)/ =~ "foo").should == "foo".size and $~.to_a.should == ["", "", ""] # Different end of line chars - /foo\z/.match("foo\0bar").should be_nil - /foo\z/.match("foo\r\nbar").should be_nil + /foo\z/.match("foo\0bar").should == nil + /foo\z/.match("foo\r\nbar").should == nil # Grouping /(foo\z)/.match("foo").to_a.should == ["foo", "foo"] @@ -124,16 +124,16 @@ describe "Regexps with anchors" do /foo\b/.match("foo").to_a.should == ["foo"] /foo\b/.match("foo\n").to_a.should == ["foo"] LanguageSpecs.white_spaces.scan(/./).each do |c| - /foo\b/.match("foo" + c).to_a.should == ["foo"] + /foo\b/.match("foo" + c).to_a.should == ["foo"] end LanguageSpecs.non_alphanum_non_space.scan(/./).each do |c| - /foo\b/.match("foo" + c).to_a.should == ["foo"] + /foo\b/.match("foo" + c).to_a.should == ["foo"] end /foo\b/.match("foo\0").to_a.should == ["foo"] # Basic non-matching - /foo\b/.match("foobar").should be_nil - /foo\b/.match("foo123").should be_nil - /foo\b/.match("foo_").should be_nil + /foo\b/.match("foobar").should == nil + /foo\b/.match("foo123").should == nil + /foo\b/.match("foo_").should == nil end it "supports \\B (non-word-boundary)" do @@ -142,15 +142,15 @@ describe "Regexps with anchors" do /foo\B/.match("foo123").to_a.should == ["foo"] /foo\B/.match("foo_").to_a.should == ["foo"] # Basic non-matching - /foo\B/.match("foo").should be_nil - /foo\B/.match("foo\n").should be_nil + /foo\B/.match("foo").should == nil + /foo\B/.match("foo\n").should == nil LanguageSpecs.white_spaces.scan(/./).each do |c| - /foo\B/.match("foo" + c).should be_nil + /foo\B/.match("foo" + c).should == nil end LanguageSpecs.non_alphanum_non_space.scan(/./).each do |c| - /foo\B/.match("foo" + c).should be_nil + /foo\B/.match("foo" + c).should == nil end - /foo\B/.match("foo\0").should be_nil + /foo\B/.match("foo\0").should == nil end it "supports (?= ) (positive lookahead)" do diff --git a/spec/ruby/language/regexp/back-references_spec.rb b/spec/ruby/language/regexp/back-references_spec.rb index 81015ac21e..3b4c5656a2 100644 --- a/spec/ruby/language/regexp/back-references_spec.rb +++ b/spec/ruby/language/regexp/back-references_spec.rb @@ -22,6 +22,15 @@ describe "Regexps with back-references" do $10.should == "0" end + it "returns nil for numbered variable with too large index" do + -> { + eval(<<~CODE).should == nil + "a" =~ /(.)/ + eval('$4294967296') + CODE + }.should complain(/warning: ('|`)\$4294967296' is too big for a number variable, always nil/) + end + it "will not clobber capture variables across threads" do cap1, cap2, cap3 = nil "foo" =~ /(o+)/ @@ -40,14 +49,101 @@ describe "Regexps with back-references" do it "supports \<n> (backreference to previous group match)" do /(foo.)\1/.match("foo1foo1").to_a.should == ["foo1foo1", "foo1"] - /(foo.)\1/.match("foo1foo2").should be_nil + /(foo.)\1/.match("foo1foo2").should == nil end it "resets nested \<n> backreference before match of outer subexpression" do /(a\1?){2}/.match("aaaa").to_a.should == ["aa", "a"] end + it "does not reset enclosed capture groups" do + /((a)|(b))+/.match("ab").captures.should == [ "b", "a", "b" ] + end + it "can match an optional quote, followed by content, followed by a matching quote, as the whole string" do /^("|)(.*)\1$/.match('x').to_a.should == ["x", "", "x"] end + + it "allows forward references" do + /(?:(\2)|(.))+/.match("aa").to_a.should == [ "aa", "a", "a" ] + end + + it "disallows forward references >= 10" do + (/\10()()()()()()()()()()/ =~ "\x08").should == 0 + end + + it "fails when trying to match a backreference to an unmatched capture group" do + /\1()/.match("").should == nil + /(?:(a)|b)\1/.match("b").should == nil + end + + it "ignores backreferences > 1000" do + /\99999/.match("99999")[0].should == "99999" + end + + it "0 is not a valid backreference" do + -> { Regexp.new("\\k<0>") }.should.raise(RegexpError) + end + + it "allows numeric conditional backreferences" do + /(a)(?(1)a|b)/.match("aa").to_a.should == [ "aa", "a" ] + /(a)(?(<1>)a|b)/.match("aa").to_a.should == [ "aa", "a" ] + /(a)(?('1')a|b)/.match("aa").to_a.should == [ "aa", "a" ] + end + + it "allows either <> or '' in named conditional backreferences" do + -> { Regexp.new("(?<a>a)(?(a)a|b)") }.should.raise(RegexpError) + /(?<a>a)(?(<a>)a|b)/.match("aa").to_a.should == [ "aa", "a" ] + /(?<a>a)(?('a')a|b)/.match("aa").to_a.should == [ "aa", "a" ] + end + + it "allows negative numeric backreferences" do + /(a)\k<-1>/.match("aa").to_a.should == [ "aa", "a" ] + /(a)\g<-1>/.match("aa").to_a.should == [ "aa", "a" ] + /(a)(?(<-1>)a|b)/.match("aa").to_a.should == [ "aa", "a" ] + /(a)(?('-1')a|b)/.match("aa").to_a.should == [ "aa", "a" ] + end + + it "delimited numeric backreferences can start with 0" do + /(a)\k<01>/.match("aa").to_a.should == [ "aa", "a" ] + /(a)\g<01>/.match("aa").to_a.should == [ "aa", "a" ] + /(a)(?(01)a|b)/.match("aa").to_a.should == [ "aa", "a" ] + /(a)(?(<01>)a|b)/.match("aa").to_a.should == [ "aa", "a" ] + /(a)(?('01')a|b)/.match("aa").to_a.should == [ "aa", "a" ] + end + + it "regular numeric backreferences cannot start with 0" do + /(a)\01/.match("aa").should == nil + /(a)\01/.match("a\x01").to_a.should == [ "a\x01", "a" ] + end + + it "named capture groups invalidate numeric backreferences" do + -> { Regexp.new("(?<a>a)\\1") }.should.raise(RegexpError) + -> { Regexp.new("(?<a>a)\\k<1>") }.should.raise(RegexpError) + -> { Regexp.new("(a)(?<a>a)\\1") }.should.raise(RegexpError) + -> { Regexp.new("(a)(?<a>a)\\k<1>") }.should.raise(RegexpError) + end + + it "treats + or - as the beginning of a level specifier in \\k<> backreferences and (?(...)...|...) conditional backreferences" do + -> { Regexp.new("(?<a+>a)\\k<a+>") }.should.raise(RegexpError) + -> { Regexp.new("(?<a+b>a)\\k<a+b>") }.should.raise(RegexpError) + -> { Regexp.new("(?<a+1>a)\\k<a+1>") }.should.raise(RegexpError) + -> { Regexp.new("(?<a->a)\\k<a->") }.should.raise(RegexpError) + -> { Regexp.new("(?<a-b>a)\\k<a-b>") }.should.raise(RegexpError) + -> { Regexp.new("(?<a-1>a)\\k<a-1>") }.should.raise(RegexpError) + + -> { Regexp.new("(?<a+>a)(?(<a+>)a|b)") }.should.raise(RegexpError) + -> { Regexp.new("(?<a+b>a)(?(<a+b>)a|b)") }.should.raise(RegexpError) + -> { Regexp.new("(?<a+1>a)(?(<a+1>)a|b)") }.should.raise(RegexpError) + -> { Regexp.new("(?<a->a)(?(<a->)a|b)") }.should.raise(RegexpError) + -> { Regexp.new("(?<a-b>a)(?(<a-b>)a|b)") }.should.raise(RegexpError) + -> { Regexp.new("(?<a-1>a)(?(<a-1>)a|b)") }.should.raise(RegexpError) + + -> { Regexp.new("(?<a+>a)(?('a+')a|b)") }.should.raise(RegexpError) + -> { Regexp.new("(?<a+b>a)(?('a+b')a|b)") }.should.raise(RegexpError) + -> { Regexp.new("(?<a+1>a)(?('a+1')a|b)") }.should.raise(RegexpError) + -> { Regexp.new("(?<a->a)(?('a-')a|b)") }.should.raise(RegexpError) + -> { Regexp.new("(?<a-b>a)(?('a-b')a|b)") }.should.raise(RegexpError) + -> { Regexp.new("(?<a-1>a)(?('a-1')a|b)") }.should.raise(RegexpError) + end end diff --git a/spec/ruby/language/regexp/character_classes_spec.rb b/spec/ruby/language/regexp/character_classes_spec.rb index 5f4221e213..c6ed92b78e 100644 --- a/spec/ruby/language/regexp/character_classes_spec.rb +++ b/spec/ruby/language/regexp/character_classes_spec.rb @@ -9,9 +9,9 @@ describe "Regexp with character classes" do /\w/.match("_").to_a.should == ["_"] # Non-matches - /\w/.match(LanguageSpecs.white_spaces).should be_nil - /\w/.match(LanguageSpecs.non_alphanum_non_space).should be_nil - /\w/.match("\0").should be_nil + /\w/.match(LanguageSpecs.white_spaces).should == nil + /\w/.match(LanguageSpecs.non_alphanum_non_space).should == nil + /\w/.match("\0").should == nil end it "supports \\W (non-word character)" do @@ -20,19 +20,19 @@ describe "Regexp with character classes" do /\W/.match("\0").to_a.should == ["\0"] # Non-matches - /\W/.match("a").should be_nil - /\W/.match("1").should be_nil - /\W/.match("_").should be_nil + /\W/.match("a").should == nil + /\W/.match("1").should == nil + /\W/.match("_").should == nil end it "supports \\s (space character)" do /\s+/.match(LanguageSpecs.white_spaces).to_a.should == [LanguageSpecs.white_spaces] # Non-matches - /\s/.match("a").should be_nil - /\s/.match("1").should be_nil - /\s/.match(LanguageSpecs.non_alphanum_non_space).should be_nil - /\s/.match("\0").should be_nil + /\s/.match("a").should == nil + /\s/.match("1").should == nil + /\s/.match(LanguageSpecs.non_alphanum_non_space).should == nil + /\s/.match("\0").should == nil end it "supports \\S (non-space character)" do @@ -42,17 +42,17 @@ describe "Regexp with character classes" do /\S/.match("\0").to_a.should == ["\0"] # Non-matches - /\S/.match(LanguageSpecs.white_spaces).should be_nil + /\S/.match(LanguageSpecs.white_spaces).should == nil end it "supports \\d (numeric digit)" do /\d/.match("1").to_a.should == ["1"] # Non-matches - /\d/.match("a").should be_nil - /\d/.match(LanguageSpecs.white_spaces).should be_nil - /\d/.match(LanguageSpecs.non_alphanum_non_space).should be_nil - /\d/.match("\0").should be_nil + /\d/.match("a").should == nil + /\d/.match(LanguageSpecs.white_spaces).should == nil + /\d/.match(LanguageSpecs.non_alphanum_non_space).should == nil + /\d/.match("\0").should == nil end it "supports \\D (non-digit)" do @@ -62,7 +62,7 @@ describe "Regexp with character classes" do /\D/.match("\0").to_a.should == ["\0"] # Non-matches - /\D/.match("1").should be_nil + /\D/.match("1").should == nil end it "supports [] (character class)" do @@ -89,7 +89,7 @@ describe "Regexp with character classes" do /[^[:lower:]A-C]+/.match("abcABCDEF123def").to_a.should == ["DEF123"] # negated character class /[:alnum:]+/.match("a:l:n:u:m").to_a.should == ["a:l:n:u:m"] # should behave like regular character class composed of the individual letters /[\[:alnum:]+/.match("[:a:l:n:u:m").to_a.should == ["[:a:l:n:u:m"] # should behave like regular character class composed of the individual letters - -> { eval('/[[:alpha:]-[:digit:]]/') }.should raise_error(SyntaxError) # can't use character class as a start value of range + -> { eval('/[[:alpha:]-[:digit:]]/') }.should.raise(SyntaxError) # can't use character class as a start value of range end it "matches ASCII characters with [[:ascii:]]" do @@ -99,8 +99,8 @@ describe "Regexp with character classes" do not_supported_on :opal do it "doesn't match non-ASCII characters with [[:ascii:]]" do - /[[:ascii:]]/.match("\u{80}").should be_nil - /[[:ascii:]]/.match("\u{9898}").should be_nil + /[[:ascii:]]/.match("\u{80}").should == nil + /[[:ascii:]]/.match("\u{9898}").should == nil end end @@ -113,7 +113,7 @@ describe "Regexp with character classes" do end it "doesn't matches Unicode marks with [[:alnum:]]" do - "\u{36F}".match(/[[:alnum:]]/).should be_nil + "\u{3099}".match(/[[:alnum:]]/).should == nil end it "doesn't match Unicode control characters with [[:alnum:]]" do @@ -133,7 +133,7 @@ describe "Regexp with character classes" do end it "doesn't matches Unicode marks with [[:alpha:]]" do - "\u{36F}".match(/[[:alpha:]]/).should be_nil + "\u{3099}".match(/[[:alpha:]]/).should == nil end it "doesn't match Unicode control characters with [[:alpha:]]" do @@ -149,39 +149,39 @@ describe "Regexp with character classes" do end it "doesn't match Unicode control characters with [[:blank:]]" do - "\u{16}".match(/[[:blank:]]/).should be_nil + "\u{16}".match(/[[:blank:]]/).should == nil end it "doesn't match Unicode punctuation characters with [[:blank:]]" do - "\u{3F}".match(/[[:blank:]]/).should be_nil + "\u{3F}".match(/[[:blank:]]/).should == nil end it "doesn't match Unicode letter characters with [[:blank:]]" do - "à".match(/[[:blank:]]/).should be_nil + "à".match(/[[:blank:]]/).should == nil end it "doesn't match Unicode digits with [[:blank:]]" do - "\u{0660}".match(/[[:blank:]]/).should be_nil + "\u{0660}".match(/[[:blank:]]/).should == nil end it "doesn't match Unicode marks with [[:blank:]]" do - "\u{36F}".match(/[[:blank:]]/).should be_nil + "\u{36F}".match(/[[:blank:]]/).should == nil end it "doesn't Unicode letter characters with [[:cntrl:]]" do - "à".match(/[[:cntrl:]]/).should be_nil + "à".match(/[[:cntrl:]]/).should == nil end it "doesn't match Unicode digits with [[:cntrl:]]" do - "\u{0660}".match(/[[:cntrl:]]/).should be_nil + "\u{0660}".match(/[[:cntrl:]]/).should == nil end it "doesn't match Unicode marks with [[:cntrl:]]" do - "\u{36F}".match(/[[:cntrl:]]/).should be_nil + "\u{36F}".match(/[[:cntrl:]]/).should == nil end it "doesn't match Unicode punctuation characters with [[:cntrl:]]" do - "\u{3F}".match(/[[:cntrl:]]/).should be_nil + "\u{3F}".match(/[[:cntrl:]]/).should == nil end it "matches Unicode control characters with [[:cntrl:]]" do @@ -189,15 +189,15 @@ describe "Regexp with character classes" do end it "doesn't match Unicode format characters with [[:cntrl:]]" do - "\u{2060}".match(/[[:cntrl:]]/).should be_nil + "\u{2060}".match(/[[:cntrl:]]/).should == nil end it "doesn't match Unicode private-use characters with [[:cntrl:]]" do - "\u{E001}".match(/[[:cntrl:]]/).should be_nil + "\u{E001}".match(/[[:cntrl:]]/).should == nil end it "doesn't match Unicode letter characters with [[:digit:]]" do - "à".match(/[[:digit:]]/).should be_nil + "à".match(/[[:digit:]]/).should == nil end it "matches Unicode digits with [[:digit:]]" do @@ -206,27 +206,27 @@ describe "Regexp with character classes" do end it "doesn't match Unicode marks with [[:digit:]]" do - "\u{36F}".match(/[[:digit:]]/).should be_nil + "\u{36F}".match(/[[:digit:]]/).should == nil end it "doesn't match Unicode punctuation characters with [[:digit:]]" do - "\u{3F}".match(/[[:digit:]]/).should be_nil + "\u{3F}".match(/[[:digit:]]/).should == nil end it "doesn't match Unicode control characters with [[:digit:]]" do - "\u{16}".match(/[[:digit:]]/).should be_nil + "\u{16}".match(/[[:digit:]]/).should == nil end it "doesn't match Unicode format characters with [[:digit:]]" do - "\u{2060}".match(/[[:digit:]]/).should be_nil + "\u{2060}".match(/[[:digit:]]/).should == nil end it "doesn't match Unicode private-use characters with [[:digit:]]" do - "\u{E001}".match(/[[:digit:]]/).should be_nil + "\u{E001}".match(/[[:digit:]]/).should == nil end it "matches Unicode letter characters with [[:graph:]]" do - "à".match(/[[:graph:]]/).to_a.should == ["à"] + "à".match(/[[:graph:]]/).to_a.should == ["à"] end it "matches Unicode digits with [[:graph:]]" do @@ -243,7 +243,7 @@ describe "Regexp with character classes" do end it "doesn't match Unicode control characters with [[:graph:]]" do - "\u{16}".match(/[[:graph:]]/).should be_nil + "\u{16}".match(/[[:graph:]]/).should == nil end it "match Unicode format characters with [[:graph:]]" do @@ -261,40 +261,40 @@ describe "Regexp with character classes" do end it "doesn't match Unicode uppercase letter characters with [[:lower:]]" do - "\u{100}".match(/[[:lower:]]/).should be_nil - "\u{130}".match(/[[:lower:]]/).should be_nil - "\u{405}".match(/[[:lower:]]/).should be_nil + "\u{100}".match(/[[:lower:]]/).should == nil + "\u{130}".match(/[[:lower:]]/).should == nil + "\u{405}".match(/[[:lower:]]/).should == nil end it "doesn't match Unicode title-case characters with [[:lower:]]" do - "\u{1F88}".match(/[[:lower:]]/).should be_nil - "\u{1FAD}".match(/[[:lower:]]/).should be_nil - "\u{01C5}".match(/[[:lower:]]/).should be_nil + "\u{1F88}".match(/[[:lower:]]/).should == nil + "\u{1FAD}".match(/[[:lower:]]/).should == nil + "\u{01C5}".match(/[[:lower:]]/).should == nil end it "doesn't match Unicode digits with [[:lower:]]" do - "\u{0660}".match(/[[:lower:]]/).should be_nil - "\u{FF12}".match(/[[:lower:]]/).should be_nil + "\u{0660}".match(/[[:lower:]]/).should == nil + "\u{FF12}".match(/[[:lower:]]/).should == nil end it "doesn't match Unicode marks with [[:lower:]]" do - "\u{36F}".match(/[[:lower:]]/).should be_nil + "\u{36F}".match(/[[:lower:]]/).should == nil end it "doesn't match Unicode punctuation characters with [[:lower:]]" do - "\u{3F}".match(/[[:lower:]]/).should be_nil + "\u{3F}".match(/[[:lower:]]/).should == nil end it "doesn't match Unicode control characters with [[:lower:]]" do - "\u{16}".match(/[[:lower:]]/).should be_nil + "\u{16}".match(/[[:lower:]]/).should == nil end it "doesn't match Unicode format characters with [[:lower:]]" do - "\u{2060}".match(/[[:lower:]]/).should be_nil + "\u{2060}".match(/[[:lower:]]/).should == nil end it "doesn't match Unicode private-use characters with [[:lower:]]" do - "\u{E001}".match(/[[:lower:]]/).should be_nil + "\u{E001}".match(/[[:lower:]]/).should == nil end it "matches Unicode lowercase letter characters with [[:print:]]" do @@ -329,7 +329,7 @@ describe "Regexp with character classes" do end it "doesn't match Unicode control characters with [[:print:]]" do - "\u{16}".match(/[[:print:]]/).should be_nil + "\u{16}".match(/[[:print:]]/).should == nil end it "match Unicode format characters with [[:print:]]" do @@ -342,30 +342,30 @@ describe "Regexp with character classes" do it "doesn't match Unicode lowercase letter characters with [[:punct:]]" do - "\u{FF41}".match(/[[:punct:]]/).should be_nil - "\u{1D484}".match(/[[:punct:]]/).should be_nil - "\u{E8}".match(/[[:punct:]]/).should be_nil + "\u{FF41}".match(/[[:punct:]]/).should == nil + "\u{1D484}".match(/[[:punct:]]/).should == nil + "\u{E8}".match(/[[:punct:]]/).should == nil end it "doesn't match Unicode uppercase letter characters with [[:punct:]]" do - "\u{100}".match(/[[:punct:]]/).should be_nil - "\u{130}".match(/[[:punct:]]/).should be_nil - "\u{405}".match(/[[:punct:]]/).should be_nil + "\u{100}".match(/[[:punct:]]/).should == nil + "\u{130}".match(/[[:punct:]]/).should == nil + "\u{405}".match(/[[:punct:]]/).should == nil end it "doesn't match Unicode title-case characters with [[:punct:]]" do - "\u{1F88}".match(/[[:punct:]]/).should be_nil - "\u{1FAD}".match(/[[:punct:]]/).should be_nil - "\u{01C5}".match(/[[:punct:]]/).should be_nil + "\u{1F88}".match(/[[:punct:]]/).should == nil + "\u{1FAD}".match(/[[:punct:]]/).should == nil + "\u{01C5}".match(/[[:punct:]]/).should == nil end it "doesn't match Unicode digits with [[:punct:]]" do - "\u{0660}".match(/[[:punct:]]/).should be_nil - "\u{FF12}".match(/[[:punct:]]/).should be_nil + "\u{0660}".match(/[[:punct:]]/).should == nil + "\u{FF12}".match(/[[:punct:]]/).should == nil end it "doesn't match Unicode marks with [[:punct:]]" do - "\u{36F}".match(/[[:punct:]]/).should be_nil + "\u{36F}".match(/[[:punct:]]/).should == nil end it "matches Unicode Pc characters with [[:punct:]]" do @@ -398,38 +398,38 @@ describe "Regexp with character classes" do end it "doesn't match Unicode format characters with [[:punct:]]" do - "\u{2060}".match(/[[:punct:]]/).should be_nil + "\u{2060}".match(/[[:punct:]]/).should == nil end it "doesn't match Unicode private-use characters with [[:punct:]]" do - "\u{E001}".match(/[[:punct:]]/).should be_nil + "\u{E001}".match(/[[:punct:]]/).should == nil end it "doesn't match Unicode lowercase letter characters with [[:space:]]" do - "\u{FF41}".match(/[[:space:]]/).should be_nil - "\u{1D484}".match(/[[:space:]]/).should be_nil - "\u{E8}".match(/[[:space:]]/).should be_nil + "\u{FF41}".match(/[[:space:]]/).should == nil + "\u{1D484}".match(/[[:space:]]/).should == nil + "\u{E8}".match(/[[:space:]]/).should == nil end it "doesn't match Unicode uppercase letter characters with [[:space:]]" do - "\u{100}".match(/[[:space:]]/).should be_nil - "\u{130}".match(/[[:space:]]/).should be_nil - "\u{405}".match(/[[:space:]]/).should be_nil + "\u{100}".match(/[[:space:]]/).should == nil + "\u{130}".match(/[[:space:]]/).should == nil + "\u{405}".match(/[[:space:]]/).should == nil end it "doesn't match Unicode title-case characters with [[:space:]]" do - "\u{1F88}".match(/[[:space:]]/).should be_nil - "\u{1FAD}".match(/[[:space:]]/).should be_nil - "\u{01C5}".match(/[[:space:]]/).should be_nil + "\u{1F88}".match(/[[:space:]]/).should == nil + "\u{1FAD}".match(/[[:space:]]/).should == nil + "\u{01C5}".match(/[[:space:]]/).should == nil end it "doesn't match Unicode digits with [[:space:]]" do - "\u{0660}".match(/[[:space:]]/).should be_nil - "\u{FF12}".match(/[[:space:]]/).should be_nil + "\u{0660}".match(/[[:space:]]/).should == nil + "\u{FF12}".match(/[[:space:]]/).should == nil end it "doesn't match Unicode marks with [[:space:]]" do - "\u{36F}".match(/[[:space:]]/).should be_nil + "\u{36F}".match(/[[:space:]]/).should == nil end it "matches Unicode Zs characters with [[:space:]]" do @@ -445,17 +445,17 @@ describe "Regexp with character classes" do end it "doesn't match Unicode format characters with [[:space:]]" do - "\u{2060}".match(/[[:space:]]/).should be_nil + "\u{2060}".match(/[[:space:]]/).should == nil end it "doesn't match Unicode private-use characters with [[:space:]]" do - "\u{E001}".match(/[[:space:]]/).should be_nil + "\u{E001}".match(/[[:space:]]/).should == nil end it "doesn't match Unicode lowercase characters with [[:upper:]]" do - "\u{FF41}".match(/[[:upper:]]/).should be_nil - "\u{1D484}".match(/[[:upper:]]/).should be_nil - "\u{E8}".match(/[[:upper:]]/).should be_nil + "\u{FF41}".match(/[[:upper:]]/).should == nil + "\u{1D484}".match(/[[:upper:]]/).should == nil + "\u{E8}".match(/[[:upper:]]/).should == nil end it "matches Unicode uppercase characters with [[:upper:]]" do @@ -465,40 +465,40 @@ describe "Regexp with character classes" do end it "doesn't match Unicode title-case characters with [[:upper:]]" do - "\u{1F88}".match(/[[:upper:]]/).should be_nil - "\u{1FAD}".match(/[[:upper:]]/).should be_nil - "\u{01C5}".match(/[[:upper:]]/).should be_nil + "\u{1F88}".match(/[[:upper:]]/).should == nil + "\u{1FAD}".match(/[[:upper:]]/).should == nil + "\u{01C5}".match(/[[:upper:]]/).should == nil end it "doesn't match Unicode digits with [[:upper:]]" do - "\u{0660}".match(/[[:upper:]]/).should be_nil - "\u{FF12}".match(/[[:upper:]]/).should be_nil + "\u{0660}".match(/[[:upper:]]/).should == nil + "\u{FF12}".match(/[[:upper:]]/).should == nil end it "doesn't match Unicode marks with [[:upper:]]" do - "\u{36F}".match(/[[:upper:]]/).should be_nil + "\u{36F}".match(/[[:upper:]]/).should == nil end it "doesn't match Unicode punctuation characters with [[:upper:]]" do - "\u{3F}".match(/[[:upper:]]/).should be_nil + "\u{3F}".match(/[[:upper:]]/).should == nil end it "doesn't match Unicode control characters with [[:upper:]]" do - "\u{16}".match(/[[:upper:]]/).should be_nil + "\u{16}".match(/[[:upper:]]/).should == nil end it "doesn't match Unicode format characters with [[:upper:]]" do - "\u{2060}".match(/[[:upper:]]/).should be_nil + "\u{2060}".match(/[[:upper:]]/).should == nil end it "doesn't match Unicode private-use characters with [[:upper:]]" do - "\u{E001}".match(/[[:upper:]]/).should be_nil + "\u{E001}".match(/[[:upper:]]/).should == nil end it "doesn't match Unicode letter characters [^a-fA-F] with [[:xdigit:]]" do - "à".match(/[[:xdigit:]]/).should be_nil - "g".match(/[[:xdigit:]]/).should be_nil - "X".match(/[[:xdigit:]]/).should be_nil + "à".match(/[[:xdigit:]]/).should == nil + "g".match(/[[:xdigit:]]/).should == nil + "X".match(/[[:xdigit:]]/).should == nil end it "matches Unicode letter characters [a-fA-F] with [[:xdigit:]]" do @@ -507,28 +507,28 @@ describe "Regexp with character classes" do end it "doesn't match Unicode digits [^0-9] with [[:xdigit:]]" do - "\u{0660}".match(/[[:xdigit:]]/).should be_nil - "\u{FF12}".match(/[[:xdigit:]]/).should be_nil + "\u{0660}".match(/[[:xdigit:]]/).should == nil + "\u{FF12}".match(/[[:xdigit:]]/).should == nil end it "doesn't match Unicode marks with [[:xdigit:]]" do - "\u{36F}".match(/[[:xdigit:]]/).should be_nil + "\u{36F}".match(/[[:xdigit:]]/).should == nil end it "doesn't match Unicode punctuation characters with [[:xdigit:]]" do - "\u{3F}".match(/[[:xdigit:]]/).should be_nil + "\u{3F}".match(/[[:xdigit:]]/).should == nil end it "doesn't match Unicode control characters with [[:xdigit:]]" do - "\u{16}".match(/[[:xdigit:]]/).should be_nil + "\u{16}".match(/[[:xdigit:]]/).should == nil end it "doesn't match Unicode format characters with [[:xdigit:]]" do - "\u{2060}".match(/[[:xdigit:]]/).should be_nil + "\u{2060}".match(/[[:xdigit:]]/).should == nil end it "doesn't match Unicode private-use characters with [[:xdigit:]]" do - "\u{E001}".match(/[[:xdigit:]]/).should be_nil + "\u{E001}".match(/[[:xdigit:]]/).should == nil end it "matches Unicode lowercase characters with [[:word:]]" do @@ -562,23 +562,30 @@ describe "Regexp with character classes" do "\u{16EE}".match(/[[:word:]]/).to_a.should == ["\u{16EE}"] end + ruby_bug "#19417", ""..."3.4.6" do + it "matches Unicode join control characters with [[:word:]]" do + "\u{200C}".match(/[[:word:]]/).to_a.should == ["\u{200C}"] + "\u{200D}".match(/[[:word:]]/).to_a.should == ["\u{200D}"] + end + end + it "doesn't match Unicode No characters with [[:word:]]" do - "\u{17F0}".match(/[[:word:]]/).should be_nil + "\u{17F0}".match(/[[:word:]]/).should == nil end it "doesn't match Unicode punctuation characters with [[:word:]]" do - "\u{3F}".match(/[[:word:]]/).should be_nil + "\u{3F}".match(/[[:word:]]/).should == nil end it "doesn't match Unicode control characters with [[:word:]]" do - "\u{16}".match(/[[:word:]]/).should be_nil + "\u{16}".match(/[[:word:]]/).should == nil end it "doesn't match Unicode format characters with [[:word:]]" do - "\u{2060}".match(/[[:word:]]/).should be_nil + "\u{2060}".match(/[[:word:]]/).should == nil end it "doesn't match Unicode private-use characters with [[:word:]]" do - "\u{E001}".match(/[[:word:]]/).should be_nil + "\u{E001}".match(/[[:word:]]/).should == nil end it "matches unicode named character properties" do @@ -609,6 +616,15 @@ describe "Regexp with character classes" do "루비(Ruby)".match(/\p{Hangul}+/u).to_a.should == ["루비"] end + it "supports negated property condition" do + "a".match(eval("/\P{L}/")).should == nil + "1".match(eval("/\P{N}/")).should == nil + end + + it "raises a RegexpError for an unterminated unicode property" do + -> { Regexp.new('\p{') }.should.raise(RegexpError) + end + it "supports \\X (unicode 9.0 with UTR #51 workarounds)" do # simple emoji without any fancy modifier or ZWJ /\X/.match("\u{1F98A}").to_a.should == ["🦊"] diff --git a/spec/ruby/language/regexp/empty_checks_spec.rb b/spec/ruby/language/regexp/empty_checks_spec.rb new file mode 100644 index 0000000000..391e65b003 --- /dev/null +++ b/spec/ruby/language/regexp/empty_checks_spec.rb @@ -0,0 +1,135 @@ +require_relative '../../spec_helper' +require_relative '../fixtures/classes' + +describe "empty checks in Regexps" do + + it "allow extra empty iterations" do + /()?/.match("").to_a.should == ["", ""] + /(a*)?/.match("").to_a.should == ["", ""] + /(a*)*/.match("").to_a.should == ["", ""] + # The bounds are high to avoid DFA-based matchers in implementations + # and to check backtracking behavior. + /(?:a|()){500,1000}/.match("a" * 500).to_a.should == ["a" * 500, ""] + + # Variations with non-greedy loops. + /()??/.match("").to_a.should == ["", nil] + /(a*?)?/.match("").to_a.should == ["", ""] + /(a*)??/.match("").to_a.should == ["", nil] + /(a*?)??/.match("").to_a.should == ["", nil] + /(a*?)*/.match("").to_a.should == ["", ""] + /(a*)*?/.match("").to_a.should == ["", nil] + /(a*?)*?/.match("").to_a.should == ["", nil] + end + + it "allow empty iterations in the middle of a loop" do + # One empty iteration between a's and b's. + /(a|\2b|())*/.match("aaabbb").to_a.should == ["aaabbb", "", ""] + /(a|\2b|()){2,4}/.match("aaabbb").to_a.should == ["aaa", "", ""] + + # Two empty iterations between a's and b's. + /(a|\2b|\3()|())*/.match("aaabbb").to_a.should == ["aaabbb", "", "", ""] + /(a|\2b|\3()|()){2,4}/.match("aaabbb").to_a.should == ["aaa", "", nil, ""] + + # Check that the empty iteration correctly updates the loop counter. + /(a|\2b|()){20,24}/.match("a" * 20 + "b" * 5).to_a.should == ["a" * 20 + "b" * 3, "b", ""] + + # Variations with non-greedy loops. + /(a|\2b|())*?/.match("aaabbb").to_a.should == ["", nil, nil] + /(a|\2b|()){2,4}/.match("aaabbb").to_a.should == ["aaa", "", ""] + /(a|\2b|\3()|())*?/.match("aaabbb").to_a.should == ["", nil, nil, nil] + /(a|\2b|\3()|()){2,4}/.match("aaabbb").to_a.should == ["aaa", "", nil, ""] + /(a|\2b|()){20,24}/.match("a" * 20 + "b" * 5).to_a.should == ["a" * 20 + "b" * 3, "b", ""] + end + + it "make the Regexp proceed past the quantified expression on failure" do + # If the contents of the ()* quantified group are empty (i.e., they fail + # the empty check), the loop will abort. It will not try to backtrack + # and try other alternatives (e.g. matching the "a") like in other Regexp + # dialects such as ECMAScript. + /(?:|a)*/.match("aaa").to_a.should == [""] + /(?:()|a)*/.match("aaa").to_a.should == ["", ""] + /(|a)*/.match("aaa").to_a.should == ["", ""] + /(()|a)*/.match("aaa").to_a.should == ["", "", ""] + + # Same expressions, but with backreferences, to force the use of non-DFA-based + # engines. + /()\1(?:|a)*/.match("aaa").to_a.should == ["", ""] + /()\1(?:()|a)*/.match("aaa").to_a.should == ["", "", ""] + /()\1(|a)*/.match("aaa").to_a.should == ["", "", ""] + /()\1(()|a)*/.match("aaa").to_a.should == ["", "", "", ""] + + # Variations with other zero-width contents of the quantified + # group: backreferences, capture groups, lookarounds + /()(?:\1|a)*/.match("aaa").to_a.should == ["", ""] + /()(?:()\1|a)*/.match("aaa").to_a.should == ["", "", ""] + /()(?:(\1)|a)*/.match("aaa").to_a.should == ["", "", ""] + /()(?:\1()|a)*/.match("aaa").to_a.should == ["", "", ""] + /()(\1|a)*/.match("aaa").to_a.should == ["", "", ""] + /()(()\1|a)*/.match("aaa").to_a.should == ["", "", "", ""] + /()((\1)|a)*/.match("aaa").to_a.should == ["", "", "", ""] + /()(\1()|a)*/.match("aaa").to_a.should == ["", "", "", ""] + + /(?:(?=a)|a)*/.match("aaa").to_a.should == [""] + /(?:(?=a)()|a)*/.match("aaa").to_a.should == ["", ""] + /(?:()(?=a)|a)*/.match("aaa").to_a.should == ["", ""] + /(?:((?=a))|a)*/.match("aaa").to_a.should == ["", ""] + /()\1(?:(?=a)|a)*/.match("aaa").to_a.should == ["", ""] + /()\1(?:(?=a)()|a)*/.match("aaa").to_a.should == ["", "", ""] + /()\1(?:()(?=a)|a)*/.match("aaa").to_a.should == ["", "", ""] + /()\1(?:((?=a))|a)*/.match("aaa").to_a.should == ["", "", ""] + + # Variations with non-greedy loops. + /(?:|a)*?/.match("aaa").to_a.should == [""] + /(?:()|a)*?/.match("aaa").to_a.should == ["", nil] + /(|a)*?/.match("aaa").to_a.should == ["", nil] + /(()|a)*?/.match("aaa").to_a.should == ["", nil, nil] + + /()\1(?:|a)*?/.match("aaa").to_a.should == ["", ""] + /()\1(?:()|a)*?/.match("aaa").to_a.should == ["", "", nil] + /()\1(|a)*?/.match("aaa").to_a.should == ["", "", nil] + /()\1(()|a)*?/.match("aaa").to_a.should == ["", "", nil, nil] + + /()(?:\1|a)*?/.match("aaa").to_a.should == ["", ""] + /()(?:()\1|a)*?/.match("aaa").to_a.should == ["", "", nil] + /()(?:(\1)|a)*?/.match("aaa").to_a.should == ["", "", nil] + /()(?:\1()|a)*?/.match("aaa").to_a.should == ["", "", nil] + /()(\1|a)*?/.match("aaa").to_a.should == ["", "", nil] + /()(()\1|a)*?/.match("aaa").to_a.should == ["", "", nil, nil] + /()((\1)|a)*?/.match("aaa").to_a.should == ["", "", nil, nil] + /()(\1()|a)*?/.match("aaa").to_a.should == ["", "", nil, nil] + + /(?:(?=a)|a)*?/.match("aaa").to_a.should == [""] + /(?:(?=a)()|a)*?/.match("aaa").to_a.should == ["", nil] + /(?:()(?=a)|a)*?/.match("aaa").to_a.should == ["", nil] + /(?:((?=a))|a)*?/.match("aaa").to_a.should == ["", nil] + /()\1(?:(?=a)|a)*?/.match("aaa").to_a.should == ["", ""] + /()\1(?:(?=a)()|a)*?/.match("aaa").to_a.should == ["", "", nil] + /()\1(?:()(?=a)|a)*?/.match("aaa").to_a.should == ["", "", nil] + /()\1(?:((?=a))|a)*?/.match("aaa").to_a.should == ["", "", nil] + end + + it "shouldn't cause the Regexp parser to get stuck in a loop" do + /(|a|\2b|())*/.match("aaabbb").to_a.should == ["", "", nil] + /(a||\2b|())*/.match("aaabbb").to_a.should == ["aaa", "", nil] + /(a|\2b||())*/.match("aaabbb").to_a.should == ["aaa", "", nil] + /(a|\2b|()|)*/.match("aaabbb").to_a.should == ["aaabbb", "", ""] + /(()|a|\3b|())*/.match("aaabbb").to_a.should == ["", "", "", nil] + /(a|()|\3b|())*/.match("aaabbb").to_a.should == ["aaa", "", "", nil] + /(a|\2b|()|())*/.match("aaabbb").to_a.should == ["aaabbb", "", "", nil] + /(a|\3b|()|())*/.match("aaabbb").to_a.should == ["aaa", "", "", nil] + /(a|()|())*/.match("aaa").to_a.should == ["aaa", "", "", nil] + /^(()|a|())*$/.match("aaa").to_a.should == ["aaa", "", "", nil] + + # Variations with non-greedy loops. + /(|a|\2b|())*?/.match("aaabbb").to_a.should == ["", nil, nil] + /(a||\2b|())*?/.match("aaabbb").to_a.should == ["", nil, nil] + /(a|\2b||())*?/.match("aaabbb").to_a.should == ["", nil, nil] + /(a|\2b|()|)*?/.match("aaabbb").to_a.should == ["", nil, nil] + /(()|a|\3b|())*?/.match("aaabbb").to_a.should == ["", nil, nil, nil] + /(a|()|\3b|())*?/.match("aaabbb").to_a.should == ["", nil, nil, nil] + /(a|\2b|()|())*?/.match("aaabbb").to_a.should == ["", nil, nil, nil] + /(a|\3b|()|())*?/.match("aaabbb").to_a.should == ["", nil, nil, nil] + /(a|()|())*?/.match("aaa").to_a.should == ["", nil, nil, nil] + /^(()|a|())*?$/.match("aaa").to_a.should == ["aaa", "a", "", nil] + end +end diff --git a/spec/ruby/language/regexp/encoding_spec.rb b/spec/ruby/language/regexp/encoding_spec.rb index 8e2a294b95..81e845af0c 100644 --- a/spec/ruby/language/regexp/encoding_spec.rb +++ b/spec/ruby/language/regexp/encoding_spec.rb @@ -1,21 +1,21 @@ -# -*- encoding: binary -*- +# encoding: binary require_relative '../../spec_helper' 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,14 @@ 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 + -> { + eval <<~RUBY + /./n.match("\303\251".dup.force_encoding('utf-8')) + RUBY + }.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 +67,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 +90,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 @@ -106,25 +114,38 @@ describe "Regexps with encoding modifiers" do end it "raises Encoding::CompatibilityError when trying match against different encodings" do - -> { /\A[[:space:]]*\z/.match(" ".encode("UTF-16LE")) }.should raise_error(Encoding::CompatibilityError) + -> { /\A[[:space:]]*\z/.match(" ".encode("UTF-16LE")) }.should.raise(Encoding::CompatibilityError) end it "raises Encoding::CompatibilityError when trying match? against different encodings" do - -> { /\A[[:space:]]*\z/.match?(" ".encode("UTF-16LE")) }.should raise_error(Encoding::CompatibilityError) + -> { /\A[[:space:]]*\z/.match?(" ".encode("UTF-16LE")) }.should.raise(Encoding::CompatibilityError) end it "raises Encoding::CompatibilityError when trying =~ against different encodings" do - -> { /\A[[:space:]]*\z/ =~ " ".encode("UTF-16LE") }.should raise_error(Encoding::CompatibilityError) + -> { /\A[[:space:]]*\z/ =~ " ".encode("UTF-16LE") }.should.raise(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(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(Encoding::CompatibilityError) + end + + it "raises ArgumentError when trying to match a broken String" do + s = "\x80".dup.force_encoding('UTF-8') + -> { s =~ /./ }.should.raise(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 14e1424d47..4a0e611540 100644 --- a/spec/ruby/language/regexp/escapes_spec.rb +++ b/spec/ruby/language/regexp/escapes_spec.rb @@ -1,9 +1,11 @@ -# -*- encoding: binary -*- +# encoding: binary 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,23 +39,93 @@ 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 + /\xAA/.match("\nA").should == nil /\x0AA/.match("\nA").to_a.should == ["\nA"] /\xAG/.match("\nG").to_a.should == ["\nG"] # Non-matches - -> { eval('/\xG/') }.should raise_error(SyntaxError) + -> { eval('/\xG/') }.should.raise(SyntaxError) # \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"] @@ -64,18 +136,34 @@ describe "Regexps with escape characters" do /\c,\cL\cl/.match("\f\f\f").to_a.should == ["\f\f\f"] /\c-\cM\cm/.match("\r\r\r").to_a.should == ["\r\r\r"] - /\cJ/.match("\r").should be_nil + /\cJ/.match("\r").should == nil # Parsing precedence /\cJ+/.match("\n\n").to_a.should == ["\n\n"] # Quantifiers apply to entire escape sequence /\\cJ/.match("\\cJ").to_a.should == ["\\cJ"] - -> { eval('/[abc\x]/') }.should raise_error(SyntaxError) # \x is treated as a escape sequence even inside a character class + -> { eval('/[abc\x]/') }.should.raise(SyntaxError) # \x is treated as a escape sequence even inside a character class # Syntax error - -> { eval('/\c/') }.should raise_error(SyntaxError) + -> { eval('/\c/') }.should.raise(SyntaxError) # \cx control char (character code point value) # \C-x control char (character code point value) # \M-x meta (x|0x80) (character code point value) # \M-\C-x meta control char (character code point value) end + + it "handles three digit octal escapes starting with 0" do + /[\000-\b]/.match("\x00")[0].should == "\x00" + end + + it "handles control escapes with \\C-x syntax" do + /\C-*\C-J\C-j/.match("\n\n\n")[0].should == "\n\n\n" + end + + it "supports the \\K keep operator" do + /a\Kb/.match("ab")[0].should == "b" + end + + it "supports the \\R line break escape" do + /\R/.match("\n")[0].should == "\n" + end end diff --git a/spec/ruby/language/regexp/grouping_spec.rb b/spec/ruby/language/regexp/grouping_spec.rb index 8806d06746..80ad7460da 100644 --- a/spec/ruby/language/regexp/grouping_spec.rb +++ b/spec/ruby/language/regexp/grouping_spec.rb @@ -12,7 +12,7 @@ describe "Regexps with grouping" do end it "raises a SyntaxError when parentheses aren't balanced" do - -> { eval "/(hay(st)ack/" }.should raise_error(SyntaxError) + -> { eval "/(hay(st)ack/" }.should.raise(SyntaxError) end it "supports (?: ) (non-capturing group)" do @@ -20,4 +20,44 @@ describe "Regexps with grouping" do # Parsing precedence /(?:xdigit:)/.match("xdigit:").to_a.should == ["xdigit:"] end + + it "group names cannot start with digits or minus" do + -> { Regexp.new("(?<1a>a)") }.should.raise(RegexpError) + -> { Regexp.new("(?<-a>a)") }.should.raise(RegexpError) + end + + it "ignore capture groups in line comments" do + /^ + (a) # there is a capture group on this line + 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/interpolation_spec.rb b/spec/ruby/language/regexp/interpolation_spec.rb index ed0b724763..f771d0a395 100644 --- a/spec/ruby/language/regexp/interpolation_spec.rb +++ b/spec/ruby/language/regexp/interpolation_spec.rb @@ -36,14 +36,14 @@ describe "Regexps with interpolation" do it "gives precedence to escape sequences over substitution" do str = "J" - /\c#{str}/.to_s.should == '(?-mix:\c#' + '{str})' + /\c#{str}/.to_s.should.include?('{str}') end it "throws RegexpError for malformed interpolation" do s = "" - -> { /(#{s}/ }.should raise_error(RegexpError) + -> { /(#{s}/ }.should.raise(RegexpError) s = "(" - -> { /#{s}/ }.should raise_error(RegexpError) + -> { /#{s}/ }.should.raise(RegexpError) end it "allows interpolation in extended mode" do diff --git a/spec/ruby/language/regexp/modifiers_spec.rb b/spec/ruby/language/regexp/modifiers_spec.rb index 2f5522bc8a..c96fbfa983 100644 --- a/spec/ruby/language/regexp/modifiers_spec.rb +++ b/spec/ruby/language/regexp/modifiers_spec.rb @@ -8,7 +8,7 @@ describe "Regexps with modifiers" do it "supports /m (multiline)" do /foo.bar/m.match("foo\nbar").to_a.should == ["foo\nbar"] - /foo.bar/.match("foo\nbar").should be_nil + /foo.bar/.match("foo\nbar").should == nil end it "supports /x (extended syntax)" do @@ -36,7 +36,7 @@ describe "Regexps with modifiers" do /foo/imox.match("foo").to_a.should == ["foo"] /foo/imoximox.match("foo").to_a.should == ["foo"] - -> { eval('/foo/a') }.should raise_error(SyntaxError) + -> { eval('/foo/a') }.should.raise(SyntaxError) end it "supports (?~) (absent operator)" do @@ -46,57 +46,57 @@ describe "Regexps with modifiers" do it "supports (?imx-imx) (inline modifiers)" do /(?i)foo/.match("FOO").to_a.should == ["FOO"] - /foo(?i)/.match("FOO").should be_nil + /foo(?i)/.match("FOO").should == nil # Interaction with /i - /(?-i)foo/i.match("FOO").should be_nil + /(?-i)foo/i.match("FOO").should == nil /foo(?-i)/i.match("FOO").to_a.should == ["FOO"] # Multiple uses /foo (?i)bar (?-i)baz/.match("foo BAR baz").to_a.should == ["foo BAR baz"] - /foo (?i)bar (?-i)baz/.match("foo BAR BAZ").should be_nil + /foo (?i)bar (?-i)baz/.match("foo BAR BAZ").should == nil /(?m)./.match("\n").to_a.should == ["\n"] - /.(?m)/.match("\n").should be_nil + /.(?m)/.match("\n").should == nil # Interaction with /m - /(?-m)./m.match("\n").should be_nil + /(?-m)./m.match("\n").should == nil /.(?-m)/m.match("\n").to_a.should == ["\n"] # Multiple uses /. (?m). (?-m)./.match(". \n .").to_a.should == [". \n ."] - /. (?m). (?-m)./.match(". \n \n").should be_nil + /. (?m). (?-m)./.match(". \n \n").should == nil /(?x) foo /.match("foo").to_a.should == ["foo"] - / foo (?x)/.match("foo").should be_nil + / foo (?x)/.match("foo").should == nil # Interaction with /x - /(?-x) foo /x.match("foo").should be_nil + /(?-x) foo /x.match("foo").should == nil / foo (?-x)/x.match("foo").to_a.should == ["foo"] # Multiple uses /( foo )(?x)( bar )(?-x)( baz )/.match(" foo bar baz ").to_a.should == [" foo bar baz ", " foo ", "bar", " baz "] - /( foo )(?x)( bar )(?-x)( baz )/.match(" foo barbaz").should be_nil + /( foo )(?x)( bar )(?-x)( baz )/.match(" foo barbaz").should == nil # Parsing - /(?i-i)foo/.match("FOO").should be_nil + /(?i-i)foo/.match("FOO").should == nil /(?ii)foo/.match("FOO").to_a.should == ["FOO"] /(?-)foo/.match("foo").to_a.should == ["foo"] - -> { eval('/(?o)/') }.should raise_error(SyntaxError) + -> { eval('/(?o)/') }.should.raise(SyntaxError) end it "supports (?imx-imx:expr) (scoped inline modifiers)" do /foo (?i:bar) baz/.match("foo BAR baz").to_a.should == ["foo BAR baz"] - /foo (?i:bar) baz/.match("foo BAR BAZ").should be_nil - /foo (?-i:bar) baz/i.match("foo BAR BAZ").should be_nil + /foo (?i:bar) baz/.match("foo BAR BAZ").should == nil + /foo (?-i:bar) baz/i.match("foo BAR BAZ").should == nil /. (?m:.) ./.match(". \n .").to_a.should == [". \n ."] - /. (?m:.) ./.match(". \n \n").should be_nil - /. (?-m:.) ./m.match("\n \n \n").should be_nil + /. (?m:.) ./.match(". \n \n").should == nil + /. (?-m:.) ./m.match("\n \n \n").should == nil /( foo )(?x: bar )( baz )/.match(" foo bar baz ").to_a.should == [" foo bar baz ", " foo ", " baz "] - /( foo )(?x: bar )( baz )/.match(" foo barbaz").should be_nil + /( foo )(?x: bar )( baz )/.match(" foo barbaz").should == nil /( foo )(?-x: bar )( baz )/x.match("foo bar baz").to_a.should == ["foo bar baz", "foo", "baz"] # Parsing - /(?i-i:foo)/.match("FOO").should be_nil + /(?i-i:foo)/.match("FOO").should == nil /(?ii:foo)/.match("FOO").to_a.should == ["FOO"] /(?-:)foo/.match("foo").to_a.should == ["foo"] - -> { eval('/(?o:)/') }.should raise_error(SyntaxError) + -> { eval('/(?o:)/') }.should.raise(SyntaxError) end it "supports . with /m" do diff --git a/spec/ruby/language/regexp/repetition_spec.rb b/spec/ruby/language/regexp/repetition_spec.rb index 7bb767ccaf..f24323de5c 100644 --- a/spec/ruby/language/regexp/repetition_spec.rb +++ b/spec/ruby/language/regexp/repetition_spec.rb @@ -15,7 +15,7 @@ describe "Regexps with repetition" do it "supports + (1 or more of previous subexpression)" do /a+/.match("aaa").to_a.should == ["aaa"] - /a+/.match("bbb").should be_nil + /a+/.match("bbb").should == nil /<.+>/.match("<a>foo</a>").to_a.should == ["<a>foo</a>"] # it is greedy end @@ -45,4 +45,94 @@ describe "Regexps with repetition" do /a?/.match("aaa").to_a.should == ["a"] /a?/.match("bbb").to_a.should == [""] end + + it "handles incomplete range quantifiers" do + /a{}/.match("a{}")[0].should == "a{}" + /a{,}/.match("a{,}")[0].should == "a{,}" + /a{1/.match("a{1")[0].should == "a{1" + /a{1,2/.match("a{1,2")[0].should == "a{1,2" + /a{,5}/.match("aaa")[0].should == "aaa" + end + + it "lets us use quantifiers on assertions" do + /a^?b/.match("ab")[0].should == "ab" + /a$?b/.match("ab")[0].should == "ab" + /a\A?b/.match("ab")[0].should == "ab" + /a\Z?b/.match("ab")[0].should == "ab" + /a\z?b/.match("ab")[0].should == "ab" + /a\G?b/.match("ab")[0].should == "ab" + /a\b?b/.match("ab")[0].should == "ab" + /a\B?b/.match("ab")[0].should == "ab" + /a(?=c)?b/.match("ab")[0].should == "ab" + /a(?!=b)?b/.match("ab")[0].should == "ab" + /a(?<=c)?b/.match("ab")[0].should == "ab" + /a(?<!a)?b/.match("ab")[0].should == "ab" + end + + it "does not delete optional assertions" do + /(?=(a))?/.match("a").to_a.should == [ "", "a" ] + end + + it "supports nested quantifiers" do + suppress_warning do + eval <<-RUBY + /a***/.match("aaa")[0].should == "aaa" + + # a+?* should not be reduced, it should be equivalent to (a+?)* + # NB: the capture group prevents regex engines from reducing the two quantifiers + # https://bugs.ruby-lang.org/issues/17341 + /a+?*/.match("")[0].should == "" + /(a+?)*/.match("")[0].should == "" + + /a+?*/.match("a")[0].should == "a" + /(a+?)*/.match("a")[0].should == "a" + + /a+?*/.match("aa")[0].should == "aa" + /(a+?)*/.match("aa")[0].should == "aa" + + # a+?+ should not be reduced, it should be equivalent to (a+?)+ + # https://bugs.ruby-lang.org/issues/17341 + /a+?+/.match("").should == nil + /(a+?)+/.match("").should == nil + + /a+?+/.match("a")[0].should == "a" + /(a+?)+/.match("a")[0].should == "a" + + /a+?+/.match("aa")[0].should == "aa" + /(a+?)+/.match("aa")[0].should == "aa" + + # both a**? and a+*? should be equivalent to (a+)?? + # this quantifier would rather match nothing, but if that's not possible, + # it will greedily take everything + /a**?/.match("")[0].should == "" + /(a*)*?/.match("")[0].should == "" + /a+*?/.match("")[0].should == "" + /(a+)*?/.match("")[0].should == "" + /(a+)??/.match("")[0].should == "" + + /a**?/.match("aaa")[0].should == "" + /(a*)*?/.match("aaa")[0].should == "" + /a+*?/.match("aaa")[0].should == "" + /(a+)*?/.match("aaa")[0].should == "" + /(a+)??/.match("aaa")[0].should == "" + + /b.**?b/.match("baaabaaab")[0].should == "baaabaaab" + /b(.*)*?b/.match("baaabaaab")[0].should == "baaabaaab" + /b.+*?b/.match("baaabaaab")[0].should == "baaabaaab" + /b(.+)*?b/.match("baaabaaab")[0].should == "baaabaaab" + /b(.+)??b/.match("baaabaaab")[0].should == "baaabaaab" + RUBY + end + end + + it "treats ? after {n} quantifier as another quantifier, not as non-greedy marker" do + /a{2}?/.match("").to_a.should == [""] + end + + it "matches zero-width capture groups in optional iterations of loops" do + /()?/.match("").to_a.should == ["", ""] + /(a*)?/.match("").to_a.should == ["", ""] + /(a*)*/.match("").to_a.should == ["", ""] + /(?:a|()){500,1000}/.match("a" * 500).to_a.should == ["a" * 500, ""] + end end diff --git a/spec/ruby/language/regexp/subexpression_call_spec.rb b/spec/ruby/language/regexp/subexpression_call_spec.rb new file mode 100644 index 0000000000..16b64cb327 --- /dev/null +++ b/spec/ruby/language/regexp/subexpression_call_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' +require_relative '../fixtures/classes' + +describe "Regexps with subexpression calls" do + it "allows numeric subexpression calls" do + /(a)\g<1>/.match("aa").to_a.should == [ "aa", "a" ] + end + + it "treats subexpression calls as distinct from simple back-references" do + # Back-references only match a string which is equal to the original captured string. + /(?<three_digits>[0-9]{3})-\k<three_digits>/.match("123-123")[0].should == "123-123" + /(?<three_digits>[0-9]{3})-\k<three_digits>/.match("123-456").should == nil + # However, subexpression calls reuse the previous expression and can match a different + # string. + /(?<three_digits>[0-9]{3})-\g<three_digits>/.match("123-456")[0].should == "123-456" + end + + it "allows recursive subexpression calls" do + # This pattern matches well-nested parenthesized expression. + parens = /^ (?<parens> (?: \( \g<parens> \) | [^()] )* ) $/x + parens.match("((a)(b))c(d)")[0].should == "((a)(b))c(d)" + parens.match("((a)(b)c(d)").should == nil + end + + it "allows access to back-references from the current level" do + # Using \\k<first_char-0> accesses the last value captured in first_char + # on the current stack level. + mirror = /^ (?<mirror> (?: (?<first_char>.) \g<mirror> \k<first_char-0> )? ) $/x + mirror.match("abccba")[0].should == "abccba" + mirror.match("abccbd").should == nil + + # OTOH, using \\k<first_char> accesses the last value captured in first_char, + # regardless of the stack level. Therefore, it can't be used to implement + # the mirror language. + broken_mirror = /^ (?<mirror> (?: (?<first_char>.) \g<mirror> \k<first_char> )? ) $/x + broken_mirror.match("abccba").should == nil + # This matches because the 'c' is captured in first_char and that value is + # then used for all subsequent back-references, regardless of nesting. + broken_mirror.match("abcccc")[0].should == "abcccc" + end + + it "allows + and - in group names and referential constructs that don't use levels, i.e. subexpression calls" do + /(?<a+>a)\g<a+>/.match("aa").to_a.should == [ "aa", "a" ] + /(?<a+b>a)\g<a+b>/.match("aa").to_a.should == [ "aa", "a" ] + /(?<a+1>a)\g<a+1>/.match("aa").to_a.should == [ "aa", "a" ] + /(?<a->a)\g<a->/.match("aa").to_a.should == [ "aa", "a" ] + /(?<a-b>a)\g<a-b>/.match("aa").to_a.should == [ "aa", "a" ] + /(?<a-1>a)\g<a-1>/.match("aa").to_a.should == [ "aa", "a" ] + end +end diff --git a/spec/ruby/language/regexp_spec.rb b/spec/ruby/language/regexp_spec.rb index def9bba5f7..1452b01935 100644 --- a/spec/ruby/language/regexp_spec.rb +++ b/spec/ruby/language/regexp_spec.rb @@ -15,13 +15,11 @@ describe "Literal Regexps" do end it "yields a Regexp" do - /Hello/.should be_kind_of(Regexp) + /Hello/.should.is_a?(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 @@ -29,11 +27,11 @@ describe "Literal Regexps" do 2.times do |i| rs << /foo/ end - rs[0].should equal(rs[1]) + rs[0].should.equal?(rs[1]) end it "throws SyntaxError for malformed literals" do - -> { eval('/(/') }.should raise_error(SyntaxError) + -> { eval('/(/') }.should.raise(SyntaxError) end ############################################################################# @@ -60,22 +58,22 @@ describe "Literal Regexps" do it "disallows first part of paired delimiters to be used as non-paired delimiters" do LanguageSpecs.paired_delimiters.each do |p0, p1| - -> { eval("%r#{p0} foo #{p0}") }.should raise_error(SyntaxError) + -> { eval("%r#{p0} foo #{p0}") }.should.raise(SyntaxError) end end - it "supports non-paired delimiters delimiters with %r" do + it "supports non-paired delimiters with %r" do LanguageSpecs.non_paired_delimiters.each do |c| eval("%r#{c} foo #{c}").should == / foo / end end it "disallows alphabets as non-paired delimiter with %r" do - -> { eval('%ra foo a') }.should raise_error(SyntaxError) + -> { eval('%ra foo a') }.should.raise(SyntaxError) end it "disallows spaces after %r and delimiter" do - -> { eval('%r !foo!') }.should raise_error(SyntaxError) + -> { eval('%r !foo!') }.should.raise(SyntaxError) end it "allows unescaped / to be used with %r" do @@ -91,19 +89,18 @@ describe "Literal Regexps" do # Basic matching /./.match("foo").to_a.should == ["f"] # Basic non-matching - /./.match("").should be_nil - /./.match("\n").should be_nil + /./.match("").should == nil + /./.match("\n").should == nil /./.match("\0").to_a.should == ["\0"] end - it "supports | (alternations)" do /a|b/.match("a").to_a.should == ["a"] end it "supports (?> ) (embedded subexpression)" do /(?>foo)(?>bar)/.match("foobar").to_a.should == ["foobar"] - /(?>foo*)obar/.match("foooooooobar").should be_nil # it is possessive + /(?>foo*)obar/.match("foooooooobar").should == nil # it is possessive end it "supports (?# )" do @@ -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", ""..."4.0" 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 @@ -137,9 +135,9 @@ describe "Literal Regexps" do it "supports possessive quantifiers" do /fooA++bar/.match("fooAAAbar").to_a.should == ["fooAAAbar"] - /fooA++Abar/.match("fooAAAbar").should be_nil - /fooA?+Abar/.match("fooAAAbar").should be_nil - /fooA*+Abar/.match("fooAAAbar").should be_nil + /fooA++Abar/.match("fooAAAbar").should == nil + /fooA?+Abar/.match("fooAAAbar").should == nil + /fooA*+Abar/.match("fooAAAbar").should == nil end it "supports conditional regular expressions with positional capture groups" 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 54cbae1440..cf16d8f6f8 100644 --- a/spec/ruby/language/rescue_spec.rb +++ b/spec/ruby/language/rescue_spec.rb @@ -52,6 +52,16 @@ describe "The rescue keyword" do RescueSpecs::SafeNavigationSetterCaptor.should_capture_exception end + it 'using a safely navigated setter method on a nil target' do + target = nil + begin + raise SpecificExampleException, "Raising this to be handled below" + rescue SpecificExampleException => target&.captured_error + :caught + end.should == :caught + target.should == nil + end + it 'using a setter method' do RescueSpecs::SetterCaptor.should_capture_exception end @@ -61,6 +71,82 @@ 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 [] + loaded_features = $".dup + begin + require_relative 'fixtures/rescue/top_level' + + ScratchPad.recorded.should == ["message"] + ensure + $".replace loaded_features + end + end + end + it "returns value from `rescue` if an exception was raised" do begin raise @@ -100,7 +186,7 @@ describe "The rescue keyword" do rescue *exception_list caught_it = true end - caught_it.should be_true + caught_it.should == true caught = [] [->{raise ArbitraryException}, ->{raise SpecificExampleException}].each do |block| begin @@ -111,10 +197,22 @@ describe "The rescue keyword" do end caught.size.should == 2 exception_list.each do |exception_class| - caught.map{|e| e.class}.should include(exception_class) + caught.map{|e| e.class}.should.include?(exception_class) 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 == true + end + it "can combine a splatted list of exceptions with a literal list of exceptions" do caught_it = false begin @@ -122,7 +220,7 @@ describe "The rescue keyword" do rescue ArbitraryException, *exception_list caught_it = true end - caught_it.should be_true + caught_it.should == true caught = [] [->{raise ArbitraryException}, ->{raise SpecificExampleException}].each do |block| begin @@ -133,7 +231,7 @@ describe "The rescue keyword" do end caught.size.should == 2 exception_list.each do |exception_class| - caught.map{|e| e.class}.should include(exception_class) + caught.map{|e| e.class}.should.include?(exception_class) end end @@ -143,7 +241,7 @@ describe "The rescue keyword" do raise OtherCustomException, "not rescued!" rescue *exception_list end - end.should raise_error(OtherCustomException) + end.should.raise(OtherCustomException) end it "can rescue different types of exceptions in different ways" do @@ -179,7 +277,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 +336,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(SyntaxError, /else without rescue is useless/) end it "will not execute an else block if an exception was raised" do @@ -333,7 +413,7 @@ describe "The rescue keyword" do ScratchPad << :two raise SpecificExampleException, "an error from else" end - end.should raise_error(SpecificExampleException) + end.should.raise(SpecificExampleException) ScratchPad.recorded.should == [:one, :two] end @@ -365,7 +445,7 @@ describe "The rescue keyword" do rescue ScratchPad << :caught end - }.should raise_error(exception.class) + }.should.raise(exception.class) end ScratchPad.recorded.should == [] end @@ -396,7 +476,7 @@ describe "The rescue keyword" do raise "error" rescue rescuer end - }.should raise_error(TypeError) { |e| + }.should.raise(TypeError) { |e| e.message.should =~ /class or module required for rescue clause/ } end @@ -408,7 +488,7 @@ describe "The rescue keyword" do raise "error" rescue *rescuer end - }.should raise_error(TypeError) { |e| + }.should.raise(TypeError) { |e| e.message.should =~ /class or module required for rescue clause/ } end @@ -428,9 +508,9 @@ describe "The rescue keyword" do raise "from block" rescue (raise "from rescue expression") end - }.should raise_error(RuntimeError, "from rescue expression") do |e| + }.should.raise(RuntimeError, "from rescue expression") { |e| e.cause.message.should == "from block" - end + } end it "should splat the handling Error classes" do @@ -462,7 +542,7 @@ describe "The rescue keyword" do :caught } ruby - }.should raise_error(SyntaxError) + }.should.raise(SyntaxError) end it "allows rescue in 'do end' block" do @@ -483,10 +563,25 @@ describe "The rescue keyword" do end it "requires the 'rescue' in method arguments to be wrapped in parens" do - -> { eval '1.+(1 rescue 1)' }.should raise_error(SyntaxError) + -> { eval '1.+(1 rescue 1)' }.should.raise(SyntaxError) 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[0].should =~ /#{__FILE__}:#{line-3}:in 'foo'/ + foo[1].should =~ /#{__FILE__}:#{line+2}:in 'block/ + end + end + describe "inline form" do it "can be inlined" do a = 1/0 rescue 1 @@ -498,7 +593,7 @@ describe "The rescue keyword" do eval <<-ruby a = 1 rescue RuntimeError 2 ruby - }.should raise_error(SyntaxError) + }.should.raise(SyntaxError) end it "rescues only StandardError and its subclasses" do @@ -507,17 +602,15 @@ describe "The rescue keyword" do -> { a = raise(Exception) rescue 1 - }.should raise_error(Exception) + }.should.raise(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/reserved_keywords.rb b/spec/ruby/language/reserved_keywords.rb new file mode 100644 index 0000000000..bd1a55feec --- /dev/null +++ b/spec/ruby/language/reserved_keywords.rb @@ -0,0 +1,149 @@ +require_relative '../spec_helper' + +describe "Ruby's reserved keywords" do + # Copied from https://github.com/ruby/ruby/blob/master/defs/keywords + keywords = %w[ + alias + and + begin + BEGIN + break + case + class + def + defined? + do + else + elsif + end + END + ensure + false + for + if + in + module + next + nil + not + or + redo + rescue + retry + return + self + super + then + true + undef + unless + until + when + while + yield + __ENCODING__ + __FILE__ + __LINE__ + ] + + keywords.each do |name| + describe "keyword '#{name}'" do + it "can't be used as local variable name" do + -> { eval(<<~RUBY) }.should.raise(SyntaxError) + #{name} = :local_variable + RUBY + end + + if name == "defined?" + it "can't be used as an instance variable name" do + -> { eval(<<~RUBY) }.should.raise(SyntaxError) + @#{name} = :instance_variable + RUBY + end + + it "can't be used as a class variable name" do + -> { eval(<<~RUBY) }.should.raise(SyntaxError) + class C + @@#{name} = :class_variable + end + RUBY + end + + it "can't be used as a global variable name" do + -> { eval(<<~RUBY) }.should.raise(SyntaxError) + $#{name} = :global_variable + RUBY + end + else + it "can be used as an instance variable name" do + result = eval <<~RUBY + @#{name} = :instance_variable + @#{name} + RUBY + + result.should == :instance_variable + end + + it "can be used as a class variable name" do + result = eval <<~RUBY + class C + @@#{name} = :class_variable + @@#{name} + end + RUBY + + result.should == :class_variable + end + + it "can be used as a global variable name" do + result = eval <<~RUBY + $#{name} = :global_variable + $#{name} + RUBY + + result.should == :global_variable + end + end + + it "can't be used as a positional parameter name" do + -> { eval(<<~RUBY) }.should.raise(SyntaxError) + def x(#{name}); end + RUBY + end + + invalid_kw_param_names = ["BEGIN","END","defined?"] + + if invalid_kw_param_names.include?(name) + it "can't be used a keyword parameter name" do + -> { eval(<<~RUBY) }.should.raise(SyntaxError) + def m(#{name}:); end + RUBY + end + else + it "can be used a keyword parameter name" do + result = instance_eval <<~RUBY + def m(#{name}:) + binding.local_variable_get(:#{name}) + end + + m(#{name}: :argument) + RUBY + + result.should == :argument + end + end + + it "can be used as a method name" do + result = instance_eval <<~RUBY + def #{name} + :method_return_value + end + + send(:#{name}) + RUBY + + result.should == :method_return_value + end + end + end +end diff --git a/spec/ruby/language/retry_spec.rb b/spec/ruby/language/retry_spec.rb index ee5377946f..39e58b7b5d 100644 --- a/spec/ruby/language/retry_spec.rb +++ b/spec/ruby/language/retry_spec.rb @@ -31,8 +31,11 @@ describe "The retry statement" do results.should == [1, 2, 3, 1, 2, 4, 5, 6, 4, 5] end - it "raises a SyntaxError when used outside of a begin statement" do - -> { eval 'retry' }.should raise_error(SyntaxError) + it "raises a SyntaxError when used outside of a rescue statement" do + -> { eval 'retry' }.should.raise(SyntaxError) + -> { eval 'begin; retry; end' }.should.raise(SyntaxError) + -> { eval 'def m; retry; end' }.should.raise(SyntaxError) + -> { eval 'module RetrySpecs; retry; end' }.should.raise(SyntaxError) end end diff --git a/spec/ruby/language/return_spec.rb b/spec/ruby/language/return_spec.rb index d8506834c8..36a3cba4d7 100644 --- a/spec/ruby/language/return_spec.rb +++ b/spec/ruby/language/return_spec.rb @@ -19,7 +19,7 @@ describe "The return keyword" do it "returns nil by default" do def r; return; end - r().should be_nil + r().should == nil end describe "in a Thread" do @@ -31,7 +31,7 @@ describe "The return keyword" do e end } - t.value.should be_an_instance_of(LocalJumpError) + t.value.should.instance_of?(LocalJumpError) end end @@ -176,11 +176,11 @@ describe "The return keyword" do end it "causes lambda to return nil if invoked without any arguments" do - -> { return; 456 }.call.should be_nil + -> { return; 456 }.call.should == nil end it "causes lambda to return nil if invoked with an empty expression" do - -> { return (); 456 }.call.should be_nil + -> { return (); 456 }.call.should == nil end it "causes lambda to return the value passed to return" do @@ -229,7 +229,7 @@ describe "The return keyword" do def f 1.times { 1.times {return true}; false}; false end - f.should be_true + f.should == true end end @@ -417,23 +417,36 @@ describe "The return keyword" do end END_OF_CODE - -> { load @filename }.should raise_error(SyntaxError) + -> { load @filename }.should.raise(SyntaxError) end 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(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..e8b429631d 100644 --- a/spec/ruby/language/safe_navigator_spec.rb +++ b/spec/ruby/language/safe_navigator_spec.rb @@ -2,48 +2,48 @@ require_relative '../spec_helper' describe "Safe navigator" do it "requires a method name to be provided" do - -> { eval("obj&. {}") }.should raise_error(SyntaxError) + -> { eval("obj&. {}") }.should.raise(SyntaxError) end 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(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(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,8 +139,8 @@ describe "Safe navigator" do obj = klass.new -> { - eval("obj&.foo += 3") - }.should raise_error(NoMethodError) { |e| + obj&.foo += 3 + }.should.raise(NoMethodError) { |e| e.name.should == :+ } end 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..f56a77d529 100644 --- a/spec/ruby/language/send_spec.rb +++ b/spec/ruby/language/send_spec.rb @@ -22,7 +22,7 @@ describe "Invoking a method" do it "raises ArgumentError if the method has a positive arity" do -> { specs.fooM1 - }.should raise_error(ArgumentError) + }.should.raise(ArgumentError) end end @@ -38,12 +38,12 @@ describe "Invoking a method" do it "raises ArgumentError if the methods arity doesn't match" do -> { specs.fooM1(1,2) - }.should raise_error(ArgumentError) + }.should.raise(ArgumentError) end 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 @@ -54,7 +54,7 @@ describe "Invoking a method" do it "raises ArgumentError if extra arguments are passed" do -> { specs.fooM0O1(2,3) - }.should raise_error(ArgumentError) + }.should.raise(ArgumentError) end end @@ -66,13 +66,13 @@ describe "Invoking a method" do it "raises an ArgumentError if there are no values for the mandatory args" do -> { specs.fooM1O1 - }.should raise_error(ArgumentError) + }.should.raise(ArgumentError) end it "raises an ArgumentError if too many values are passed" do -> { specs.fooM1O1(1,2,3) - }.should raise_error(ArgumentError) + }.should.raise(ArgumentError) end end @@ -94,7 +94,7 @@ describe "Invoking a method" do it "with a block converts the block to a Proc" do prc = specs.makeproc { "hello" } - prc.should be_kind_of(Proc) + prc.should.is_a?(Proc) prc.call.should == "hello" end @@ -106,10 +106,28 @@ describe "Invoking a method" do specs.yield_now(&o).should == :from_to_proc end + ruby_version_is "4.0" do + it "raises TypeError if 'to_proc' doesn't return a Proc" do + o = LangSendSpecs::RawToProc.new(42) + + -> { + specs.makeproc(&o) + }.should raise_consistent_error(TypeError, "can't convert LangSendSpecs::RawToProc into Proc (LangSendSpecs::RawToProc#to_proc gives Integer)") + end + + it "raises TypeError if block object isn't a Proc and doesn't respond to `to_proc`" do + o = Object.new + + -> { + specs.makeproc(&o) + }.should.raise(TypeError, "no implicit conversion of Object into Proc") + end + end + it "raises a SyntaxError with both a literal block and an object as block" do -> { eval "specs.oneb(10, &l){ 42 }" - }.should raise_error(SyntaxError) + }.should.raise(SyntaxError) end it "with same names as existing variables is ok" do @@ -194,21 +212,42 @@ describe "Invoking a method" do o.args.should == [1,2] end - it "raises NameError if invoked as a vcall" do - -> { no_such_method }.should raise_error NameError + describe "if invoked as a vcall" do + it "raises NameError" do + -> { no_such_method }.should.raise NameError + end + + it "raises NameError with $! as a cause" do + begin + raise RuntimeError.new + rescue => cause + -> { no_such_method }.should.raise(NameError, cause:) + end + end end it "should omit the method_missing call from the backtrace for NameError" do - -> { no_such_method }.should raise_error { |e| e.backtrace.first.should_not include("method_missing") } + -> { no_such_method }.should.raise { |e| e.backtrace.first.should_not.include?("method_missing") } end - it "raises NoMethodError if invoked as an unambiguous method call" do - -> { no_such_method() }.should raise_error NoMethodError - -> { no_such_method(1,2,3) }.should raise_error NoMethodError + describe "if invoked as an unambiguous method call" do + it "raises NoMethodError" do + -> { no_such_method() }.should.raise NoMethodError + -> { no_such_method(1,2,3) }.should.raise NoMethodError + end + + it "raises NoMethodError with $! as a cause" do + begin + raise + rescue => cause + -> { no_such_method() }.should.raise(NoMethodError, cause:) + -> { no_such_method(1,2,3) }.should.raise(NoMethodError, cause:) + end + end end it "should omit the method_missing call from the backtrace for NoMethodError" do - -> { no_such_method() }.should raise_error { |e| e.backtrace.first.should_not include("method_missing") } + -> { no_such_method() }.should.raise { |e| e.backtrace.first.should_not.include?("method_missing") } end end @@ -258,20 +297,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 @@ -347,7 +376,7 @@ describe "Invoking a method" do it "with splat operator * and non-Array value uses value unchanged if it does not respond_to?(:to_ary)" do obj = Object.new - obj.should_not respond_to(:to_a) + obj.should_not.respond_to?(:to_a) specs.fooM0R(*obj).should == [obj] specs.fooM1R(1,*obj).should == [1, [obj]] @@ -421,36 +450,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/shared/__FILE__.rb b/spec/ruby/language/shared/__FILE__.rb index 3e4f5c958d..7e2e871932 100644 --- a/spec/ruby/language/shared/__FILE__.rb +++ b/spec/ruby/language/shared/__FILE__.rb @@ -9,14 +9,14 @@ describe :language___FILE__, shared: true do end it "equals the absolute path of a file loaded by an absolute path" do - @object.send(@method, @path).should be_true + @object.send(@method, @path).should == true ScratchPad.recorded.should == [@path] end it "equals the absolute path of a file loaded by a relative path" do $LOAD_PATH << "." Dir.chdir CODE_LOADING_DIR do - @object.send(@method, "file_fixture.rb").should be_true + @object.send(@method, "file_fixture.rb").should == true end ScratchPad.recorded.should == [@path] end diff --git a/spec/ruby/language/shared/__LINE__.rb b/spec/ruby/language/shared/__LINE__.rb index 076b74b3ba..6bfc8c1c17 100644 --- a/spec/ruby/language/shared/__LINE__.rb +++ b/spec/ruby/language/shared/__LINE__.rb @@ -9,7 +9,7 @@ describe :language___LINE__, shared: true do end it "equals the line number of the text in a loaded file" do - @object.send(@method, @path).should be_true + @object.send(@method, @path).should == true ScratchPad.recorded.should == [1, 5] end end diff --git a/spec/ruby/language/singleton_class_spec.rb b/spec/ruby/language/singleton_class_spec.rb index 705d9f3548..20256a323c 100644 --- a/spec/ruby/language/singleton_class_spec.rb +++ b/spec/ruby/language/singleton_class_spec.rb @@ -14,40 +14,40 @@ describe "A singleton class" do nil.singleton_class.should == NilClass end - it "raises a TypeError for Fixnum's" do - -> { 1.singleton_class }.should raise_error(TypeError) + it "raises a TypeError for Integer's" do + -> { 1.singleton_class }.should.raise(TypeError) end it "raises a TypeError for symbols" do - -> { :symbol.singleton_class }.should raise_error(TypeError) + -> { :symbol.singleton_class }.should.raise(TypeError) end it "is a singleton Class instance" do o = mock('x') - o.singleton_class.should be_kind_of(Class) - o.singleton_class.should_not equal(Object) - o.should be_kind_of(o.singleton_class) + o.singleton_class.should.is_a?(Class) + o.singleton_class.should_not.equal?(Object) + o.should.is_a?(o.singleton_class) end it "is a Class for classes" do - ClassSpecs::A.singleton_class.should be_kind_of(Class) + ClassSpecs::A.singleton_class.should.is_a?(Class) end it "inherits from Class for classes" do - Class.should be_ancestor_of(Object.singleton_class) + Object.singleton_class.ancestors.should.include?(Class) end it "is a subclass of Class's singleton class" do ec = ClassSpecs::A.singleton_class - ec.should be_kind_of(Class.singleton_class) + ec.should.is_a?(Class.singleton_class) end it "is a subclass of the same level of Class's singleton class" do ecec = ClassSpecs::A.singleton_class.singleton_class class_ec = Class.singleton_class - ecec.should be_kind_of(class_ec.singleton_class) - ecec.should be_kind_of(class_ec) + ecec.should.is_a?(class_ec.singleton_class) + ecec.should.is_a?(class_ec) end it "is a subclass of a superclass's singleton class" do @@ -60,7 +60,7 @@ describe "A singleton class" do ClassSpecs::H.singleton_class.singleton_class end - it "for BasicObject has Class as it's superclass" do + it "for BasicObject has Class as its superclass" do BasicObject.singleton_class.superclass.should == Class end @@ -70,11 +70,11 @@ 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 - -> { bignum_value.singleton_class.superclass.should == Bignum }.should raise_error(TypeError) + -> { bignum_value.singleton_class }.should.raise(TypeError) end end @@ -105,20 +105,20 @@ describe "A constant on a singleton class" do end it "is not defined on the object's class" do - @object.class.const_defined?(:CONST).should be_false + @object.class.const_defined?(:CONST).should == false end it "is not defined in the singleton class opener's scope" do class << @object CONST end - -> { CONST }.should raise_error(NameError) + -> { CONST }.should.raise(NameError) end it "cannot be accessed via object::CONST" do -> do @object::CONST - end.should raise_error(TypeError) + end.should.raise(TypeError) end it "raises a NameError for anonymous_module::CONST" do @@ -129,15 +129,15 @@ describe "A constant on a singleton class" do -> do @object::CONST - end.should raise_error(NameError) + end.should.raise(NameError) end it "appears in the singleton class constant list" do - @object.singleton_class.should have_constant(:CONST) + @object.singleton_class.should.const_defined?(:CONST, false) end it "does not appear in the object's class constant list" do - @object.class.should_not have_constant(:CONST) + @object.class.should_not.const_defined?(:CONST) end it "is not preserved when the object is duped" do @@ -145,31 +145,14 @@ describe "A constant on a singleton class" do -> do class << @object; CONST; end - end.should raise_error(NameError) + end.should.raise(NameError) end it "is preserved when the object is cloned" do @object = @object.clone class << @object - CONST.should_not be_nil - end - 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/) + CONST.should_not == nil end end end @@ -185,7 +168,7 @@ describe "Defining instance methods on a singleton class" do end it "defines public methods" do - @k_sc.should have_public_instance_method(:singleton_method) + @k_sc.public_instance_methods(false).should.include?(:singleton_method) end end @@ -198,46 +181,46 @@ describe "Instance methods of a singleton class" do end it "include ones of the object's class" do - @k_sc.should have_instance_method(:example_instance_method) + @k_sc.should.method_defined?(:example_instance_method, true) end it "does not include class methods of the object's class" do - @k_sc.should_not have_instance_method(:example_class_method) + @k_sc.should_not.method_defined?(:example_class_method) end it "include instance methods of Object" do - @a_sc.should have_instance_method(:example_instance_method_of_object) + @a_sc.should.method_defined?(:example_instance_method_of_object, true) end it "does not include class methods of Object" do - @a_sc.should_not have_instance_method(:example_class_method_of_object) + @a_sc.should_not.method_defined?(:example_class_method_of_object) end describe "for a class" do it "include instance methods of Class" do - @a_c_sc.should have_instance_method(:example_instance_method_of_class) + @a_c_sc.should.method_defined?(:example_instance_method_of_class, true) end it "does not include class methods of Class" do - @a_c_sc.should_not have_instance_method(:example_class_method_of_class) + @a_c_sc.should_not.method_defined?(:example_class_method_of_class) end it "does not include instance methods of the singleton class of Class" do - @a_c_sc.should_not have_instance_method(:example_instance_method_of_singleton_class) + @a_c_sc.should_not.method_defined?(:example_instance_method_of_singleton_class) end it "does not include class methods of the singleton class of Class" do - @a_c_sc.should_not have_instance_method(:example_class_method_of_singleton_class) + @a_c_sc.should_not.method_defined?(:example_class_method_of_singleton_class) end end describe "for a singleton class" do it "includes instance methods of the singleton class of Class" do - @a_c_sc.singleton_class.should have_instance_method(:example_instance_method_of_singleton_class) + @a_c_sc.singleton_class.should.method_defined?(:example_instance_method_of_singleton_class, true) end it "does not include class methods of the singleton class of Class" do - @a_c_sc.singleton_class.should_not have_instance_method(:example_class_method_of_singleton_class) + @a_c_sc.singleton_class.should_not.method_defined?(:example_class_method_of_singleton_class) end end end @@ -251,46 +234,46 @@ describe "Class methods of a singleton class" do end it "include ones of the object's class" do - @k_sc.should have_method(:example_class_method) + @k_sc.should.respond_to?(:example_class_method) end it "does not include instance methods of the object's class" do - @k_sc.should_not have_method(:example_instance_method) + @k_sc.should_not.respond_to?(:example_instance_method) end it "include instance methods of Class" do - @a_sc.should have_method(:example_instance_method_of_class) + @a_sc.should.respond_to?(:example_instance_method_of_class) end it "does not include class methods of Class" do - @a_sc.should_not have_method(:example_class_method_of_class) + @a_sc.should_not.respond_to?(:example_class_method_of_class) end describe "for a class" do it "include instance methods of Class" do - @a_c_sc.should have_method(:example_instance_method_of_class) + @a_c_sc.should.respond_to?(:example_instance_method_of_class) end it "include class methods of Class" do - @a_c_sc.should have_method(:example_class_method_of_class) + @a_c_sc.should.respond_to?(:example_class_method_of_class) end it "include instance methods of the singleton class of Class" do - @a_c_sc.should have_method(:example_instance_method_of_singleton_class) + @a_c_sc.should.respond_to?(:example_instance_method_of_singleton_class) end it "does not include class methods of the singleton class of Class" do - @a_c_sc.should_not have_method(:example_class_method_of_singleton_class) + @a_c_sc.should_not.respond_to?(:example_class_method_of_singleton_class) end end describe "for a singleton class" do it "include instance methods of the singleton class of Class" do - @a_c_sc.singleton_class.should have_method(:example_instance_method_of_singleton_class) + @a_c_sc.singleton_class.should.respond_to?(:example_instance_method_of_singleton_class) end it "include class methods of the singleton class of Class" do - @a_c_sc.singleton_class.should have_method(:example_class_method_of_singleton_class) + @a_c_sc.singleton_class.should.respond_to?(:example_class_method_of_singleton_class) end end end @@ -299,12 +282,36 @@ describe "Instantiating a singleton class" do it "raises a TypeError when new is called" do -> { Object.new.singleton_class.new - }.should raise_error(TypeError) + }.should.raise(TypeError) end it "raises a TypeError when allocate is called" do -> { Object.new.singleton_class.allocate - }.should raise_error(TypeError) + }.should.raise(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 a0a29f63de..5a243fa2c3 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 @@ -29,7 +29,7 @@ describe "Source files" do touch(path, "wb") { |f| f.write source } begin - ruby_exe(path, args: "2>&1").should =~ /invalid multibyte char/ + ruby_exe(path, args: "2>&1", exit_status: 1).b.should =~ /invalid multibyte char/ ensure rm_r path end @@ -51,7 +51,7 @@ describe "Source files" do touch(path, "wb") { |f| f.write source } begin - ruby_exe(path, args: "2>&1").should =~ /invalid multibyte char/ + ruby_exe(path, args: "2>&1", exit_status: 1).b.should =~ /invalid multibyte char/ ensure rm_r path end diff --git a/spec/ruby/language/string_spec.rb b/spec/ruby/language/string_spec.rb index 0178083f58..163063032f 100644 --- a/spec/ruby/language/string_spec.rb +++ b/spec/ruby/language/string_spec.rb @@ -1,6 +1,7 @@ -# -*- encoding: binary -*- +# encoding: binary require_relative '../spec_helper' +require_relative 'fixtures/class_with_class_variable' # TODO: rewrite these horrid specs. it "are..." seriously?! @@ -27,6 +28,11 @@ describe "Ruby character strings" do "#$ip".should == 'xxx' end + it "interpolate class variables just with the # character" do + object = StringSpecs::ClassWithClassVariable.new + object.foo.should == 'xxx' + end + it "allows underscore as part of a variable name in a simple interpolation" do @my_ip = 'xxx' "#@my_ip".should == 'xxx' @@ -56,28 +62,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" @@ -149,6 +133,12 @@ describe "Ruby character strings" do "#{obj}".should == '42' end + it "raise NoMethodError when #to_s is not defined for the object" do + obj = BasicObject.new + + -> { "#{obj}" }.should.raise(NoMethodError) + end + it "uses an internal representation when #to_s doesn't return a String" do obj = mock('to_s') obj.stub!(:to_s).and_return(42) @@ -159,7 +149,7 @@ describe "Ruby character strings" do # is that if you interpolate an object that fails to return # a String, you will still get a String and not raise an # exception. - "#{obj}".should be_an_instance_of(String) + "#{obj}".should.instance_of?(String) end it "allows a dynamic string to parse a nested do...end block as an argument to a call without parens, interpolated" do @@ -253,8 +243,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 +271,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,25 +285,23 @@ 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) + -> { "#{a} #{b}" }.should.raise(Encoding::CompatibilityError) end it "creates a non-frozen String" do code = <<~'RUBY' - "a#{6*7}c" + "a#{6*7}c" RUBY 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' + 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 + 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..f595d49395 100644 --- a/spec/ruby/language/super_spec.rb +++ b/spec/ruby/language/super_spec.rb @@ -70,7 +70,7 @@ describe "The super keyword" do SuperSpecs::S4::B.new.foo([],"test").should == ["B#foo(a,test)", "A#foo"] end - it "raises an error error when super method does not exist" do + it "raises an error when super method does not exist" do sup = Class.new sub_normal = Class.new(sup) do def foo @@ -83,8 +83,8 @@ describe "The super keyword" do end end - -> {sub_normal.new.foo}.should raise_error(NoMethodError, /super/) - -> {sub_zsuper.new.foo}.should raise_error(NoMethodError, /super/) + -> {sub_normal.new.foo}.should.raise(NoMethodError, /super/) + -> {sub_zsuper.new.foo}.should.raise(NoMethodError, /super/) end it "uses given block even if arguments are passed explicitly" do @@ -130,7 +130,7 @@ describe "The super keyword" do end end - c2.new.m('a') { raise }.should be_false + c2.new.m('a') { raise }.should == false end it "uses block argument given to method when used in a block" do @@ -200,7 +200,26 @@ describe "The super keyword" do end end - -> { klass.new.a(:a_called) }.should raise_error(RuntimeError) + -> { klass.new.a(:a_called) }.should.raise(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 @@ -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 @@ -431,4 +461,18 @@ describe "The super keyword" do @all.foo('a', b: 'b').should == [['a'], {b: 'b'}] end end + + it "works in method definitions using **nil" do + parent = Class.new do + def m(*args, **kwargs) + [args, kwargs] + end + end + child = Class.new(parent) do + def m(*args, **nil) + super + end + end + child.new.m(1, 2).should == [[1, 2], {}] + end end diff --git a/spec/ruby/language/symbol_spec.rb b/spec/ruby/language/symbol_spec.rb index d6a41d3059..81fe06b50b 100644 --- a/spec/ruby/language/symbol_spec.rb +++ b/spec/ruby/language/symbol_spec.rb @@ -3,7 +3,7 @@ require_relative '../spec_helper' describe "A Symbol literal" do it "is a ':' followed by any number of valid characters" do a = :foo - a.should be_kind_of(Symbol) + a.should.is_a?(Symbol) a.inspect.should == ':foo' end @@ -21,7 +21,7 @@ describe "A Symbol literal" do :_Foo, :&, :_9 - ].each { |s| s.should be_kind_of(Symbol) } + ].each { |s| s.should.is_a?(Symbol) } end it "is a ':' followed by a single- or double-quoted string that may contain otherwise invalid characters" do @@ -31,7 +31,7 @@ describe "A Symbol literal" do [:"foo #{1 + 1}", ':"foo 2"'], [:"foo\nbar", ':"foo\nbar"'], ].each { |sym, str| - sym.should be_kind_of(Symbol) + sym.should.is_a?(Symbol) sym.inspect.should == str } end @@ -42,22 +42,22 @@ describe "A Symbol literal" do end it "may contain '::' in the string" do - :'Some::Class'.should be_kind_of(Symbol) + :'Some::Class'.should.is_a?(Symbol) end it "is converted to a literal, unquoted representation if the symbol contains only valid characters" do a, b, c = :'foo', :'+', :'Foo__9' - a.should be_kind_of(Symbol) + a.should.is_a?(Symbol) a.inspect.should == ':foo' - b.should be_kind_of(Symbol) + b.should.is_a?(Symbol) b.inspect.should == ':+' - c.should be_kind_of(Symbol) + c.should.is_a?(Symbol) c.inspect.should == ':Foo__9' end it "can be created by the %s-delimited expression" do a, b = :'foo bar', %s{foo bar} - b.should be_kind_of(Symbol) + b.should.is_a?(Symbol) b.inspect.should == ':"foo bar"' b.should == a end @@ -68,7 +68,7 @@ describe "A Symbol literal" do [:'a string', :'a string'], [:"#{var}", :"#{var}"] ].each { |a, b| - a.should equal(b) + a.should.equal?(b) } end @@ -78,7 +78,7 @@ describe "A Symbol literal" do it "can be an empty string" do c = :'' - c.should be_kind_of(Symbol) + c.should.is_a?(Symbol) c.inspect.should == ':""' end @@ -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(SyntaxError, /invalid symbol/) + ScratchPad.recorded.should == [] + end end end diff --git a/spec/ruby/language/throw_spec.rb b/spec/ruby/language/throw_spec.rb index d723843688..73f64de17d 100644 --- a/spec/ruby/language/throw_spec.rb +++ b/spec/ruby/language/throw_spec.rb @@ -35,7 +35,7 @@ describe "The throw keyword" do throw :exit end end - $!.should be_nil + $!.should == nil end it "allows any object as its argument" do @@ -45,7 +45,7 @@ describe "The throw keyword" do end it "does not convert strings to a symbol" do - -> { catch(:exit) { throw "exit" } }.should raise_error(ArgumentError) + -> { catch(:exit) { throw "exit" } }.should.raise(ArgumentError) end it "unwinds stack from within a method" do @@ -64,8 +64,8 @@ describe "The throw keyword" do end it "raises an ArgumentError if outside of scope of a matching catch" do - -> { throw :test, 5 }.should raise_error(ArgumentError) - -> { catch(:different) { throw :test, 5 } }.should raise_error(ArgumentError) + -> { throw :test, 5 }.should.raise(ArgumentError) + -> { catch(:different) { throw :test, 5 } }.should.raise(ArgumentError) end it "raises an UncaughtThrowError if used to exit a thread" do @@ -73,7 +73,7 @@ describe "The throw keyword" do t = Thread.new { -> { throw :what - }.should raise_error(UncaughtThrowError) + }.should.raise(UncaughtThrowError) } t.join end diff --git a/spec/ruby/language/undef_spec.rb b/spec/ruby/language/undef_spec.rb index 4e473b803f..98ecd99c21 100644 --- a/spec/ruby/language/undef_spec.rb +++ b/spec/ruby/language/undef_spec.rb @@ -14,35 +14,42 @@ describe "The undef keyword" do @undef_class.class_eval do undef meth end - -> { @obj.meth(5) }.should raise_error(NoMethodError) + -> { @obj.meth(5) }.should.raise(NoMethodError) end it "with a simple symbol" do @undef_class.class_eval do undef :meth end - -> { @obj.meth(5) }.should raise_error(NoMethodError) + -> { @obj.meth(5) }.should.raise(NoMethodError) end it "with a single quoted symbol" do @undef_class.class_eval do undef :'meth' end - -> { @obj.meth(5) }.should raise_error(NoMethodError) + -> { @obj.meth(5) }.should.raise(NoMethodError) end it "with a double quoted symbol" do @undef_class.class_eval do undef :"meth" end - -> { @obj.meth(5) }.should raise_error(NoMethodError) + -> { @obj.meth(5) }.should.raise(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) + -> { @obj.meth(5) }.should.raise(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(NoMethodError) end end @@ -62,8 +69,8 @@ describe "The undef keyword" do it "raises a NameError when passed a missing name" do Class.new do -> { - undef not_exist - }.should raise_error(NameError) { |e| + undef not_exist + }.should.raise(NameError) { |e| # a NameError and not a NoMethodError e.class.should == NameError } diff --git a/spec/ruby/language/variables_spec.rb b/spec/ruby/language/variables_spec.rb index ce072baa2c..e01e03f01e 100644 --- a/spec/ruby/language/variables_spec.rb +++ b/spec/ruby/language/variables_spec.rb @@ -1,6 +1,51 @@ 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 + 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 + describe "Multiple assignment" do context "with a single RHS value" do it "assigns a simple MLHS" do @@ -46,7 +91,7 @@ describe "Multiple assignment" do x = mock("multi-assign single RHS") x.should_receive(:to_ary).and_return(1) - -> { a, b, c = x }.should raise_error(TypeError) + -> { a, b, c = x }.should.raise(TypeError) end it "does not call #to_a to convert an Object RHS when assigning a simple MLHS" do @@ -77,7 +122,7 @@ describe "Multiple assignment" do ary = [1, 2] x = (a, b = ary) - x.should equal(ary) + x.should.equal?(ary) end it "returns the RHS when it is an Array subclass" do @@ -85,7 +130,7 @@ describe "Multiple assignment" do ary = cls.new [1, 2] x = (a, b = ary) - x.should equal(ary) + x.should.equal?(ary) end it "does not call #to_ary on an Array subclass instance" do @@ -127,7 +172,7 @@ describe "Multiple assignment" do x = mock("multi-assign splat") x.should_receive(:to_ary).and_return(1) - -> { *a = x }.should raise_error(TypeError) + -> { *a = x }.should.raise(TypeError) end it "does not call #to_ary on an Array subclass" do @@ -144,8 +189,8 @@ describe "Multiple assignment" do ary = cls.new [1, 2] x = (*a = ary) - x.should equal(ary) - a.should be_an_instance_of(Array) + x.should.equal?(ary) + a.should.instance_of?(Array) end it "calls #to_ary to convert an Object RHS with MLHS" do @@ -160,7 +205,7 @@ describe "Multiple assignment" do x = mock("multi-assign splat") x.should_receive(:to_ary).and_return(1) - -> { a, *b, c = x }.should raise_error(TypeError) + -> { a, *b, c = x }.should.raise(TypeError) end it "does not call #to_a to convert an Object RHS with a MLHS" do @@ -256,7 +301,7 @@ describe "Multiple assignment" do x = mock("multi-assign attributes") x.should_receive(:m).and_return(y) - -> { a, b = x.m }.should raise_error(TypeError) + -> { a, b = x.m }.should.raise(TypeError) end it "assigns values from a RHS method call with receiver and arguments" do @@ -287,8 +332,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 @@ -296,6 +346,9 @@ describe "Multiple assignment" do SINGLE_RHS_1, SINGLE_RHS_2 = 1 [SINGLE_RHS_1, SINGLE_RHS_2].should == [1, nil] end + ensure + VariableSpecs.send(:remove_const, :SINGLE_RHS_1) + VariableSpecs.send(:remove_const, :SINGLE_RHS_2) end end @@ -310,11 +363,22 @@ describe "Multiple assignment" do a.should == [] end - it "calls #to_a to convert nil to an empty Array" do - nil.should_receive(:to_a).and_return([]) + ruby_version_is "4.0" do + it "converts nil to empty array without calling a method" do + nil.should_not_receive(:to_a) - (*a = *nil).should == [] - a.should == [] + (*a = *nil).should == [] + a.should == [] + end + end + + ruby_version_is ""..."4.0" do + it "calls #to_a to convert nil to an empty Array" do + nil.should_receive(:to_a).and_return([]) + + (*a = *nil).should == [] + a.should == [] + end end it "does not call #to_a on an Array" do @@ -329,7 +393,7 @@ describe "Multiple assignment" do ary = [1, 2] (a = *ary).should == [1, 2] - a.should_not equal(ary) + a.should_not.equal?(ary) end it "does not call #to_a on an Array subclass" do @@ -348,10 +412,10 @@ describe "Multiple assignment" do x = (a = *ary) x.should == [1, 2] - x.should be_an_instance_of(Array) + x.should.instance_of?(Array) a.should == [1, 2] - a.should be_an_instance_of(Array) + a.should.instance_of?(Array) end it "unfreezes the array returned from calling 'to_a' on the splatted value" do @@ -417,7 +481,7 @@ describe "Multiple assignment" do x = mock("multi-assign RHS splat") x.should_receive(:to_a).and_return(1) - -> { *a = *x }.should raise_error(TypeError) + -> { *a = *x }.should.raise(TypeError) end it "does not call #to_ary to convert an Object RHS with a single splat LHS" do @@ -463,7 +527,7 @@ describe "Multiple assignment" do x = mock("multi-assign splat") x.should_receive(:to_a).and_return(1) - -> { a = *x }.should raise_error(TypeError) + -> { a = *x }.should.raise(TypeError) end it "calls #to_a to convert an Object splat RHS when assigned to a simple MLHS" do @@ -478,7 +542,7 @@ describe "Multiple assignment" do x = mock("multi-assign splat") x.should_receive(:to_a).and_return(1) - -> { a, b, c = *x }.should raise_error(TypeError) + -> { a, b, c = *x }.should.raise(TypeError) end it "does not call #to_ary to convert an Object splat RHS when assigned to a simple MLHS" do @@ -501,7 +565,7 @@ describe "Multiple assignment" do x = mock("multi-assign splat") x.should_receive(:to_a).and_return(1) - -> { a, *b, c = *x }.should raise_error(TypeError) + -> { a, *b, c = *x }.should.raise(TypeError) end it "does not call #to_ary to convert an Object RHS with a MLHS" do @@ -534,6 +598,8 @@ describe "Multiple assignment" do (*SINGLE_SPLATTED_RHS) = *1 SINGLE_SPLATTED_RHS.should == [1] end + ensure + VariableSpecs.send(:remove_const, :SINGLE_SPLATTED_RHS) end end @@ -579,7 +645,7 @@ describe "Multiple assignment" do x = mock("multi-assign splat MRHS") x.should_receive(:to_a).and_return(1) - -> { a, *b = 1, *x }.should raise_error(TypeError) + -> { a, *b = 1, *x }.should.raise(TypeError) end it "does not call #to_ary to convert a splatted Object as part of a MRHS with a splat MRHS" do @@ -602,7 +668,7 @@ describe "Multiple assignment" do x = mock("multi-assign splat MRHS") x.should_receive(:to_a).and_return(1) - -> { a, *b = *x, 1 }.should raise_error(TypeError) + -> { a, *b = *x, 1 }.should.raise(TypeError) end it "does not call #to_ary to convert a splatted Object with a splat MRHS" do @@ -651,7 +717,7 @@ describe "Multiple assignment" do x = mock("multi-assign mixed RHS") x.should_receive(:to_ary).and_return(x) - -> { a, (b, c), d = 1, x, 3, 4 }.should raise_error(TypeError) + -> { a, (b, c), d = 1, x, 3, 4 }.should.raise(TypeError) end it "calls #to_a to convert a splatted Object value in a MRHS" do @@ -675,7 +741,7 @@ describe "Multiple assignment" do x = mock("multi-assign mixed splatted RHS") x.should_receive(:to_ary).and_return(x) - -> { a, *b, (c, d) = 1, 2, 3, *x }.should raise_error(TypeError) + -> { a, *b, (c, d) = 1, 2, 3, *x }.should.raise(TypeError) end it "does not call #to_ary to convert an Object when the position receiving the value is a simple variable" do @@ -715,12 +781,27 @@ 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 MRHS_VALUES_1.should == 1 MRHS_VALUES_2.should == 2 end + ensure + VariableSpecs.send(:remove_const, :MRHS_VALUES_1) + VariableSpecs.send(:remove_const, :MRHS_VALUES_2) end it "assigns all RHS values as an array to a single LHS constant" do @@ -728,6 +809,8 @@ describe "Multiple assignment" do MRHS_VALUES = 1, 2, 3 MRHS_VALUES.should == [1, 2, 3] end + ensure + VariableSpecs.send(:remove_const, :MRHS_VALUES) end end @@ -770,47 +853,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 +881,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(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/while_spec.rb b/spec/ruby/language/while_spec.rb index e172453ca6..b9bf32047d 100644 --- a/spec/ruby/language/while_spec.rb +++ b/spec/ruby/language/while_spec.rb @@ -88,7 +88,7 @@ describe "The while expression" do break if c c = false ) - end.should be_nil + end.should == nil end it "stops running body if interrupted by break in a begin ... end element op-assign-or value" do @@ -99,7 +99,7 @@ describe "The while expression" do break if c c = false end - end.should be_nil + end.should == nil end it "stops running body if interrupted by break in a parenthesized element op-assign value" do @@ -111,7 +111,7 @@ describe "The while expression" do break if c c = false ) - end.should be_nil + end.should == nil a.should == [1, 2] end @@ -123,7 +123,7 @@ describe "The while expression" do break if c c = false end - end.should be_nil + end.should == nil a.should == [1, 2] end @@ -139,7 +139,7 @@ describe "The while expression" do break unless d d = false ) - end.should be_nil + end.should == nil end it "stops running body if interrupted by break with unless in a begin ... end attribute op-assign-or value" do @@ -153,7 +153,7 @@ describe "The while expression" do break unless d d = false end - end.should be_nil + end.should == nil end it "stops running body if interrupted by break in a parenthesized attribute op-assign-or value" do @@ -168,7 +168,7 @@ describe "The while expression" do break if c c = false ) - end.should be_nil + end.should == nil end it "stops running body if interrupted by break in a begin ... end attribute op-assign-or value" do @@ -182,7 +182,7 @@ describe "The while expression" do break if c c = false end - end.should be_nil + end.should == nil end it "returns value passed to break if interrupted by break" do diff --git a/spec/ruby/language/yield_spec.rb b/spec/ruby/language/yield_spec.rb index 5fad7cb176..3173f41b0c 100644 --- a/spec/ruby/language/yield_spec.rb +++ b/spec/ruby/language/yield_spec.rb @@ -13,7 +13,7 @@ describe "The yield call" do describe "taking no arguments" do it "raises a LocalJumpError when the method is not passed a block" do - -> { @y.z }.should raise_error(LocalJumpError) + -> { @y.z }.should.raise(LocalJumpError) end it "ignores assignment to the explicit block argument and calls the passed block" do @@ -28,7 +28,7 @@ describe "The yield call" do describe "taking a single argument" do describe "when no block is given" do it "raises a LocalJumpError" do - -> { @y.s(1) }.should raise_error(LocalJumpError) + -> { @y.s(1) }.should.raise(LocalJumpError) end end @@ -48,6 +48,12 @@ describe "The yield call" do it "passes a single, multi-value Array" do @y.s([1, 2, 3]) { |*a| a }.should == [[1, 2, 3]] end + + describe "with optional argument" do + it "does not destructure a single array argument" do + @y.s([1, 2, 3]) { |a = 99| a }.should == [1, 2, 3] + end + end end describe "yielding to a lambda" do @@ -70,20 +76,20 @@ describe "The yield call" do it "raises an ArgumentError if too few arguments are passed" do -> { @y.s(1, &-> a, b { [a,b] }) - }.should raise_error(ArgumentError) + }.should.raise(ArgumentError) end it "should not destructure an Array into multiple arguments" do -> { @y.s([1, 2], &-> a, b { [a,b] }) - }.should raise_error(ArgumentError) + }.should.raise(ArgumentError) end end end describe "taking multiple arguments" do it "raises a LocalJumpError when the method is not passed a block" do - -> { @y.m(1, 2, 3) }.should raise_error(LocalJumpError) + -> { @y.m(1, 2, 3) }.should.raise(LocalJumpError) end it "passes the arguments to the block" do @@ -97,19 +103,19 @@ describe "The yield call" do it "raises an ArgumentError if too many arguments are passed to a lambda" do -> { @y.m(1, 2, 3, &-> a { }) - }.should raise_error(ArgumentError) + }.should.raise(ArgumentError) end it "raises an ArgumentError if too few arguments are passed to a lambda" do -> { @y.m(1, 2, 3, &-> a, b, c, d { }) - }.should raise_error(ArgumentError) + }.should.raise(ArgumentError) end end describe "taking a single splatted argument" do it "raises a LocalJumpError when the method is not passed a block" do - -> { @y.r(0) }.should raise_error(LocalJumpError) + -> { @y.r(0) }.should.raise(LocalJumpError) end it "passes a single value" do @@ -141,7 +147,7 @@ describe "The yield call" do describe "taking multiple arguments with a splat" do it "raises a LocalJumpError when the method is not passed a block" do - -> { @y.rs(1, 2, [3, 4]) }.should raise_error(LocalJumpError) + -> { @y.rs(1, 2, [3, 4]) }.should.raise(LocalJumpError) end it "passes the arguments to the block" do @@ -166,7 +172,7 @@ describe "The yield call" do describe "taking matching arguments with splats and post args" do it "raises a LocalJumpError when the method is not passed a block" do - -> { @y.rs(1, 2, [3, 4]) }.should raise_error(LocalJumpError) + -> { @y.rs(1, 2, [3, 4]) }.should.raise(LocalJumpError) end it "passes the arguments to the block" do @@ -183,5 +189,38 @@ 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(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(SyntaxError, /Invalid yield/) + end +end + +describe "Using yield in a module literal" do + it 'raises a SyntaxError' do + code = <<~RUBY + module YieldSpecs::ModuleWithYield + yield + end + RUBY + + -> { eval(code) }.should.raise(SyntaxError, /Invalid yield/) + end end |
