diff options
Diffstat (limited to 'spec/ruby/language')
136 files changed, 9693 insertions, 2128 deletions
diff --git a/spec/ruby/language/BEGIN_spec.rb b/spec/ruby/language/BEGIN_spec.rb index c0784971b0..5aef5a1d7c 100644 --- a/spec/ruby/language/BEGIN_spec.rb +++ b/spec/ruby/language/BEGIN_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "The BEGIN keyword" do before :each do @@ -15,7 +15,12 @@ describe "The BEGIN keyword" do end it "must appear in a top-level context" do - lambda { eval "1.times { BEGIN { 1 } }" }.should raise_error(SyntaxError) + -> { eval "1.times { BEGIN { 1 } }" }.should raise_error(SyntaxError) + end + + it "uses top-level for self" do + eval("BEGIN { ScratchPad << self.to_s }", TOPLEVEL_BINDING) + ScratchPad.recorded.should == ['main'] end it "runs first in a given code unit" do diff --git a/spec/ruby/language/END_spec.rb b/spec/ruby/language/END_spec.rb new file mode 100644 index 0000000000..c84f0cc9ac --- /dev/null +++ b/spec/ruby/language/END_spec.rb @@ -0,0 +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 "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 + + 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 e9f0050e17..61fddb0184 100644 --- a/spec/ruby/language/alias_spec.rb +++ b/spec/ruby/language/alias_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' class AliasObject attr :foo @@ -38,20 +38,29 @@ describe "The alias keyword" do @obj.a.should == 5 end - it "works with a doubule quoted symbol on the left-hand side" do + it "works with a double quoted symbol on the left-hand side" do @meta.class_eval do alias :"a" value end @obj.a.should == 5 end - it "works with an interoplated symbol on the left-hand side" do + it "works with an interpolated symbol on the left-hand side" do @meta.class_eval do alias :"#{'a'}" value end @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 @@ -66,20 +75,29 @@ describe "The alias keyword" do @obj.a.should == 5 end - it "works with a doubule quoted symbol on the right-hand side" do + it "works with a double quoted symbol on the right-hand side" do @meta.class_eval do alias a :"value" end @obj.a.should == 5 end - it "works with an interoplated symbol on the right-hand side" do + it "works with an interpolated symbol on the right-hand side" do @meta.class_eval do alias a :"#{'value'}" end @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 - lambda { AliasObject.new.__value }.should raise_error(NoMethodError) + -> { AliasObject.new.__value }.should raise_error(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 - lambda { Object.__klass_method }.should raise_error(NoMethodError) + -> { Object.__klass_method }.should raise_error(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 - lambda { Object.__klass_method2 }.should raise_error(NoMethodError) + -> { Object.__klass_method2 }.should raise_error(NoMethodError) end it "operates on methods defined via attr, attr_reader, and attr_accessor" do @@ -204,7 +222,7 @@ describe "The alias keyword" do end it "operates on methods with splat arguments defined in a superclass using text block for class eval" do - class Sub < AliasObject;end + subclass = Class.new(AliasObject) AliasObject.class_eval <<-code def test(*args) 4 @@ -215,17 +233,17 @@ describe "The alias keyword" do alias test_without_check test alias test test_with_check code - Sub.new.test("testing").should == 4 + subclass.new.test("testing").should == 4 end - it "is not allowed against Fixnum or String instances" do - lambda 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) - lambda do + -> do :blah.instance_eval do alias :foo :to_s end @@ -234,13 +252,43 @@ describe "The alias keyword" do it "on top level defines the alias on Object" do # because it defines on the default definee / current module - ruby_exe("def foo; end; alias bla foo; print method(:bla).owner", escape: true).should == "Object" + ruby_exe("def foo; end; alias bla foo; print method(:bla).owner").should == "Object" end it "raises a NameError when passed a missing name" do - lambda { @meta.class_eval { alias undef_method not_exist } }.should raise_error(NameError) { |e| + -> { @meta.class_eval { alias undef_method not_exist } }.should raise_error(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 + it "can create a new global variable, synonym of the original" do + code = '$a = 1; alias $b $a; p [$a, $b]; $b = 2; p [$a, $b]' + ruby_exe(code).should == "[1, 1]\n[2, 2]\n" + end + + it "can override an existing global variable and make them synonyms" do + code = '$a = 1; $b = 2; alias $b $a; p [$a, $b]; $b = 3; p [$a, $b]' + ruby_exe(code).should == "[1, 1]\n[3, 3]\n" + end + + it "supports aliasing twice the same global variables" do + code = '$a = 1; alias $b $a; alias $b $a; p [$a, $b]' + ruby_exe(code).should == "[1, 1]\n" + end end diff --git a/spec/ruby/language/and_spec.rb b/spec/ruby/language/and_spec.rb index e084fd3cef..55a2a3103a 100644 --- a/spec/ruby/language/and_spec.rb +++ b/spec/ruby/language/and_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "The '&&' statement" do diff --git a/spec/ruby/language/array_spec.rb b/spec/ruby/language/array_spec.rb index c3ed8c14c5..2583cffbf7 100644 --- a/spec/ruby/language/array_spec.rb +++ b/spec/ruby/language/array_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/array', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/array' describe "Array literals" do it "[] should return a new array populated with the given elements" do @@ -36,6 +36,13 @@ describe "Array literals" do [1, *nil, 3].should == [1, 3] [*nil, *nil, *nil].should == [] end + + it "evaluates each argument exactly once" do + se = ArraySpec::SideEffect.new + se.array_result(true) + se.array_result(false) + se.call_count.should == 4 + end end describe "Bareword array literal" do diff --git a/spec/ruby/language/assignments_spec.rb b/spec/ruby/language/assignments_spec.rb new file mode 100644 index 0000000000..c4adf73c1c --- /dev/null +++ b/spec/ruby/language/assignments_spec.rb @@ -0,0 +1,590 @@ +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_error(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_error(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_error(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_error(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.3" 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}, 105], {}] + end + end + + ruby_version_is "3.3"..."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_error(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 733e90211c..cc003b8946 100644 --- a/spec/ruby/language/block_spec.rb +++ b/spec/ruby/language/block_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/block', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/block' describe "A block yielded a single" do before :all do @@ -36,89 +36,102 @@ describe "A block yielded a single" do m([1, 2]) { |a=5, b=4, c=3| [a, b, c] }.should == [1, 2, 3] end - it "assgins elements to post arguments" do + it "assigns elements to post arguments" 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 - 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, 2, {x: 9}] + it "assigns elements to pre and post arguments" do + m([1 ]) { |a, b=5, c=6, d, e| [a, b, c, d, e] }.should == [1, 5, 6, nil, nil] + m([1, 2 ]) { |a, b=5, c=6, d, e| [a, b, c, d, e] }.should == [1, 5, 6, 2, nil] + m([1, 2, 3 ]) { |a, b=5, c=6, d, e| [a, b, c, d, e] }.should == [1, 5, 6, 2, 3] + m([1, 2, 3, 4 ]) { |a, b=5, c=6, d, e| [a, b, c, d, e] }.should == [1, 2, 6, 3, 4] + m([1, 2, 3, 4, 5 ]) { |a, b=5, c=6, d, e| [a, b, c, d, e] }.should == [1, 2, 3, 4, 5] + m([1, 2, 3, 4, 5, 6]) { |a, b=5, c=6, d, e| [a, b, c, d, e] }.should == [1, 2, 3, 4, 5] end - it "assigns symbol keys from a Hash to keyword arguments" do - result = m(["a" => 1, a: 10]) { |a=nil, **b| [a, b] } - result.should == [{"a" => 1}, a: 10] + 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 - it "assigns symbol keys from a Hash returned by #to_hash to keyword arguments" 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 a keyword rest argument is present" do + m([1, 2]) { |a, **k| [a, k] }.should == [[1, 2], {}] + end - result = m([obj]) { |a=nil, **b| [a, b] } - result.should == [{"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 + + it "raises error when required keyword arguments are present" do + -> { + m([1, 2]) { |a, b:, c:| [a, b, c] } + }.should raise_error(ArgumentError, "missing keywords: :b, :c") + end + + it "assigns elements to mixed argument types" do + result = m([1, 2, 3, {x: 9}]) { |a, b=5, *c, d, e: 2, **k| [a, b, c, d, e, k] } + result.should == [1, 2, [3], {x: 9}, 2, {}] end - ruby_version_is "2.2.1" do # SEGV on MRI 2.2.0 - it "calls #to_hash on the argument but does not use the result when no keywords are present" do + 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_receive(:to_hash).and_return({"a" => 1, "b" => 2}) + obj.should_not_receive(:to_hash) result = m([obj]) { |a=nil, **b| [a, b] } - result.should == [{"a" => 1, "b" => 2}, {}] + result.should == [[obj], {}] end end - it "assigns non-symbol keys to non-keyword arguments" do - result = m(["a" => 10, b: 2]) { |a=nil, **b| [a, b] } - result.should == [{"a" => 10}, {b: 2}] - 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) - 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}, {}] + result = m([obj]) { |a=nil, **b| [a, b] } + result.should == [[obj], {}] end - it "calls #to_hash on the last element if keyword arguments are present" do - obj = mock("destructure block keyword arguments") - obj.should_receive(:to_hash).and_return({x: 9}) + 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 + result = m(["a" => 10, b: 2]) { |a=nil, **b| [a, b] } + result.should == [[{"a" => 10, b: 2}], {}] + end + end + end - result = m([1, 2, 3, obj]) { |a, *b, c, **k| [a, b, c, k] } - result.should == [1, [2], 3, {x: 9}] + 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 - it "assigns the last element to a non-keyword argument if #to_hash returns nil" 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_receive(:to_hash).and_return(nil) + 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 - it "calls #to_hash on the last element when there are more arguments than parameters" do + 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_receive(:to_hash).and_return({x: 9}) + 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, {x: 9}] - 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) - - lambda { 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) - - lambda { m([1, 2, 3, obj]) { |a, *b, c, **k| } }.should raise_error(error) + result.should == [1, 2, 3, {}] end it "does not call #to_ary on the Array" do @@ -165,11 +178,54 @@ describe "A block yielded a single" do m(obj) { |a, b, c| [a, b, c] }.should == [obj, nil, nil] end + it "receives the object if it does not respond to #to_ary" do + obj = Object.new + + m(obj) { |a, b, c| [a, b, c] }.should == [obj, nil, nil] + end + + it "calls #respond_to? to check if object has method #to_ary" do + obj = mock("destructure block arguments") + obj.should_receive(:respond_to?).with(:to_ary, true).and_return(true) + obj.should_receive(:to_ary).and_return([1, 2]) + + m(obj) { |a, b, c| [a, b, c] }.should == [1, 2, nil] + end + + it "receives the object if it does not respond to #respond_to?" do + obj = BasicObject.new + + m(obj) { |a, b, c| [a, b, c] }.should == [obj, nil, nil] + end + + it "calls #to_ary on the object when it is defined dynamically" do + obj = Object.new + def obj.method_missing(name, *args, &block) + if name == :to_ary + [1, 2] + else + super + end + end + def obj.respond_to_missing?(name, include_private) + name == :to_ary + end + + m(obj) { |a, b, c| [a, b, c] }.should == [1, 2, nil] + end + it "raises a TypeError if #to_ary does not return an Array" do obj = mock("destructure block arguments") obj.should_receive(:to_ary).and_return(1) - lambda { m(obj) { |a, b| } }.should raise_error(TypeError) + -> { m(obj) { |a, b| } }.should raise_error(TypeError) + end + + it "raises error transparently if #to_ary raises error on its own" do + obj = Object.new + def obj.to_ary; raise "Exception raised in #to_ary" end + + -> { m(obj) { |a, b| } }.should raise_error(RuntimeError, "Exception raised in #to_ary") end end end @@ -217,6 +273,10 @@ describe "A block" do it "does not raise an exception when values are yielded" do @y.s(0) { 1 }.should == 1 end + + it "may include a rescue clause" do + @y.z do raise ArgumentError; rescue ArgumentError; 7; end.should == 7 + end end describe "taking || arguments" do @@ -227,6 +287,10 @@ describe "A block" do it "does not raise an exception when values are yielded" do @y.s(0) { || 1 }.should == 1 end + + it "may include a rescue clause" do + @y.z do || raise ArgumentError; rescue ArgumentError; 7; end.should == 7 + end end describe "taking |a| arguments" do @@ -252,10 +316,14 @@ describe "A block" do it "does not destructure a single Array value" do @y.s([1, 2]) { |a| a }.should == [1, 2] end + + it "may include a rescue clause" do + @y.s(1) do |x| raise ArgumentError; rescue ArgumentError; 7; end.should == 7 + end end describe "taking |a, b| arguments" do - it "assgins nil to the arguments when no values are yielded" do + it "assigns nil to the arguments when no values are yielded" do @y.z { |a, b| [a, b] }.should == [nil, nil] end @@ -314,16 +382,15 @@ describe "A block" do obj = mock("block yield to_ary invalid") obj.should_receive(:to_ary).and_return(1) - lambda { @y.s(obj) { |a, b| } }.should raise_error(TypeError) + -> { @y.s(obj) { |a, b| } }.should raise_error(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) - lambda { @y.s(obj) { |a, b| } }.should raise_error(ZeroDivisionError) + -> { @y.s(obj) { |a, b| } }.should raise_error(ZeroDivisionError) end - end describe "taking |a, *b| arguments" do @@ -378,7 +445,7 @@ describe "A block" do obj = mock("block yield to_ary invalid") obj.should_receive(:to_ary).and_return(1) - lambda { @y.s(obj) { |a, *b| } }.should raise_error(TypeError) + -> { @y.s(obj) { |a, *b| } }.should raise_error(TypeError) end end @@ -459,7 +526,7 @@ describe "A block" do @y.z { |a, | a }.should be_nil end - it "assgins the argument a single value yielded" do + it "assigns the argument a single value yielded" do @y.s(1) { |a, | a }.should == 1 end @@ -503,7 +570,7 @@ describe "A block" do obj = mock("block yield to_ary invalid") obj.should_receive(:to_ary).and_return(1) - lambda { @y.s(obj) { |a, | } }.should raise_error(TypeError) + -> { @y.s(obj) { |a, | } }.should raise_error(TypeError) end end @@ -545,7 +612,7 @@ describe "A block" do obj = mock("block yield to_ary invalid") obj.should_receive(:to_ary).and_return(1) - lambda { @y.s(obj) { |(a, b)| } }.should raise_error(TypeError) + -> { @y.s(obj) { |(a, b)| } }.should raise_error(TypeError) end end @@ -586,7 +653,7 @@ describe "A block" do obj = mock("block yield to_ary invalid") obj.should_receive(:to_ary).and_return(1) - lambda { @y.s(obj) { |(a, b), c| } }.should raise_error(TypeError) + -> { @y.s(obj) { |(a, b), c| } }.should raise_error(TypeError) end end @@ -626,6 +693,12 @@ describe "A block" do end end + describe "taking |*a, b:|" do + it "merges the hash into the splatted array" do + @y.k { |*a, b:| [a, b] }.should == [[], true] + end + end + describe "arguments with _" do it "extracts arguments with _" do @y.m([[1, 2, 3], 4]) { |(_, a, _), _| a }.should == 2 @@ -639,15 +712,51 @@ describe "A block" do describe "taking identically-named arguments" do it "raises a SyntaxError for standard arguments" do - lambda { eval "lambda { |x,x| }" }.should raise_error(SyntaxError) - lambda { eval "->(x,x) {}" }.should raise_error(SyntaxError) - lambda { eval "Proc.new { |x,x| }" }.should raise_error(SyntaxError) + -> { eval "lambda { |x,x| }" }.should raise_error(SyntaxError) + -> { eval "->(x,x) {}" }.should raise_error(SyntaxError) + -> { eval "Proc.new { |x,x| }" }.should raise_error(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 be_an_instance_of(Proc) # rubocop:disable Style/Lambda + -> _,_ {}.should be_an_instance_of(Proc) + Proc.new { |_,_| }.should be_an_instance_of(Proc) + end + end + + describe 'pre and post parameters' do + it "assigns nil to unassigned required arguments" do + proc { |a, *b, c, d| [a, b, c, d] }.call(1, 2).should == [1, [], 2, nil] + end + + it "assigns elements to optional arguments" do + proc { |a=5, b=4, c=3| [a, b, c] }.call(1, 2).should == [1, 2, 3] + end + + it "assigns elements to post arguments" do + proc { |a=5, b, c, d| [a, b, c, d] }.call(1, 2).should == [5, 1, 2, nil] + end + + it "assigns elements to pre arguments" do + proc { |a, b, c, d=5| [a, b, c, d] }.call(1, 2).should == [1, 2, nil, 5] + end + + it "assigns elements to pre and post arguments" do + proc { |a, b=5, c=6, d, e| [a, b, c, d, e] }.call(1 ).should == [1, 5, 6, nil, nil] + proc { |a, b=5, c=6, d, e| [a, b, c, d, e] }.call(1, 2 ).should == [1, 5, 6, 2, nil] + proc { |a, b=5, c=6, d, e| [a, b, c, d, e] }.call(1, 2, 3 ).should == [1, 5, 6, 2, 3] + proc { |a, b=5, c=6, d, e| [a, b, c, d, e] }.call(1, 2, 3, 4 ).should == [1, 2, 6, 3, 4] + proc { |a, b=5, c=6, d, e| [a, b, c, d, e] }.call(1, 2, 3, 4, 5 ).should == [1, 2, 3, 4, 5] + proc { |a, b=5, c=6, d, e| [a, b, c, d, e] }.call(1, 2, 3, 4, 5, 6).should == [1, 2, 3, 4, 5] + end + + it "assigns elements to pre and post arguments when *rest is present" do + proc { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.call(1 ).should == [1, 5, 6, [], nil, nil] + proc { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.call(1, 2 ).should == [1, 5, 6, [], 2, nil] + proc { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.call(1, 2, 3 ).should == [1, 5, 6, [], 2, 3] + proc { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.call(1, 2, 3, 4 ).should == [1, 2, 6, [], 3, 4] + proc { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.call(1, 2, 3, 4, 5 ).should == [1, 2, 3, [], 4, 5] + proc { |a, b=5, c=6, *d, e, f| [a, b, c, d, e, f] }.call(1, 2, 3, 4, 5, 6).should == [1, 2, 3, [4], 5, 6] end end end @@ -662,27 +771,27 @@ describe "Block-local variables" do end it "can not have the same name as one of the standard parameters" do - lambda { eval "[1].each {|foo; foo| }" }.should raise_error(SyntaxError) - lambda { eval "[1].each {|foo, bar; glark, bar| }" }.should raise_error(SyntaxError) + -> { eval "[1].each {|foo; foo| }" }.should raise_error(SyntaxError) + -> { eval "[1].each {|foo, bar; glark, bar| }" }.should raise_error(SyntaxError) end it "can not be prefixed with an asterisk" do - lambda { eval "[1].each {|foo; *bar| }" }.should raise_error(SyntaxError) - lambda do + -> { eval "[1].each {|foo; *bar| }" }.should raise_error(SyntaxError) + -> do eval "[1].each {|foo, bar; glark, *fnord| }" end.should raise_error(SyntaxError) end it "can not be prefixed with an ampersand" do - lambda { eval "[1].each {|foo; &bar| }" }.should raise_error(SyntaxError) - lambda do + -> { eval "[1].each {|foo; &bar| }" }.should raise_error(SyntaxError) + -> do eval "[1].each {|foo, bar; glark, &fnord| }" end.should raise_error(SyntaxError) end it "can not be assigned default values" do - lambda { eval "[1].each {|foo; bar=1| }" }.should raise_error(SyntaxError) - lambda do + -> { eval "[1].each {|foo; bar=1| }" }.should raise_error(SyntaxError) + -> do eval "[1].each {|foo, bar; glark, fnord=:fnord| }" end.should raise_error(SyntaxError) end @@ -693,8 +802,8 @@ describe "Block-local variables" do end it "only allow a single semi-colon in the parameter list" do - lambda { eval "[1].each {|foo; bar; glark| }" }.should raise_error(SyntaxError) - lambda { eval "[1].each {|; bar; glark| }" }.should raise_error(SyntaxError) + -> { eval "[1].each {|foo; bar; glark| }" }.should raise_error(SyntaxError) + -> { eval "[1].each {|; bar; glark| }" }.should raise_error(SyntaxError) end it "override shadowed variables from the outer scope" do @@ -756,14 +865,20 @@ describe "Post-args" do end.call(1, 2, 3).should == [[], 1, 2, 3] end - it "are required" do - lambda { - lambda do |*a, b| + it "are required for a lambda" do + -> { + -> *a, b do [a, b] end.call }.should raise_error(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 it "gathers remaining args in the splat" do @@ -826,26 +941,27 @@ describe "Post-args" do end describe "with a circular argument reference" do - it "shadows an existing local with the same name as the argument" do - a = 1 - -> { - @proc = eval "proc { |a=a| a }" - }.should complain(/circular argument reference/) - @proc.call.should == nil + ruby_version_is ""..."3.4" do + it "raises a SyntaxError if using the argument in its default value" do + a = 1 + -> { + eval "proc { |a=a| a }" + }.should raise_error(SyntaxError) + end end - it "shadows an existing method with the same name as the argument" do - def a; 1; end - -> { - @proc = eval "proc { |a=a| a }" - }.should complain(/circular argument reference/) - @proc.call.should == nil + ruby_version_is "3.4" do + it "is nil if using the argument in its default value" do + -> { + 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 @@ -863,3 +979,160 @@ 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_error(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 with no ordinary parameters" do + ruby_version_is "3.3"..."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 "3.4" do + it "does not emit a deprecation warning" do + -> { + eval "proc { it }" + }.should_not complain + end + + it "acts as the first argument if no local variables exist" do + eval("proc { it * 2 }").call(5).should == 10 + end + + it "can be reassigned to act as a local variable" do + eval("proc { tmp = it; it = tmp * 2; it }").call(21).should == 42 + end + + it "can be used in nested calls" do + eval("proc { it.map { it * 2 } }").call([1, 2, 3]).should == [2, 4, 6] + end + + it "cannot be mixed with numbered parameters" do + -> { + eval "proc { it + _1 }" + }.should raise_error(SyntaxError, /numbered parameters are not allowed when 'it' is already used|'it' is already used in/) + + -> { + eval "proc { _1 + it }" + }.should raise_error(SyntaxError, /numbered parameter is already used in|'it' is not allowed when a numbered parameter is already used/) + end + end +end + +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 diff --git a/spec/ruby/language/break_spec.rb b/spec/ruby/language/break_spec.rb index 09e8ff3d93..7e5b6fb328 100644 --- a/spec/ruby/language/break_spec.rb +++ b/spec/ruby/language/break_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/break', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/break' describe "The break statement in a block" do before :each do @@ -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 - lambda { @program.break_in_method }.should raise_error(LocalJumpError) + -> { @program.break_in_method }.should raise_error(LocalJumpError) ScratchPad.recorded.should == [:a, :xa, :d, :b] end it "raises a LocalJumpError when invoking the block from a method" do - lambda { @program.break_in_nested_method }.should raise_error(LocalJumpError) + -> { @program.break_in_nested_method }.should raise_error(LocalJumpError) ScratchPad.recorded.should == [:a, :xa, :cc, :aa, :b] end it "raises a LocalJumpError when yielding to the block" do - lambda { @program.break_in_yielding_method }.should raise_error(LocalJumpError) + -> { @program.break_in_yielding_method }.should raise_error(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 - lambda { @program.break_in_method_captured }.should raise_error(LocalJumpError) + -> { @program.break_in_method_captured }.should raise_error(LocalJumpError) ScratchPad.recorded.should == [:a, :za, :xa, :zd, :zb] end it "raises a LocalJumpError when yielding to the block" do - lambda { @program.break_in_yield_captured }.should raise_error(LocalJumpError) + -> { @program.break_in_yield_captured }.should raise_error(LocalJumpError) ScratchPad.recorded.should == [:a, :za, :xa, :zd, :aa, :zb] end end @@ -100,7 +100,7 @@ describe "The break statement in a lambda" do end it "returns from the lambda" do - l = lambda { + l = -> { ScratchPad << :before break :foo ScratchPad << :after @@ -111,7 +111,7 @@ describe "The break statement in a lambda" do it "returns from the call site if the lambda is passed as a block" do def mid(&b) - lambda { + -> { ScratchPad << :before b.call ScratchPad << :unreachable1 @@ -208,7 +208,7 @@ describe "Break inside a while loop" do it "passes the value returned by a method with omitted parenthesis and passed block" do obj = BreakSpecs::Block.new - lambda { break obj.method :value do |x| x end }.call.should == :value + -> { break obj.method :value do |x| x end }.call.should == :value 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_error(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_error(SyntaxError) + end +end # TODO: Rewrite all the specs from here to the end of the file in the style # above. @@ -362,4 +381,22 @@ describe "Executing break from within a block" do bt2.three ScratchPad.recorded.should == [:two_ensure, :three_post, :three_ensure] end + + it "works when passing through a super call" do + cls1 = Class.new { def foo; yield; end } + cls2 = Class.new(cls1) { def foo; super { break 1 }; end } + + -> do + cls2.new.foo.should == 1 + end.should_not raise_error + end + + 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 end diff --git a/spec/ruby/language/case_spec.rb b/spec/ruby/language/case_spec.rb index 25f5d0efc4..464d06e46a 100644 --- a/spec/ruby/language/case_spec.rb +++ b/spec/ruby/language/case_spec.rb @@ -1,17 +1,17 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "The 'case'-construct" do it "evaluates the body of the when clause matching the case target expression" do case 1 - when 2; false - when 1; true + when 2; false + when 1; true end.should == true end it "evaluates the body of the when clause whose array expression includes the case target expression" do case 2 - when 3, 4; false - when 1, 2; true + when 3, 4; false + when 1, 2; true end.should == true end @@ -21,7 +21,7 @@ describe "The 'case'-construct" do def bar; @calls << :bar; end case true - when foo, bar; + when foo, bar; end @calls.should == [:foo, :bar] @@ -29,31 +29,31 @@ describe "The 'case'-construct" do it "evaluates the body of the when clause whose range expression includes the case target expression" do case 5 - when 21..30; false - when 1..20; true + when 21..30; false + when 1..20; true end.should == true end it "returns nil when no 'then'-bodies are given" do case "a" - when "a" - when "b" + when "a" + when "b" end.should == nil end it "evaluates the 'else'-body when no other expression matches" do case "c" - when "a"; 'foo' - when "b"; 'bar' - else 'zzz' + when "a"; 'foo' + when "b"; 'bar' + else 'zzz' end.should == 'zzz' end it "returns nil when no expression matches and 'else'-body is empty" do case "c" - when "a"; "a" - when "b"; "b" - else + when "a"; "a" + when "b"; "b" + else end.should == nil end @@ -70,105 +70,152 @@ describe "The 'case'-construct" do it "returns the statement following 'then'" do case "a" - when "a" then 'foo' - when "b" then 'bar' + when "a" then 'foo' + when "b" then 'bar' end.should == 'foo' end it "tests classes with case equality" do case "a" - when String - 'foo' - when Symbol - 'bar' + when String + 'foo' + when Symbol + 'bar' end.should == 'foo' end it "tests with matching regexps" do case "hello" - when /abc/; false - when /^hell/; true + when /abc/; false + when /^hell/; true end.should == true end + it "tests with matching regexps and sets $~ and captures" do + case "foo42" + when /oo(\d+)/ + $~.should be_kind_of(MatchData) + $1.should == "42" + else + flunk + end + $~.should be_kind_of(MatchData) + $1.should == "42" + end + + it "tests with a string interpolated in a regexp" do + digits = '\d+' + case "foo44" + when /oo(#{digits})/ + $~.should be_kind_of(MatchData) + $1.should == "44" + else + flunk + end + $~.should be_kind_of(MatchData) + $1.should == "44" + end + + it "tests with a regexp interpolated within another regexp" do + digits_regexp = /\d+/ + case "foo43" + when /oo(#{digits_regexp})/ + $~.should be_kind_of(MatchData) + $1.should == "43" + else + flunk + end + $~.should be_kind_of(MatchData) + $1.should == "43" + end + it "does not test with equality when given classes" do case :symbol.class - when Symbol - "bar" - when String - "bar" - else - "foo" + when Symbol + "bar" + when String + "bar" + else + "foo" end.should == "foo" end it "takes lists of values" do case 'z' - when 'a', 'b', 'c', 'd' - "foo" - when 'x', 'y', 'z' - "bar" + when 'a', 'b', 'c', 'd' + "foo" + when 'x', 'y', 'z' + "bar" end.should == "bar" case 'b' - when 'a', 'b', 'c', 'd' - "foo" - when 'x', 'y', 'z' - "bar" + when 'a', 'b', 'c', 'd' + "foo" + when 'x', 'y', 'z' + "bar" 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'] - "foo" - when *['x', 'y', 'z'] - "bar" + when *['a', 'b', 'c', 'd'] + "foo" + when *['x', 'y', 'z'] + "bar" end.should == "bar" end it "takes an expanded array in addition to a list of values" do case 'f' - when 'f', *['a', 'b', 'c', 'd'] - "foo" - when *['x', 'y', 'z'] - "bar" + when 'f', *['a', 'b', 'c', 'd'] + "foo" + when *['x', 'y', 'z'] + "bar" end.should == "foo" case 'b' - when 'f', *['a', 'b', 'c', 'd'] - "foo" - when *['x', 'y', 'z'] - "bar" + when 'f', *['a', 'b', 'c', 'd'] + "foo" + when *['x', 'y', 'z'] + "bar" end.should == "foo" end it "takes an expanded array before additional listed values" do case 'f' - when *['a', 'b', 'c', 'd'], 'f' - "foo" - when *['x', 'y', 'z'] - "bar" + when *['a', 'b', 'c', 'd'], 'f' + "foo" + when *['x', 'y', 'z'] + "bar" end.should == 'foo' end it "expands arrays from variables before additional listed values" do a = ['a', 'b', 'c'] case 'a' - when *a, 'd', 'e' - "foo" - when 'x' - "bar" + when *a, 'd', 'e' + "foo" + when 'x' + "bar" end.should == "foo" end it "expands arrays from variables before a single additional listed value" do a = ['a', 'b', 'c'] case 'a' - when *a, 'd' - "foo" - when 'x' - "bar" + when *a, 'd' + "foo" + when 'x' + "bar" end.should == "foo" end @@ -177,10 +224,10 @@ describe "The 'case'-construct" do b = ['d', 'e', 'f'] case 'f' - when *a, *b, 'g', 'h' - "foo" - when 'x' - "bar" + when *a, *b, 'g', 'h' + "foo" + when 'x' + "bar" end.should == "foo" end @@ -190,47 +237,47 @@ describe "The 'case'-construct" do b = ['f'] case 'f' - when 'f', *a|b - "foo" - when *['x', 'y', 'z'] - "bar" + when 'f', *a|b + "foo" + when *['x', 'y', 'z'] + "bar" end.should == "foo" end it "never matches when clauses with no values" do case nil - when *[] - "foo" + when *[] + "foo" end.should == nil end it "lets you define a method after the case statement" do case (def foo; 'foo'; end; 'f') - when 'a' - 'foo' - when 'f' - 'bar' + when 'a' + 'foo' + when 'f' + 'bar' end.should == 'bar' end it "raises a SyntaxError when 'else' is used when no 'when' is given" do - lambda { + -> { eval <<-CODE case 4 - else - true + else + true end CODE }.should raise_error(SyntaxError) end it "raises a SyntaxError when 'else' is used before a 'when' was given" do - lambda { + -> { eval <<-CODE case 4 - else - true - when 4; false + else + true + when 4; false end CODE }.should raise_error(SyntaxError) @@ -282,61 +329,18 @@ 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 - when true; "bad" - when Integer; "good" + when true; "bad" + when Integer; "good" end.should == "good" end it "evaluates false as only 'false' when false is the first clause" do case nil - when false; "bad" - when nil; "good" + when false; "bad" + when nil; "good" end.should == "good" end @@ -352,17 +356,17 @@ describe "The 'case'-construct with no target expression" do a2 = ['b', 'a', 'r'] case 'f' - when *a1, *['x', 'y', 'z'] - "foo" - when *a2, *['x', 'y', 'z'] - "bar" + when *a1, *['x', 'y', 'z'] + "foo" + when *a2, *['x', 'y', 'z'] + "bar" end.should == "foo" case 'b' - when *a1, *['x', 'y', 'z'] - "foo" - when *a2, *['x', 'y', 'z'] - "bar" + when *a1, *['x', 'y', 'z'] + "foo" + when *a2, *['x', 'y', 'z'] + "bar" end.should == "bar" end @@ -386,4 +390,120 @@ describe "The 'case'-construct with no target expression" do :called 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 + case + when 1; 'foo' + 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 ba4af3d880..6fb785fd56 100644 --- a/spec/ruby/language/class_spec.rb +++ b/spec/ruby/language/class_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/class', __FILE__) +require_relative '../spec_helper' +require_relative '../fixtures/class' ClassSpecsNumber = 12 @@ -13,11 +13,22 @@ describe "The class keyword" do ClassSpecsKeywordWithSemicolon.should be_an_instance_of(Class) end - ruby_version_is "2.3" do - 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) - 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) + end + + it "can redefine a class when called from a block" do + ClassSpecs::DEFINE_CLASS.call + A.should be_an_instance_of(Class) + + Object.send(:remove_const, :A) + defined?(A).should be_nil + + ClassSpecs::DEFINE_CLASS.call + A.should be_an_instance_of(Class) + ensure + Object.send(:remove_const, :A) if defined?(::A) end end @@ -32,27 +43,34 @@ describe "A class definition" do end it "raises TypeError if constant given as class name exists and is not a Module" do - lambda { + -> { class ClassSpecsNumber end - }.should raise_error(TypeError) + }.should raise_error(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_error(TypeError, /\AClassSpecs is not a class/) end # test case known to be detecting bugs (JRuby, MRI) it "raises TypeError if the constant qualifying the class is nil" do - lambda { + -> { class nil::Foo end }.should raise_error(TypeError) end it "raises TypeError if any constant qualifying the class is not a Module" do - lambda { + -> { class ClassSpecs::Number::MyClass end }.should raise_error(TypeError) - lambda { + -> { class ClassSpecsNumber::MyClass end }.should raise_error(TypeError) @@ -66,7 +84,7 @@ describe "A class definition" do module ClassSpecs class SuperclassResetToSubclass < L end - lambda { + -> { class SuperclassResetToSubclass < M end }.should raise_error(TypeError, /superclass mismatch/) @@ -79,7 +97,7 @@ describe "A class definition" do end SuperclassReopenedBasicObject.superclass.should == A - lambda { + -> { class SuperclassReopenedBasicObject < BasicObject end }.should raise_error(TypeError, /superclass mismatch/) @@ -94,7 +112,7 @@ describe "A class definition" do end SuperclassReopenedObject.superclass.should == A - lambda { + -> { class SuperclassReopenedObject < Object end }.should raise_error(TypeError, /superclass mismatch/) @@ -119,7 +137,7 @@ describe "A class definition" do class NoSuperclassSet end - lambda { + -> { class NoSuperclassSet < String end }.should raise_error(TypeError, /superclass mismatch/) @@ -129,7 +147,7 @@ describe "A class definition" do it "allows using self as the superclass if self is a class" do ClassSpecs::I::J.superclass.should == ClassSpecs::I - lambda { + -> { class ShouldNotWork < self; end }.should raise_error(TypeError) end @@ -150,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 - lambda { class ClassSpecs::MetaclassSuper < meta; end }.should raise_error(TypeError) + -> { class ClassSpecs::MetaclassSuper < meta; end }.should raise_error(TypeError) end it "allows the declaration of class variables in the body" do @@ -212,16 +230,16 @@ describe "A class definition" do describe "within a block creates a new class in the lexical scope" do it "for named classes at the toplevel" do klass = Class.new do - class Howdy + class CS_CONST_CLASS_SPECS end def self.get_class_name - Howdy.name + CS_CONST_CLASS_SPECS.name end end - Howdy.name.should == 'Howdy' - klass.get_class_name.should == 'Howdy' + klass.get_class_name.should == 'CS_CONST_CLASS_SPECS' + ::CS_CONST_CLASS_SPECS.name.should == 'CS_CONST_CLASS_SPECS' end it "for named classes in a module" do @@ -260,6 +278,8 @@ describe "A class definition" do AnonWithConstant.name.should == 'AnonWithConstant' klass.get_class_name.should == 'AnonWithConstant' + ensure + Object.send(:remove_const, :AnonWithConstant) end end end @@ -276,7 +296,7 @@ describe "A class definition extending an object (sclass)" do end it "raises a TypeError when trying to extend numbers" do - lambda { + -> { eval <<-CODE class << 1 def xyz @@ -287,8 +307,20 @@ describe "A class definition extending an object (sclass)" do }.should raise_error(TypeError) end - it "allows accessing the block of the original scope" do - ClassSpecs.sclass_with_block { 123 }.should == 123 + 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 + + it "does not allow accessing the block of the original scope" do + -> { + ClassSpecs.sclass_with_block { 123 } + }.should raise_error(SyntaxError) end it "can use return to cause the enclosing method to return" do @@ -308,11 +340,11 @@ describe "Reopening a class" do end it "raises a TypeError when superclasses mismatch" do - lambda { class ClassSpecs::A < Array; end }.should raise_error(TypeError) + -> { class ClassSpecs::A < Array; end }.should raise_error(TypeError) end it "adds new methods to subclasses" do - lambda { ClassSpecs::M.m }.should raise_error(NoMethodError) + -> { ClassSpecs::M.m }.should raise_error(NoMethodError) class ClassSpecs::L def self.m 1 @@ -321,6 +353,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 463f731a93..a26a3fb8de 100644 --- a/spec/ruby/language/class_variable_spec.rb +++ b/spec/ruby/language/class_variable_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/class_variables', __FILE__) +require_relative '../spec_helper' +require_relative '../fixtures/class_variables' describe "A class variable" do after :each do @@ -70,15 +70,45 @@ describe 'A class variable definition' do c = Class.new(b) b.class_variable_set(:@@cv, :value) - lambda { a.class_variable_get(:@@cv) }.should raise_error(NameError) + -> { a.class_variable_get(:@@cv) }.should raise_error(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) - lambda { a.class_variable_get(:@@cv) }.should raise_error(NameError) + -> { a.class_variable_get(:@@cv) }.should raise_error(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_error(RuntimeError, 'class variable access from toplevel') + -> { + eval "@@cvar_toplevel2 = 2" + }.should raise_error(RuntimeError, 'class variable access from toplevel') + end + + it "does not raise an error when checking if defined from the toplevel scope" do + -> { + eval "defined?(@@cvar_toplevel1)" + }.should_not raise_error + end + + it "raises a RuntimeError when a class variable is overtaken in an ancestor class" do + parent = Class.new() + subclass = Class.new(parent) + subclass.class_variable_set(:@@cvar_overtaken, :subclass) + parent.class_variable_set(:@@cvar_overtaken, :parent) + + -> { + subclass.class_variable_get(:@@cvar_overtaken) + }.should raise_error(RuntimeError, /class variable @@cvar_overtaken of .+ is overtaken by .+/) + + parent.class_variable_get(:@@cvar_overtaken).should == :parent + end +end diff --git a/spec/ruby/language/comment_spec.rb b/spec/ruby/language/comment_spec.rb new file mode 100644 index 0000000000..dd788e681c --- /dev/null +++ b/spec/ruby/language/comment_spec.rb @@ -0,0 +1,13 @@ +require_relative '../spec_helper' + +describe "The comment" do + it "can be placed between fluent dot now" do + code = <<~CODE + 10 + # some comment + .to_s + CODE + + eval(code).should == '10' + end +end diff --git a/spec/ruby/language/constants_spec.rb b/spec/ruby/language/constants_spec.rb index 1f1e254fb8..063c52c422 100644 --- a/spec/ruby/language/constants_spec.rb +++ b/spec/ruby/language/constants_spec.rb @@ -1,7 +1,7 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/constants', __FILE__) -require File.expand_path('../fixtures/constants_sclass', __FILE__) -require File.expand_path('../fixtures/constant_visibility', __FILE__) +require_relative '../spec_helper' +require_relative '../fixtures/constants' +require_relative 'fixtures/constants_sclass' +require_relative 'fixtures/constant_visibility' # Read the documentation in fixtures/constants.rb for the guidelines and # rationale for the structure and organization of these specs. @@ -49,10 +49,10 @@ describe "Literal (A::X) constant resolution" do end it "does not search the singleton class of the class or module" do - lambda do + -> do ConstantSpecs::ContainerA::ChildA::CS_CONST14 end.should raise_error(NameError) - lambda { ConstantSpecs::CS_CONST14 }.should raise_error(NameError) + -> { ConstantSpecs::CS_CONST14 }.should raise_error(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 @@ -112,7 +133,7 @@ describe "Literal (A::X) constant resolution" do CS_CONST108 = :const108_1 end - lambda do + -> do ConstantSpecs::ContainerB::ChildB::CS_CONST108 end.should raise_error(NameError) @@ -122,7 +143,10 @@ describe "Literal (A::X) constant resolution" do end end - lambda { ConstantSpecs::CS_CONST108 }.should raise_error(NameError) + -> { ConstantSpecs::CS_CONST108 }.should raise_error(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,25 +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 - lambda { ConstantSpecs::ParentA::CS_CONSTX }.should raise_error(NameError) + -> { ConstantSpecs::ParentA::CS_CONSTX }.should raise_error(NameError) + end + + 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 + end + + -> { mod::DOES_NOT_EXIST }.should raise_error(NameError, /uninitialized constant ModuleName::DOES_NOT_EXIST/) + 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 + + def self.inspect + "<unusable info>" + end + end + + -> { mod::DOES_NOT_EXIST }.should raise_error(NameError, /uninitialized constant <unusable info>::DOES_NOT_EXIST/) end it "sends #const_missing to the original class or module scope" do @@ -163,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 - lambda { CS_CONST1::CS_CONST }.should raise_error(TypeError) - lambda { 1::CS_CONST }.should raise_error(TypeError) - lambda { "mod"::CS_CONST }.should raise_error(TypeError) - lambda { false::CS_CONST }.should raise_error(TypeError) + -> { 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) end end @@ -212,7 +264,7 @@ describe "Constant resolution within methods" do end it "does not search the lexical scope of the caller" do - lambda { ConstantSpecs::ClassA.const16 }.should raise_error(NameError) + -> { ConstantSpecs::ClassA.const16 }.should raise_error(NameError) end it "searches the lexical scope of a block" do @@ -225,7 +277,7 @@ describe "Constant resolution within methods" do end it "does not search the lexical scope of qualifying modules" do - lambda do + -> do ConstantSpecs::ContainerA::ChildA.const23 end.should raise_error(NameError) end @@ -248,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 @@ -256,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 @@ -264,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 @@ -272,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 @@ -279,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 @@ -290,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 @@ -297,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 - lambda { ConstantSpecs::ClassB.const209 }.should raise_error(NameError) + -> { ConstantSpecs::ClassB.const209 }.should raise_error(NameError) + ensure + ConstantSpecs::ClassB.send(:remove_const, :CS_CONST209) end it "searches the lexical scope of a block" do @@ -310,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 @@ -320,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 @@ -332,71 +417,29 @@ 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 ConstantSpecs::ContainerB::CS_CONST214 = :const214 - lambda do + -> do ConstantSpecs::ContainerB::ChildB.const214 end.should raise_error(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 - lambda { ConstantSpecs::ParentA.constx }.should raise_error(NameError) + -> { ConstantSpecs::ParentA.constx }.should raise_error(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 @@ -404,46 +447,34 @@ describe "Constant resolution within a singleton class (class << obj)" do ConstantSpecs::CS_SINGLETON1.foo.should == 1 end - ruby_version_is "2.3" do - it "uses its own namespace for each object" do - a = ConstantSpecs::CS_SINGLETON2[0].foo - b = ConstantSpecs::CS_SINGLETON2[1].foo - [a, b].should == [1, 2] - end + it "uses its own namespace for each object" do + a = ConstantSpecs::CS_SINGLETON2[0].foo + b = ConstantSpecs::CS_SINGLETON2[1].foo + [a, b].should == [1, 2] + end - 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) - end + 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) + end - it "allows nested modules to have proper resolution" do - a = ConstantSpecs::CS_SINGLETON4_CLASSES[0].new - b = ConstantSpecs::CS_SINGLETON4_CLASSES[1].new - [a.foo, b.foo].should == [1, 2] - end + it "allows nested modules to have proper resolution" do + a = ConstantSpecs::CS_SINGLETON4_CLASSES[0].new + b = ConstantSpecs::CS_SINGLETON4_CLASSES[1].new + [a.foo, b.foo].should == [1, 2] end end describe "top-level constant lookup" do context "on a class" do - ruby_version_is "" ... "2.5" do - it "searches Object successfully after searching other scopes" do - ->() { - String::Hash.should == Hash - }.should complain(/toplevel constant Hash referenced by/) - end - end - - ruby_version_is "2.5" do - it "does not search Object after searching other scopes" do - ->() { String::Hash }.should raise_error(NameError) - end + it "does not search Object after searching other scopes" do + -> { String::Hash }.should raise_error(NameError) end end it "searches Object unsuccessfully when searches on a module" do - ->() { Enumerable::Hash }.should raise_error(NameError) + -> { Enumerable::Hash }.should raise_error(NameError) end end @@ -457,24 +488,35 @@ describe "Module#private_constant marked constants" do mod.const_set :Foo, false }.should complain(/already initialized constant/) - lambda {mod::Foo}.should raise_error(NameError) + -> {mod::Foo}.should raise_error(NameError) + end + + 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 - lambda do + -> do ConstantVisibility::PrivConstModule::PRIVATE_CONSTANT_MODULE end.should raise_error(NameError) end it "cannot be reopened as a module from scope where constant would be private" do - lambda do + -> do module ConstantVisibility::ModuleContainer::PrivateModule; end end.should raise_error(NameError) end it "cannot be reopened as a class from scope where constant would be private" do - lambda do + -> do class ConstantVisibility::ModuleContainer::PrivateClass; end end.should raise_error(NameError) end @@ -487,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 @@ -497,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 @@ -520,29 +570,42 @@ 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 be_true + end + + it "can be accessed from modules that include the module" do + ConstantVisibility::ModuleIncludingPrivConstModule.private_constant_from_include.should be_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_error(NameError) + -> do + ConstantVisibility::ModuleIncludingPrivConstModule.private_constant_named_from_include + end.should raise_error(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 describe "in a class" do it "cannot be accessed from outside the class" do - lambda do + -> do ConstantVisibility::PrivConstClass::PRIVATE_CONSTANT_CLASS end.should raise_error(NameError) end it "cannot be reopened as a module" do - lambda do + -> do module ConstantVisibility::ClassContainer::PrivateModule; end end.should raise_error(NameError) end it "cannot be reopened as a class" do - lambda do + -> do class ConstantVisibility::ClassContainer::PrivateClass; end end.should raise_error(NameError) end @@ -555,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 @@ -565,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 @@ -598,7 +669,7 @@ describe "Module#private_constant marked constants" do describe "in Object" do it "cannot be accessed using ::Const form" do - lambda do + -> do ::PRIVATE_CONSTANT_IN_OBJECT end.should raise_error(NameError) end @@ -615,6 +686,40 @@ describe "Module#private_constant marked constants" do defined?(PRIVATE_CONSTANT_IN_OBJECT).should == "constant" end end + + describe "NameError by #private_constant" do + it "has :receiver and :name attributes" do + -> do + ConstantVisibility::PrivConstClass::PRIVATE_CONSTANT_CLASS + end.should raise_error(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| + e.receiver.should == ConstantVisibility::PrivConstModule + e.name.should == :PRIVATE_CONSTANT_MODULE + } + end + + it "has the defined class as the :name attribute" do + -> do + ConstantVisibility::PrivConstClassChild::PRIVATE_CONSTANT_CLASS + end.should raise_error(NameError) {|e| + e.receiver.should == ConstantVisibility::PrivConstClass + e.name.should == :PRIVATE_CONSTANT_CLASS + } + + -> do + ConstantVisibility::ClassIncludingPrivConstModule::PRIVATE_CONSTANT_MODULE + end.should raise_error(NameError) {|e| + e.receiver.should == ConstantVisibility::PrivConstModule + e.name.should == :PRIVATE_CONSTANT_MODULE + } + end + end end describe "Module#public_constant marked constants" do @@ -666,3 +771,39 @@ describe "Module#public_constant marked constants" do end end end + +describe 'Allowed characters' do + it 'allows not ASCII characters in the middle of a name' do + mod = Module.new + mod.const_set("BBἍBB", 1) + + eval("mod::BBἍBB").should == 1 + end + + 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 + + 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 + +describe 'Assignment' do + context 'dynamic assignment' do + it 'raises SyntaxError' do + -> do + eval <<-CODE + def test + B = 1 + end + CODE + end.should raise_error(SyntaxError, /dynamic constant assignment/) + end + end +end diff --git a/spec/ruby/language/def_spec.rb b/spec/ruby/language/def_spec.rb index 55ee283b90..0cf1790791 100644 --- a/spec/ruby/language/def_spec.rb +++ b/spec/ruby/language/def_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/def', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/def' # Language-level method behaviour describe "Redefining a method" do @@ -79,6 +79,40 @@ describe "Defining a method" do end 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)') + 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)') + end + + it "raises FrozenError with the correct class name" do + -> { + Module.new do + self.freeze + def foo; end + end + }.should raise_error(FrozenError) { |e| + msg_class = ruby_version_is("4.0") ? "Module" : "module" + e.message.should == "can't modify frozen #{msg_class}: #{e.receiver}" + } + + -> { + Class.new do + self.freeze + def foo; end + end + }.should raise_error(FrozenError){ |e| + msg_class = ruby_version_is("4.0") ? "Class" : "class" + e.message.should == "can't modify frozen #{msg_class}: #{e.receiver}" + } + end +end + describe "An instance method definition with a splat" do it "accepts an unnamed '*' argument" do def foo(*); end; @@ -101,12 +135,12 @@ describe "An instance method definition with a splat" do end it "allows only a single * argument" do - lambda { eval 'def foo(a, *b, *c); end' }.should raise_error(SyntaxError) + -> { eval 'def foo(a, *b, *c); end' }.should raise_error(SyntaxError) end it "requires the presence of any arguments that precede the *" do def foo(a, b, *c); end - lambda { foo 1 }.should raise_error(ArgumentError) + -> { foo 1 }.should raise_error(ArgumentError, 'wrong number of arguments (given 1, expected 2+)') end end @@ -139,7 +173,7 @@ describe "An instance method with a default argument" do def foo(a, b = 2) [a,b] end - lambda { foo }.should raise_error(ArgumentError) + -> { foo }.should raise_error(ArgumentError, 'wrong number of arguments (given 0, expected 1..2)') foo(1).should == [1, 2] end @@ -147,7 +181,7 @@ describe "An instance method with a default argument" do def foo(a, b = 2, *c) [a,b,c] end - lambda { foo }.should raise_error(ArgumentError) + -> { foo }.should raise_error(ArgumentError, 'wrong number of arguments (given 0, expected 1+)') foo(1).should == [1,2,[]] end @@ -165,17 +199,25 @@ describe "An instance method with a default argument" do foo(2,3,3).should == [2,3,[3]] end - it "shadows an existing method with the same name as the local" do - def bar - 1 + 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 raise_error(SyntaxError) + end + 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 + foo" + }.call.should == nil end - -> { - eval "def foo(bar = bar) - bar - end" - }.should complain(/circular argument reference/) - foo.should == nil - foo(2).should == 2 end it "calls a method with the same name as the local when explicitly using ()" do @@ -208,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 @@ -234,10 +276,30 @@ describe "A singleton method definition" do (obj==2).should == 2 end - it "raises RuntimeError if frozen" do + it "raises FrozenError if frozen" do + obj = Object.new + obj.freeze + -> { def obj.foo; end }.should raise_error(FrozenError) + end + + it "raises FrozenError with the correct class name" do obj = Object.new obj.freeze - lambda { def obj.foo; end }.should raise_error(RuntimeError) + msg_class = ruby_version_is("4.0") ? "Object" : "object" + -> { def obj.foo; end }.should raise_error(FrozenError, "can't modify frozen #{msg_class}: #{obj}") + + obj = Object.new + c = obj.singleton_class + c.singleton_class.freeze + -> { def c.foo; end }.should raise_error(FrozenError, "can't modify frozen Class: #{c}") + + c = Class.new + c.freeze + -> { def c.foo; end }.should raise_error(FrozenError, "can't modify frozen Class: #{c}") + + m = Module.new + m.freeze + -> { def m.foo; end }.should raise_error(FrozenError, "can't modify frozen Module: #{m}") end end @@ -310,7 +372,7 @@ describe "A method defined with extreme default arguments" do end it "may use a lambda as a default" do - def foo(output = 'a', prc = lambda {|n| output * n}) + def foo(output = 'a', prc = -> n { output * n }) prc.call(5) end foo.should == 'aaaaa' @@ -356,7 +418,7 @@ describe "A singleton method defined with extreme default arguments" do it "may use a lambda as a default" do a = Object.new - def a.foo(output = 'a', prc = lambda {|n| output * n}) + def a.foo(output = 'a', prc = -> n { output * n }) prc.call(5) end a.foo.should == 'aaaaa' @@ -372,7 +434,7 @@ describe "A method definition inside a metaclass scope" do end DefSpecSingleton.a_class_method.should == DefSpecSingleton - lambda { Object.a_class_method }.should raise_error(NoMethodError) + -> { Object.a_class_method }.should raise_error(NoMethodError) end it "can create a singleton method" do @@ -382,15 +444,15 @@ describe "A method definition inside a metaclass scope" do end obj.a_singleton_method.should == obj - lambda { Object.new.a_singleton_method }.should raise_error(NoMethodError) + -> { Object.new.a_singleton_method }.should raise_error(NoMethodError) end - it "raises RuntimeError if frozen" do + it "raises FrozenError if frozen" do obj = Object.new obj.freeze class << obj - lambda { def foo; end }.should raise_error(RuntimeError) + -> { def foo; end }.should raise_error(FrozenError) end end end @@ -426,11 +488,11 @@ describe "A nested method definition" do end end - lambda { DefSpecNested.a_class_method }.should raise_error(NoMethodError) + -> { DefSpecNested.a_class_method }.should raise_error(NoMethodError) DefSpecNested.create_class_method.should == DefSpecNested DefSpecNested.a_class_method.should == DefSpecNested - lambda { Object.a_class_method }.should raise_error(NoMethodError) - lambda { DefSpecNested.new.a_class_method }.should raise_error(NoMethodError) + -> { Object.a_class_method }.should raise_error(NoMethodError) + -> { DefSpecNested.new.a_class_method }.should raise_error(NoMethodError) end it "creates a singleton method when evaluated in the metaclass of an instance" do @@ -448,7 +510,7 @@ describe "A nested method definition" do obj.a_singleton_method.should == obj other = DefSpecNested.new - lambda { other.a_singleton_method }.should raise_error(NoMethodError) + -> { other.a_singleton_method }.should raise_error(NoMethodError) end it "creates a method in the surrounding context when evaluated in a def expr.method" do @@ -465,6 +527,8 @@ describe "A nested method definition" do obj = DefSpecNested.new obj.inherited_method.should == obj + ensure + DefSpecNested.send(:remove_const, :TARGET) end # See http://yugui.jp/articles/846#label-3 @@ -486,9 +550,11 @@ describe "A nested method definition" do DefSpecNested.should_not have_instance_method :arg_method DefSpecNested.should_not have_instance_method :body_method + ensure + DefSpecNested.send(:remove_const, :OBJ) end - it "defines methods as public by default" do + it "creates an instance method inside Class.new" do cls = Class.new do def do_def def new_def @@ -500,6 +566,41 @@ describe "A nested method definition" do obj = cls.new obj.do_def obj.new_def.should == 1 + + cls.new.new_def.should == 1 + + -> { Object.new.new_def }.should raise_error(NoMethodError) + end +end + +describe "A method definition always resets the visibility to public for nested definitions" do + it "in Class.new" do + cls = Class.new do + private + def do_def + def new_def + 1 + end + end + end + + obj = cls.new + -> { obj.do_def }.should raise_error(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) + end + + it "at the toplevel" do + obj = Object.new + -> { obj.toplevel_define_other_method }.should raise_error(NoMethodError, /private/) + toplevel_define_other_method + nested_method_in_toplevel_method.should == 42 + + Object.new.nested_method_in_toplevel_method.should == 42 end end @@ -512,7 +613,7 @@ describe "A method definition inside an instance_eval" do obj.an_instance_eval_method.should == obj other = Object.new - lambda { other.an_instance_eval_method }.should raise_error(NoMethodError) + -> { other.an_instance_eval_method }.should raise_error(NoMethodError) end it "creates a singleton method when evaluated inside a metaclass" do @@ -525,7 +626,7 @@ describe "A method definition inside an instance_eval" do obj.a_metaclass_eval_method.should == obj other = Object.new - lambda { other.a_metaclass_eval_method }.should raise_error(NoMethodError) + -> { other.a_metaclass_eval_method }.should raise_error(NoMethodError) end it "creates a class method when the receiver is a class" do @@ -534,7 +635,7 @@ describe "A method definition inside an instance_eval" do end DefSpecNested.an_instance_eval_class_method.should == DefSpecNested - lambda { Object.an_instance_eval_class_method }.should raise_error(NoMethodError) + -> { Object.an_instance_eval_class_method }.should raise_error(NoMethodError) end it "creates a class method when the receiver is an anonymous class" do @@ -546,7 +647,7 @@ describe "A method definition inside an instance_eval" do end m.klass_method.should == :test - lambda { Object.klass_method }.should raise_error(NoMethodError) + -> { Object.klass_method }.should raise_error(NoMethodError) end it "creates a class method when instance_eval is within class" do @@ -559,7 +660,7 @@ describe "A method definition inside an instance_eval" do end m.klass_method.should == :test - lambda { Object.klass_method }.should raise_error(NoMethodError) + -> { Object.klass_method }.should raise_error(NoMethodError) end end @@ -572,7 +673,7 @@ describe "A method definition inside an instance_exec" do end DefSpecNested.an_instance_exec_class_method.should == 1 - lambda { Object.an_instance_exec_class_method }.should raise_error(NoMethodError) + -> { Object.an_instance_exec_class_method }.should raise_error(NoMethodError) end it "creates a class method when the receiver is an anonymous class" do @@ -586,7 +687,7 @@ describe "A method definition inside an instance_exec" do end m.klass_method.should == 1 - lambda { Object.klass_method }.should raise_error(NoMethodError) + -> { Object.klass_method }.should raise_error(NoMethodError) end it "creates a class method when instance_exec is within class" do @@ -601,7 +702,7 @@ describe "A method definition inside an instance_exec" do end m.klass_method.should == 2 - lambda { Object.klass_method }.should raise_error(NoMethodError) + -> { Object.klass_method }.should raise_error(NoMethodError) end end @@ -621,7 +722,7 @@ describe "A method definition in an eval" do other = DefSpecNested.new other.an_eval_instance_method.should == other - lambda { Object.new.an_eval_instance_method }.should raise_error(NoMethodError) + -> { Object.new.an_eval_instance_method }.should raise_error(NoMethodError) end it "creates a class method" do @@ -637,8 +738,8 @@ describe "A method definition in an eval" do DefSpecNestedB.eval_class_method.should == DefSpecNestedB DefSpecNestedB.an_eval_class_method.should == DefSpecNestedB - lambda { Object.an_eval_class_method }.should raise_error(NoMethodError) - lambda { DefSpecNestedB.new.an_eval_class_method}.should raise_error(NoMethodError) + -> { Object.an_eval_class_method }.should raise_error(NoMethodError) + -> { DefSpecNestedB.new.an_eval_class_method}.should raise_error(NoMethodError) end it "creates a singleton method" do @@ -656,7 +757,7 @@ describe "A method definition in an eval" do obj.an_eval_singleton_method.should == obj other = DefSpecNested.new - lambda { other.an_eval_singleton_method }.should raise_error(NoMethodError) + -> { other.an_eval_singleton_method }.should raise_error(NoMethodError) end end @@ -682,7 +783,7 @@ describe "a method definition that sets more than one default parameter all to t end it "only allows overriding the default value of the first such parameter in each set" do - lambda { foo(1,2) }.should raise_error(ArgumentError) + -> { foo(1,2) }.should raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 0..1)') end def bar(a=b=c=1,d=2) @@ -693,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] - lambda { bar(3,4,5) }.should raise_error(ArgumentError) + -> { bar(3,4,5) }.should raise_error(ArgumentError, 'wrong number of arguments (given 3, expected 0..2)') end end @@ -703,7 +804,7 @@ describe "The def keyword" do module DefSpecsLambdaVisibility private - lambda { + -> { def some_method; end }.call end diff --git a/spec/ruby/language/defined_spec.rb b/spec/ruby/language/defined_spec.rb index 9fe460a9de..80ad1818b1 100644 --- a/spec/ruby/language/defined_spec.rb +++ b/spec/ruby/language/defined_spec.rb @@ -1,25 +1,29 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/defined', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/defined' 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,15 +31,16 @@ 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 - ret = defined?([NonExistantConstant, Array]) + ret = defined?([NonExistentConstant, Array]) ret.should == nil end it "returns nil if all elements are not defined" do - ret = defined?([NonExistantConstant, AnotherNonExistantConstant]) + ret = defined?([NonExistentConstant, AnotherNonExistentConstant]) ret.should == nil end @@ -43,9 +48,25 @@ describe "The defined? keyword for literals" do end describe "The defined? keyword when called with a method name" do + before :each do + ScratchPad.clear + end + + it "does not call the method" do + defined?(DefinedSpecs.side_effects).should == "method" + ScratchPad.recorded.should != :defined_specs_side_effects + end + + it "does not execute the arguments" do + defined?(DefinedSpecs.any_args(DefinedSpecs.side_effects)).should == "method" + ScratchPad.recorded.should != :defined_specs_side_effects + end + describe "without a receiver" do it "returns 'method' if the method is defined" do - defined?(puts).should == "method" + ret = defined?(puts) + ret.should == "method" + ret.frozen?.should == true end it "returns nil if the method is not defined" do @@ -95,6 +116,11 @@ describe "The defined? keyword when called with a method name" do defined?(obj.a_defined_method).should == "method" end + it "returns 'method' for []=" do + a = [] + defined?(a[0] = 1).should == "method" + end + it "returns nil if the method is not defined" do obj = DefinedSpecs::Basic.new defined?(obj.an_undefined_method).should be_nil @@ -159,6 +185,32 @@ describe "The defined? keyword when called with a method name" do ScratchPad.recorded.should == :defined_specs_fixnum_method end end + + describe "having a throw in the receiver" do + it "escapes defined? and performs the throw semantics as normal" do + defined_returned = false + catch(:out) { + # NOTE: defined? behaves differently if it is called in a void context, see below + defined?(throw(:out, 42).foo).should == :unreachable + defined_returned = true + }.should == 42 + defined_returned.should == false + end + end + + describe "in a void context" do + it "does not execute the receiver" do + ScratchPad.record :not_executed + defined?(DefinedSpecs.side_effects / 2) + ScratchPad.recorded.should == :not_executed + end + + it "warns about the void context when parsing it" do + -> { + eval "defined?(DefinedSpecs.side_effects / 2); 42" + }.should complain(/warning: possibly useless use of defined\? in void context/, verbose: true) + end + end end describe "The defined? keyword for an expression" do @@ -167,7 +219,9 @@ describe "The defined? keyword for an expression" do end it "returns 'assignment' for assigning a local variable" do - defined?(x = 2).should == "assignment" + ret = defined?(x = 2) + ret.should == "assignment" + ret.frozen?.should == true end it "returns 'assignment' for assigning an instance variable" do @@ -182,6 +236,14 @@ describe "The defined? keyword for an expression" do defined?(@@defined_specs_x = 2).should == "assignment" end + it "returns 'assignment' for assigning a constant" do + defined?(A = 2).should == "assignment" + end + + it "returns 'assignment' for assigning a fully qualified constant" do + defined?(Object::A = 2).should == "assignment" + end + it "returns 'assignment' for assigning multiple variables" do defined?((a, b = 1, 2)).should == "assignment" end @@ -199,7 +261,27 @@ describe "The defined? keyword for an expression" do end it "returns 'assignment' for an expression with '+='" do - defined?(x += 2).should == "assignment" + defined?(a += 1).should == "assignment" + defined?(@a += 1).should == "assignment" + defined?(@@a += 1).should == "assignment" + defined?($a += 1).should == "assignment" + defined?(A += 1).should == "assignment" + # fully qualified constant check is moved out into a separate test case + defined?(a.b += 1).should == "assignment" + defined?(a[:b] += 1).should == "assignment" + end + + # https://bugs.ruby-lang.org/issues/20111 + ruby_version_is ""..."3.4" do + it "returns 'expression' for an assigning a fully qualified constant with '+='" do + defined?(Object::A += 1).should == "expression" + end + end + + ruby_version_is "3.4" do + it "returns 'assignment' for an assigning a fully qualified constant with '+='" do + defined?(Object::A += 1).should == "assignment" + end end it "returns 'assignment' for an expression with '*='" do @@ -230,12 +312,90 @@ describe "The defined? keyword for an expression" do defined?(x >>= 2).should == "assignment" end - it "returns 'assignment' for an expression with '||='" do - defined?(x ||= 2).should == "assignment" + context "||=" do + it "returns 'assignment' for assigning a local variable with '||='" do + defined?(a ||= true).should == "assignment" + end + + it "returns 'assignment' for assigning an instance variable with '||='" do + defined?(@a ||= true).should == "assignment" + end + + it "returns 'assignment' for assigning a class variable with '||='" do + defined?(@@a ||= true).should == "assignment" + end + + it "returns 'assignment' for assigning a global variable with '||='" do + defined?($a ||= true).should == "assignment" + end + + it "returns 'assignment' for assigning a constant with '||='" do + defined?(A ||= true).should == "assignment" + end + + # https://bugs.ruby-lang.org/issues/20111 + ruby_version_is ""..."3.4" do + it "returns 'expression' for assigning a fully qualified constant with '||='" do + defined?(Object::A ||= true).should == "expression" + end + end + + ruby_version_is "3.4" do + it "returns 'assignment' for assigning a fully qualified constant with '||='" do + defined?(Object::A ||= true).should == "assignment" + end + end + + it "returns 'assignment' for assigning an attribute with '||='" do + defined?(a.b ||= true).should == "assignment" + end + + it "returns 'assignment' for assigning a referenced element with '||='" do + defined?(a[:b] ||= true).should == "assignment" + end end - it "returns 'assignment' for an expression with '&&='" do - defined?(x &&= 2).should == "assignment" + context "&&=" do + it "returns 'assignment' for assigning a local variable with '&&='" do + defined?(a &&= true).should == "assignment" + end + + it "returns 'assignment' for assigning an instance variable with '&&='" do + defined?(@a &&= true).should == "assignment" + end + + it "returns 'assignment' for assigning a class variable with '&&='" do + defined?(@@a &&= true).should == "assignment" + end + + it "returns 'assignment' for assigning a global variable with '&&='" do + defined?($a &&= true).should == "assignment" + end + + it "returns 'assignment' for assigning a constant with '&&='" do + defined?(A &&= true).should == "assignment" + end + + # https://bugs.ruby-lang.org/issues/20111 + ruby_version_is ""..."3.4" do + it "returns 'expression' for assigning a fully qualified constant with '&&='" do + defined?(Object::A &&= true).should == "expression" + end + end + + ruby_version_is "3.4" do + it "returns 'assignment' for assigning a fully qualified constant with '&&='" do + defined?(Object::A &&= true).should == "assignment" + end + end + + it "returns 'assignment' for assigning an attribute with '&&='" do + defined?(a.b &&= true).should == "assignment" + end + + it "returns 'assignment' for assigning a referenced element with '&&='" do + defined?(a[:b] &&= true).should == "assignment" + end end it "returns 'assignment' for an expression with '**='" do @@ -275,9 +435,7 @@ describe "The defined? keyword for an expression" do end it "returns nil for an expression with '!' and an unset class variable" do - -> { - @result = defined?(!@@defined_specs_undefined_class_variable) - }.should complain(/class variable access from toplevel/) + @result = eval("class singleton_class::A; defined?(!@@doesnt_exist) end", binding, __FILE__, __LINE__) @result.should be_nil end @@ -286,9 +444,7 @@ describe "The defined? keyword for an expression" do end it "returns nil for an expression with 'not' and an unset class variable" do - -> { - @result = defined?(not @@defined_specs_undefined_class_variable) - }.should complain(/class variable access from toplevel/) + @result = eval("class singleton_class::A; defined?(not @@doesnt_exist) end", binding, __FILE__, __LINE__) @result.should be_nil end @@ -474,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 @@ -490,7 +648,9 @@ describe "The defined? keyword for variables" do end it "returns 'instance-variable' for an instance variable that has been assigned" do - DefinedSpecs::Basic.new.instance_variable_defined.should == "instance-variable" + ret = DefinedSpecs::Basic.new.instance_variable_defined + ret.should == "instance-variable" + ret.frozen?.should == true end it "returns 'instance-variable' for an instance variable that has been assigned to nil" do @@ -505,6 +665,12 @@ describe "The defined? keyword for variables" do DefinedSpecs::Basic.new.global_variable_read.should be_nil end + it "returns 'global-variable' for a global variable that has been assigned nil" do + 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 # 'global-variable' even when they are not set (or they are always "set" # but the value may be nil). In other words, 'defined?($~)' will return @@ -543,16 +709,12 @@ describe "The defined? keyword for variables" do defined?($+).should be_nil end - it "returns nil for $1-$9" do + it "returns nil for any last match global" do defined?($1).should be_nil - defined?($2).should be_nil - defined?($3).should be_nil defined?($4).should be_nil - defined?($5).should be_nil - defined?($6).should be_nil defined?($7).should be_nil - defined?($8).should be_nil - defined?($9).should be_nil + defined?($10).should be_nil + defined?($200).should be_nil end end @@ -587,13 +749,10 @@ describe "The defined? keyword for variables" do end it "returns nil for non-captures" do - defined?($3).should be_nil defined?($4).should be_nil - defined?($5).should be_nil - defined?($6).should be_nil defined?($7).should be_nil - defined?($8).should be_nil - defined?($9).should be_nil + defined?($10).should be_nil + defined?($200).should be_nil end end @@ -622,16 +781,12 @@ describe "The defined? keyword for variables" do defined?($+).should be_nil end - it "returns nil for $1-$9" do + it "returns nil for any last match global" do defined?($1).should be_nil - defined?($2).should be_nil - defined?($3).should be_nil defined?($4).should be_nil - defined?($5).should be_nil - defined?($6).should be_nil defined?($7).should be_nil - defined?($8).should be_nil - defined?($9).should be_nil + defined?($10).should be_nil + defined?($200).should be_nil end end @@ -666,13 +821,10 @@ describe "The defined? keyword for variables" do end it "returns nil for non-captures" do - defined?($3).should be_nil defined?($4).should be_nil - defined?($5).should be_nil - defined?($6).should be_nil defined?($7).should be_nil - defined?($8).should be_nil - defined?($9).should be_nil + defined?($10).should be_nil + defined?($200).should be_nil end end it "returns 'global-variable' for a global variable that has been assigned" do @@ -688,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 @@ -699,7 +853,9 @@ end describe "The defined? keyword for a simple constant" do it "returns 'constant' when the constant is defined" do - defined?(DefinedSpecs).should == "constant" + ret = defined?(DefinedSpecs) + ret.should == "constant" + ret.frozen?.should == true end it "returns nil when the constant is not defined" do @@ -744,6 +900,10 @@ describe "The defined? keyword for a scoped constant" do defined?(DefinedSpecs::Undefined).should be_nil end + it "returns nil when the constant is not defined and the outer module implements .const_missing" do + defined?(DefinedSpecs::ModuleWithConstMissing::Undefined).should be_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 @@ -766,16 +926,8 @@ describe "The defined? keyword for a scoped constant" do defined?(DefinedSpecs::String).should be_nil end - ruby_version_is ""..."2.5" do - it "returns 'constant' when a constant is defined on top-level but not on the class" do - defined?(DefinedSpecs::Basic::String).should == 'constant' - end - end - - ruby_version_is "2.5" do - it "returns nil when a constant is defined on top-level but not on the class" do - defined?(DefinedSpecs::Basic::String).should be_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 end it "returns 'constant' if the scoped-scoped constant is defined" do @@ -915,17 +1067,21 @@ describe "The defined? keyword for a variable scoped constant" do end it "returns nil if the class scoped constant is not defined" do - -> { - @@defined_specs_obj = DefinedSpecs::Basic - defined?(@@defined_specs_obj::Undefined).should be_nil - }.should complain(/class variable access from toplevel/) + eval(<<-END, binding, __FILE__, __LINE__) + class singleton_class::A + @@defined_specs_obj = DefinedSpecs::Basic + defined?(@@defined_specs_obj::Undefined).should be_nil + end + END end it "returns 'constant' if the constant is defined in the scope of the class variable" do - -> { - @@defined_specs_obj = DefinedSpecs::Basic - defined?(@@defined_specs_obj::A).should == "constant" - }.should complain(/class variable access from toplevel/) + eval(<<-END, binding, __FILE__, __LINE__) + class singleton_class::A + @@defined_specs_obj = DefinedSpecs::Basic + defined?(@@defined_specs_obj::A).should == "constant" + end + END end it "returns nil if the local scoped constant is not defined" do @@ -959,12 +1115,26 @@ describe "The defined? keyword for yield" do end it "returns 'yield' if a block is passed to a method not taking a block parameter" do - DefinedSpecs::Basic.new.yield_block.should == "yield" + ret = DefinedSpecs::Basic.new.yield_block + ret.should == "yield" + ret.frozen?.should == true end it "returns 'yield' if a block is passed to a method taking a block parameter" do DefinedSpecs::Basic.new.yield_block_parameter.should == "yield" end + + it "returns 'yield' when called within a block" do + def yielder + yield + end + + def call_defined + yielder { defined?(yield) } + end + + call_defined() { }.should == "yield" + end end describe "The defined? keyword for super" do @@ -990,7 +1160,9 @@ describe "The defined? keyword for super" do end it "returns 'super' when a superclass method exists" do - DefinedSpecs::Super.new.method_no_args.should == "super" + ret = DefinedSpecs::Super.new.method_no_args + ret.should == "super" + ret.frozen?.should == true end it "returns 'super' from a block when a superclass method exists" do diff --git a/spec/ruby/language/delegation_spec.rb b/spec/ruby/language/delegation_spec.rb new file mode 100644 index 0000000000..c711a536c2 --- /dev/null +++ b/spec/ruby/language/delegation_spec.rb @@ -0,0 +1,158 @@ +require_relative '../spec_helper' +require_relative 'fixtures/delegation' + +# 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 "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], 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 + + 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 + + a.new.delegate(0, 1).should == [[0, 1], {}, nil] + end + + ruby_version_is "3.3" do + 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_error(SyntaxError, /anonymous rest parameter is also used within block/) + end + end + end +end + +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 + + ruby_version_is "3.3" do + 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_error(SyntaxError, /anonymous keyword rest parameter is also used within block/) + end + 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 + + ruby_version_is "3.3" do + 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_error(SyntaxError, /anonymous block parameter is also used within block/) + end + end + end +end diff --git a/spec/ruby/language/encoding_spec.rb b/spec/ruby/language/encoding_spec.rb index 070fa52bba..e761a53cb6 100644 --- a/spec/ruby/language/encoding_spec.rb +++ b/spec/ruby/language/encoding_spec.rb @@ -1,7 +1,7 @@ # -*- encoding: us-ascii -*- -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/coding_us_ascii', __FILE__) -require File.expand_path('../fixtures/coding_utf_8', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/coding_us_ascii' +require_relative 'fixtures/coding_utf_8' describe "The __ENCODING__ pseudo-variable" do it "is an instance of Encoding" 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("ASCII-8BIT")).should == Encoding::ASCII_8BIT + 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: ASCII-8BIT\n__ENCODING__".force_encoding("US-ASCII") - eval(code).should == Encoding::ASCII_8BIT + code = "# encoding: BINARY\n__ENCODING__".dup.force_encoding("US-ASCII") + eval(code).should == Encoding::BINARY - code = "# encoding: us-ascii\n__ENCODING__".force_encoding("ASCII-8BIT") + 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 - lambda { eval("__ENCODING__ = 1") }.should raise_error(SyntaxError) + -> { eval("__ENCODING__ = 1") }.should raise_error(SyntaxError) end end diff --git a/spec/ruby/language/ensure_spec.rb b/spec/ruby/language/ensure_spec.rb index 1d99dcf5f2..b76292c007 100644 --- a/spec/ruby/language/ensure_spec.rb +++ b/spec/ruby/language/ensure_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/ensure', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/ensure' describe "An ensure block inside a begin block" do before :each do @@ -7,7 +7,7 @@ describe "An ensure block inside a begin block" do end it "is executed when an exception is raised in it's corresponding begin block" do - lambda { + -> { begin ScratchPad << :begin raise EnsureSpec::Error @@ -66,6 +66,18 @@ describe "An ensure block inside a begin block" do :ensure end.should == :begin end + + it "sets exception cause if raises exception in block and in ensure" do + -> { + begin + raise "from block" + ensure + raise "from ensure" + end + }.should raise_error(RuntimeError, "from ensure") { |e| + e.cause.message.should == "from block" + } + end end describe "The value of an ensure expression," do @@ -96,7 +108,7 @@ describe "An ensure block inside a method" do end it "is executed when an exception is raised in the method" do - lambda { @obj.raise_in_method_with_ensure }.should raise_error(EnsureSpec::Error) + -> { @obj.raise_in_method_with_ensure }.should raise_error(EnsureSpec::Error) @obj.executed.should == [:method, :ensure] end @@ -117,6 +129,34 @@ describe "An ensure block inside a method" do it "has an impact on the method's explicit return value" do @obj.explicit_return_in_method_with_ensure.should == :ensure end + + it "has an impact on the method's explicit return value from rescue if returns explicitly" do + @obj.explicit_return_in_rescue_and_explicit_return_in_ensure.should == "returned in ensure" + end + + it "has no impact on the method's explicit return value from rescue if returns implicitly" do + @obj.explicit_return_in_rescue_and_implicit_return_in_ensure.should == "returned in rescue" + end + + it "suppresses exception raised in method if returns value explicitly" do + @obj.raise_and_explicit_return_in_ensure.should == "returned in ensure" + end + + it "suppresses exception raised in rescue if returns value explicitly" do + @obj.raise_in_rescue_and_explicit_return_in_ensure.should == "returned in ensure" + end + + 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") + 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") + end end describe "An ensure block inside a class" do @@ -125,7 +165,7 @@ describe "An ensure block inside a class" do end it "is executed when an exception is raised" do - lambda { + -> { eval <<-ruby class EnsureInClassExample ScratchPad << :class @@ -200,7 +240,7 @@ end describe "An ensure block inside {} block" do it "is not allowed" do - lambda { + -> { eval <<-ruby lambda { raise @@ -211,83 +251,96 @@ describe "An ensure block inside {} block" do end end -ruby_version_is "2.5" do - describe "An ensure block inside 'do end' block" do - before :each do - ScratchPad.record [] - end - - it "is executed when an exception is raised in it's corresponding begin block" do - lambda { - eval(<<-ruby).call - lambda do - ScratchPad << :begin - raise EnsureSpec::Error - ensure - ScratchPad << :ensure - end - ruby - }.should raise_error(EnsureSpec::Error) - - ScratchPad.recorded.should == [:begin, :ensure] - end +describe "An ensure block inside 'do end' block" do + before :each do + ScratchPad.record [] + 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 in it's corresponding begin block" do + -> { eval(<<-ruby).call lambda do ScratchPad << :begin - raise "An exception occurred!" - rescue - ScratchPad << :rescue + raise EnsureSpec::Error ensure ScratchPad << :ensure end ruby + }.should raise_error(EnsureSpec::Error) - ScratchPad.recorded.should == [:begin, :rescue, :ensure] - end + ScratchPad.recorded.should == [:begin, :ensure] + end - it "is executed even when a symbol is thrown in it's corresponding begin block" do - catch(:symbol) do - eval(<<-ruby).call - lambda do - ScratchPad << :begin - throw(:symbol) - rescue - ScratchPad << :rescue - ensure - ScratchPad << :ensure - end - ruby + it "is executed when an exception is raised and rescued in it's corresponding begin block" do + eval(<<-ruby).call + lambda do + ScratchPad << :begin + raise "An exception occurred!" + rescue + ScratchPad << :rescue + ensure + ScratchPad << :ensure end + ruby - ScratchPad.recorded.should == [:begin, :ensure] - end + ScratchPad.recorded.should == [:begin, :rescue, :ensure] + end - it "is executed when nothing is raised or thrown in it's corresponding begin block" do + it "is executed even when a symbol is thrown in it's corresponding begin block" do + catch(:symbol) do eval(<<-ruby).call lambda do ScratchPad << :begin + throw(:symbol) rescue ScratchPad << :rescue ensure ScratchPad << :ensure end ruby - - ScratchPad.recorded.should == [:begin, :ensure] end - it "has no return value" do - result = eval(<<-ruby).call - lambda do - :begin + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "is executed when nothing is raised or thrown in it's corresponding begin block" do + eval(<<-ruby).call + lambda do + ScratchPad << :begin + rescue + ScratchPad << :rescue + ensure + ScratchPad << :ensure + end + ruby + + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "has no return value" do + result = eval(<<-ruby).call + lambda do + :begin + ensure + :ensure + end + ruby + + result.should == :begin + end + + ruby_version_is "3.4" do + it "does not introduce extra backtrace entries" do + def foo + begin + raise "oops" ensure - :ensure + return caller(0, 2) # rubocop:disable Lint/EnsureReturn end - ruby - - result.should == :begin + 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 3e6e7ff48c..51bcde62e8 100644 --- a/spec/ruby/language/execution_spec.rb +++ b/spec/ruby/language/execution_spec.rb @@ -1,10 +1,49 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "``" do it "returns the output of the executed sub-process" 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 409400ca83..59563d9642 100644 --- a/spec/ruby/language/file_spec.rb +++ b/spec/ruby/language/file_spec.rb @@ -1,29 +1,29 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/code_loading', __FILE__) -require File.expand_path('../shared/__FILE__', __FILE__) +require_relative '../spec_helper' +require_relative '../fixtures/code_loading' +require_relative 'shared/__FILE__' describe "The __FILE__ pseudo-variable" do it "raises a SyntaxError if assigned to" do - lambda { eval("__FILE__ = 1") }.should raise_error(SyntaxError) + -> { eval("__FILE__ = 1") }.should raise_error(SyntaxError) end - it "equals (eval) inside an eval" do - eval("__FILE__").should == "(eval)" + ruby_version_is ""..."3.3" do + it "equals (eval) inside an eval" do + eval("__FILE__").should == "(eval)" + end end -end -describe "The __FILE__ pseudo-variable" do - it_behaves_like :language___FILE__, :require, CodeLoadingSpecs::Method.new + ruby_version_is "3.3" do + it "equals (eval at __FILE__:__LINE__) inside an eval" do + eval("__FILE__").should == "(eval at #{__FILE__}:#{__LINE__})" + end + end end -describe "The __FILE__ pseudo-variable" do +describe "The __FILE__ pseudo-variable with require" do it_behaves_like :language___FILE__, :require, Kernel end -describe "The __FILE__ pseudo-variable" do - it_behaves_like :language___FILE__, :load, CodeLoadingSpecs::Method.new -end - -describe "The __FILE__ pseudo-variable" do +describe "The __FILE__ pseudo-variable with load" do it_behaves_like :language___FILE__, :load, Kernel end diff --git a/spec/ruby/language/fixtures/array.rb b/spec/ruby/language/fixtures/array.rb index 4d8ce74ed6..c1036575ff 100644 --- a/spec/ruby/language/fixtures/array.rb +++ b/spec/ruby/language/fixtures/array.rb @@ -8,4 +8,25 @@ module ArraySpec [a, b, c, d] end end + + class SideEffect + def initialize() + @call_count = 0 + end + + attr_reader :call_count + + def array_result(a_number) + [result(a_number), result(a_number)] + end + + def result(a_number) + @call_count += 1 + if a_number + 1 + else + :thing + end + end + end end diff --git a/spec/ruby/language/fixtures/block.rb b/spec/ruby/language/fixtures/block.rb index 9848d18776..33baac6aeb 100644 --- a/spec/ruby/language/fixtures/block.rb +++ b/spec/ruby/language/fixtures/block.rb @@ -15,6 +15,10 @@ module BlockSpecs def r(a) yield(*a) end + + def k(*a) + yield(*a, b: true) + end end # TODO: rewrite all specs that use Yield to use Yielder diff --git a/spec/ruby/language/fixtures/break.rb b/spec/ruby/language/fixtures/break.rb index 2d07cc3d48..217c20a2c0 100644 --- a/spec/ruby/language/fixtures/break.rb +++ b/spec/ruby/language/fixtures/break.rb @@ -163,7 +163,7 @@ module BreakSpecs # on the call stack when the lambda is invoked. def break_in_defining_scope(value=true) note :a - note lambda { + note -> { note :b if value break :break @@ -177,7 +177,7 @@ module BreakSpecs def break_in_nested_scope note :a - l = lambda do + l = -> do note :b break :break note :c @@ -197,7 +197,7 @@ module BreakSpecs def break_in_nested_scope_yield note :a - l = lambda do + l = -> do note :b break :break note :c @@ -217,7 +217,7 @@ module BreakSpecs def break_in_nested_scope_block note :a - l = lambda do + l = -> do note :b break :break note :c @@ -251,7 +251,7 @@ module BreakSpecs # active on the call stack when the lambda is invoked. def create_lambda note :la - l = lambda do + l = -> do note :lb break :break note :lc diff --git a/spec/ruby/language/fixtures/break_lambda_toplevel.rb b/spec/ruby/language/fixtures/break_lambda_toplevel.rb index 05af1d3fdc..da5abbaf00 100644 --- a/spec/ruby/language/fixtures/break_lambda_toplevel.rb +++ b/spec/ruby/language/fixtures/break_lambda_toplevel.rb @@ -1,6 +1,6 @@ print "a," -print lambda { +print -> { print "b," break "break," print "c," diff --git a/spec/ruby/language/fixtures/break_lambda_toplevel_block.rb b/spec/ruby/language/fixtures/break_lambda_toplevel_block.rb index a35cb8a8a1..3dcee62424 100644 --- a/spec/ruby/language/fixtures/break_lambda_toplevel_block.rb +++ b/spec/ruby/language/fixtures/break_lambda_toplevel_block.rb @@ -1,6 +1,6 @@ print "a," -l = lambda { +l = -> { print "b," break "break," print "c," diff --git a/spec/ruby/language/fixtures/break_lambda_toplevel_method.rb b/spec/ruby/language/fixtures/break_lambda_toplevel_method.rb index 200040d614..a5936a3d70 100644 --- a/spec/ruby/language/fixtures/break_lambda_toplevel_method.rb +++ b/spec/ruby/language/fixtures/break_lambda_toplevel_method.rb @@ -1,6 +1,6 @@ print "a," -l = lambda { +l = -> { print "b," break "break," print "c," diff --git a/spec/ruby/language/fixtures/bytes_magic_comment.rb b/spec/ruby/language/fixtures/bytes_magic_comment.rb new file mode 100644 index 0000000000..2bc2bcfb07 --- /dev/null +++ b/spec/ruby/language/fixtures/bytes_magic_comment.rb @@ -0,0 +1,2 @@ +# encoding: big5 +$magic_comment_result = 'An'.bytes.inspect diff --git a/spec/ruby/language/fixtures/case_magic_comment.rb b/spec/ruby/language/fixtures/case_magic_comment.rb new file mode 100644 index 0000000000..96f35a7c94 --- /dev/null +++ b/spec/ruby/language/fixtures/case_magic_comment.rb @@ -0,0 +1,2 @@ +# CoDiNg: bIg5 +$magic_comment_result = __ENCODING__.name 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/def.rb b/spec/ruby/language/fixtures/def.rb index 81bfce73d0..e07060ed74 100644 --- a/spec/ruby/language/fixtures/def.rb +++ b/spec/ruby/language/fixtures/def.rb @@ -1,3 +1,9 @@ +def toplevel_define_other_method + def nested_method_in_toplevel_method + 42 + end +end + def some_toplevel_method end diff --git a/spec/ruby/language/fixtures/defined.rb b/spec/ruby/language/fixtures/defined.rb index d26e553c4b..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 @@ -94,6 +97,11 @@ module DefinedSpecs defined? $defined_specs_global_variable_defined end + def global_variable_defined_as_nil + $defined_specs_global_variable_defined_as_nil = nil + defined? $defined_specs_global_variable_defined_as_nil + end + def class_variable_undefined defined? @@class_variable_undefined end @@ -277,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 new file mode 100644 index 0000000000..da2b024791 --- /dev/null +++ b/spec/ruby/language/fixtures/delegation.rb @@ -0,0 +1,11 @@ +module DelegationSpecs + class Target + def target(*args, **kwargs, &block) + [args, kwargs, block] + end + + def target_block(*args, **kwargs) + yield [kwargs, args] + end + end +end diff --git a/spec/ruby/language/fixtures/emacs_magic_comment.rb b/spec/ruby/language/fixtures/emacs_magic_comment.rb new file mode 100644 index 0000000000..2b09f3e74c --- /dev/null +++ b/spec/ruby/language/fixtures/emacs_magic_comment.rb @@ -0,0 +1,2 @@ +# -*- encoding: big5 -*- +$magic_comment_result = __ENCODING__.name diff --git a/spec/ruby/language/fixtures/ensure.rb b/spec/ruby/language/fixtures/ensure.rb index d1a9da37b8..6047ac5bc0 100644 --- a/spec/ruby/language/fixtures/ensure.rb +++ b/spec/ruby/language/fixtures/ensure.rb @@ -40,6 +40,50 @@ module EnsureSpec ensure return :ensure end + + def explicit_return_in_rescue_and_explicit_return_in_ensure + raise + rescue + return 2 + ensure + return "returned in ensure" + end + + def explicit_return_in_rescue_and_implicit_return_in_ensure + raise + rescue + return "returned in rescue" + ensure + 3 + end + + def raise_and_explicit_return_in_ensure + raise + ensure + return "returned in ensure" + end + + def raise_in_rescue_and_explicit_return_in_ensure + raise + rescue + raise + ensure + return "returned in ensure" + end + + def raise_in_rescue_and_raise_in_ensure + raise + rescue + raise "raised in rescue" + ensure + raise "raised in ensure" + end + + def raise_in_method_and_raise_in_ensure + raise + ensure + raise "raised in ensure" + end end end diff --git a/spec/ruby/language/fixtures/for_scope.rb b/spec/ruby/language/fixtures/for_scope.rb new file mode 100644 index 0000000000..9c44a23a2c --- /dev/null +++ b/spec/ruby/language/fixtures/for_scope.rb @@ -0,0 +1,15 @@ +module ForSpecs + class ForInClassMethod + m = :same_variable_set_outside + + def self.foo + all = [] + for m in [:bar, :baz] + all << m + end + all + end + + READER = -> { m } + end +end diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb b/spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb Binary files differindex d0558a2251..f72a32e879 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_two_literals.rb b/spec/ruby/language/fixtures/freeze_magic_comment_two_literals.rb index a4d655ad02..cccc5969bd 100644 --- a/spec/ruby/language/fixtures/freeze_magic_comment_two_literals.rb +++ b/spec/ruby/language/fixtures/freeze_magic_comment_two_literals.rb @@ -1,3 +1,3 @@ # frozen_string_literal: true -p "abc".object_id == "abc".object_id +p "abc".equal?("abc") diff --git a/spec/ruby/language/fixtures/hash_strings_ascii8bit.rb b/spec/ruby/language/fixtures/hash_strings_binary.rb index 4ac11b9930..44b99cbf80 100644 --- a/spec/ruby/language/fixtures/hash_strings_ascii8bit.rb +++ b/spec/ruby/language/fixtures/hash_strings_binary.rb @@ -1,6 +1,6 @@ -# encoding: ascii-8bit +# encoding: binary -module HashStringsASCII8BIT +module HashStringsBinary def self.literal_hash {"foo" => "bar"} end diff --git a/spec/ruby/language/fixtures/magic_comment.rb b/spec/ruby/language/fixtures/magic_comment.rb new file mode 100644 index 0000000000..120ef6ff4a --- /dev/null +++ b/spec/ruby/language/fixtures/magic_comment.rb @@ -0,0 +1,2 @@ +# encoding: big5 +$magic_comment_result = __ENCODING__.name diff --git a/spec/ruby/language/fixtures/metaclass.rb b/spec/ruby/language/fixtures/metaclass.rb index a1990b9225..a8f837e701 100644 --- a/spec/ruby/language/fixtures/metaclass.rb +++ b/spec/ruby/language/fixtures/metaclass.rb @@ -31,4 +31,3 @@ module MetaClassSpecs class D < C; end end - 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/no_magic_comment.rb b/spec/ruby/language/fixtures/no_magic_comment.rb new file mode 100644 index 0000000000..743a0f9503 --- /dev/null +++ b/spec/ruby/language/fixtures/no_magic_comment.rb @@ -0,0 +1 @@ +$magic_comment_result = __ENCODING__.name diff --git a/spec/ruby/language/fixtures/print_magic_comment_result_at_exit.rb b/spec/ruby/language/fixtures/print_magic_comment_result_at_exit.rb new file mode 100644 index 0000000000..aa82cf4471 --- /dev/null +++ b/spec/ruby/language/fixtures/print_magic_comment_result_at_exit.rb @@ -0,0 +1,3 @@ +at_exit { + print $magic_comment_result +} 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.rb b/spec/ruby/language/fixtures/rescue.rb index 3fa5df1eb5..b906e17a2f 100644 --- a/spec/ruby/language/fixtures/rescue.rb +++ b/spec/ruby/language/fixtures/rescue.rb @@ -60,4 +60,8 @@ module RescueSpecs ScratchPad << :outside_begin :return_val end + + def self.raise_standard_error + raise StandardError, "an error occurred" + 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/rescue_captures.rb b/spec/ruby/language/fixtures/rescue_captures.rb new file mode 100644 index 0000000000..69f9b83904 --- /dev/null +++ b/spec/ruby/language/fixtures/rescue_captures.rb @@ -0,0 +1,107 @@ +module RescueSpecs + class Captor + attr_accessor :captured_error + + def self.should_capture_exception + captor = new + captor.capture('some text').should == :caught # Ensure rescue body still runs + captor.captured_error.message.should == 'some text' + end + end + + class ClassVariableCaptor < Captor + def capture(msg) + raise msg + rescue => @@captured_error + :caught + end + + def captured_error + self.class.remove_class_variable(:@@captured_error) + end + end + + class ConstantCaptor < Captor + # Using lambda gets around the dynamic constant assignment warning + CAPTURE = -> msg { + begin + raise msg + rescue => CapturedError + :caught + end + } + + def capture(msg) + CAPTURE.call(msg) + end + + def captured_error + self.class.send(:remove_const, :CapturedError) + end + end + + class GlobalVariableCaptor < Captor + def capture(msg) + raise msg + rescue => $captured_error + :caught + end + + def captured_error + $captured_error.tap do + $captured_error = nil # Can't remove globals, only nil them out + end + end + end + + class InstanceVariableCaptor < Captor + def capture(msg) + raise msg + rescue => @captured_error + :caught + end + end + + class LocalVariableCaptor < Captor + def capture(msg) + raise msg + rescue => captured_error + @captured_error = captured_error + :caught + end + end + + class SafeNavigationSetterCaptor < Captor + def capture(msg) + raise msg + rescue => self&.captured_error + :caught + end + end + + class SetterCaptor < Captor + def capture(msg) + raise msg + rescue => self.captured_error + :caught + end + end + + class SquareBracketsCaptor < Captor + def capture(msg) + @hash = {} + + raise msg + rescue => self[:error] + :caught + end + + def []=(key, value) + @hash[key] = value + end + + def captured_error + @hash[:error] + end + end +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/second_line_magic_comment.rb b/spec/ruby/language/fixtures/second_line_magic_comment.rb new file mode 100644 index 0000000000..a3dd50393b --- /dev/null +++ b/spec/ruby/language/fixtures/second_line_magic_comment.rb @@ -0,0 +1,3 @@ + +# encoding: big5 +$magic_comment_result = __ENCODING__.name diff --git a/spec/ruby/language/fixtures/second_token_magic_comment.rb b/spec/ruby/language/fixtures/second_token_magic_comment.rb new file mode 100644 index 0000000000..8d443e68f3 --- /dev/null +++ b/spec/ruby/language/fixtures/second_token_magic_comment.rb @@ -0,0 +1,2 @@ +1 + 1 # encoding: big5 +$magic_comment_result = __ENCODING__.name diff --git a/spec/ruby/language/fixtures/send.rb b/spec/ruby/language/fixtures/send.rb index c3013616b2..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 @@ -53,8 +53,9 @@ module LangSendSpecs end class PrivateGetter - attr_reader :foo + attr_accessor :foo private :foo + private :foo= def call_self_foo self.foo @@ -80,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/shebang_magic_comment.rb b/spec/ruby/language/fixtures/shebang_magic_comment.rb new file mode 100755 index 0000000000..f8e5e7d8e4 --- /dev/null +++ b/spec/ruby/language/fixtures/shebang_magic_comment.rb @@ -0,0 +1,3 @@ +#!/usr/bin/ruby +# encoding: big5 +$magic_comment_result = __ENCODING__.name diff --git a/spec/ruby/language/fixtures/squiggly_heredoc.rb b/spec/ruby/language/fixtures/squiggly_heredoc.rb index afc87514c7..984a629e5b 100644 --- a/spec/ruby/language/fixtures/squiggly_heredoc.rb +++ b/spec/ruby/language/fixtures/squiggly_heredoc.rb @@ -29,6 +29,22 @@ module SquigglyHeredocSpecs HERE end + def self.backslash + <<~HERE + a + b\ + c + HERE + end + + def self.least_indented_on_the_first_line + <<~HERE + a + b + c + HERE + end + def self.least_indented_on_the_last_line <<~HERE a @@ -36,4 +52,20 @@ module SquigglyHeredocSpecs c HERE end + + def self.least_indented_on_the_first_line_single + <<~'HERE' + a + b + c + HERE + end + + def self.least_indented_on_the_last_line_single + <<~'HERE' + a + b + c + HERE + end end diff --git a/spec/ruby/language/fixtures/super.rb b/spec/ruby/language/fixtures/super.rb index 09a454bdf4..c5bdcf0e40 100644 --- a/spec/ruby/language/fixtures/super.rb +++ b/spec/ruby/language/fixtures/super.rb @@ -1,4 +1,4 @@ -module Super +module SuperSpecs module S1 class A def foo(a) @@ -282,7 +282,7 @@ module Super # # When name3 is called then, Alias2 (NOT Alias3) is presented as the # current module to Alias2#name, so that when super is called, - # Alias2->superclass is next. + # Alias2's superclass is next. # # Otherwise, Alias2 is next, which is where name was to begin with, # causing the wrong #name method to be called. @@ -377,12 +377,12 @@ module Super end def b - block_ref = lambda { 15 } + block_ref = -> { 15 } [super { 14 }, super(&block_ref)] end def c - block_ref = lambda { 16 } + block_ref = -> { 16 } super(&block_ref) end end @@ -455,12 +455,114 @@ module Super end end + module ZSuperWithRestAndPost + class A + def m(*args, a, b) + args + end + + def m_modified(*args, a, b) + args + end + end + + class B < A + def m(*args, a, b) + super + end + + def m_modified(*args, a, b) + args[1] = 14 + super + end + end + end + + module ZSuperWithRestOthersAndPost + class A + def m(a, *args, b) + args + end + + def m_modified(a, *args, b) + args + end + end + + class B < A + def m(a, *args, b) + super + end + + def m_modified(a, *args, b) + args[1] = 14 + super + end + end + end + + module ZSuperWithRestReassigned + class A + def a(*args) + args + end + end + + class B < A + def a(*args) + args = ["foo"] + + super + end + end + end + + module ZSuperWithRestReassignedWithScalar + class A + def a(*args) + args + end + end + + class B < A + def a(*args) + args = "foo" + + super + end + end + end + module ZSuperWithUnderscores class A def m(*args) 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 @@ -471,6 +573,30 @@ module Super 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 @@ -478,6 +604,20 @@ module Super 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/utf16-be-nobom.rb b/spec/ruby/language/fixtures/utf16-be-nobom.rb Binary files differnew file mode 100644 index 0000000000..99e2ce8ce8 --- /dev/null +++ b/spec/ruby/language/fixtures/utf16-be-nobom.rb diff --git a/spec/ruby/language/fixtures/utf16-le-nobom.rb b/spec/ruby/language/fixtures/utf16-le-nobom.rb Binary files differnew file mode 100644 index 0000000000..98de9697ca --- /dev/null +++ b/spec/ruby/language/fixtures/utf16-le-nobom.rb diff --git a/spec/ruby/language/fixtures/utf8-bom.rb b/spec/ruby/language/fixtures/utf8-bom.rb new file mode 100644 index 0000000000..50c223a922 --- /dev/null +++ b/spec/ruby/language/fixtures/utf8-bom.rb @@ -0,0 +1,2 @@ +# encoding: utf-8 +puts 'hello' diff --git a/spec/ruby/language/fixtures/utf8-nobom.rb b/spec/ruby/language/fixtures/utf8-nobom.rb new file mode 100644 index 0000000000..75f5563b95 --- /dev/null +++ b/spec/ruby/language/fixtures/utf8-nobom.rb @@ -0,0 +1,2 @@ +# encoding: utf-8 +puts 'hello' 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/fixtures/vim_magic_comment.rb b/spec/ruby/language/fixtures/vim_magic_comment.rb new file mode 100644 index 0000000000..60cbe7a3bf --- /dev/null +++ b/spec/ruby/language/fixtures/vim_magic_comment.rb @@ -0,0 +1,2 @@ +# vim: filetype=ruby, fileencoding=big5, tabsize=3, shiftwidth=3 +$magic_comment_result = __ENCODING__.name diff --git a/spec/ruby/language/fixtures/yield.rb b/spec/ruby/language/fixtures/yield.rb index a195616640..9f7a2ba238 100644 --- a/spec/ruby/language/fixtures/yield.rb +++ b/spec/ruby/language/fixtures/yield.rb @@ -21,6 +21,10 @@ module YieldSpecs yield(*a) end + def k(a) + yield(*a, b: true) + end + def rs(a, b, c) yield(a, b, *c) end diff --git a/spec/ruby/language/for_spec.rb b/spec/ruby/language/for_spec.rb index c9d043fa25..b8ddfe5f0d 100644 --- a/spec/ruby/language/for_spec.rb +++ b/spec/ruby/language/for_spec.rb @@ -1,4 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/for_scope' # for name[, name]... in expr [do] # body @@ -18,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 @@ -32,14 +54,13 @@ describe "The for expression" do end it "iterates over any object responding to 'each'" do - class XYZ - def each - (0..10).each { |i| yield i } - end + obj = Object.new + def obj.each + (0..10).each { |i| yield i } end j = 0 - for i in XYZ.new + for i in obj j += i end j.should == 55 @@ -81,6 +102,88 @@ 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 + + # Segfault in MRI 3.3 and lower: https://bugs.ruby-lang.org/issues/20468 + ruby_bug "#20468", ""..."3.4" do + 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 be_nil + n.should == 3 + end + 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 @@ -131,6 +234,11 @@ describe "The for expression" do a.should == 123 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 + end + it "returns expr" do for i in 1..3; end.should == (1..3) for i,j in { 1 => 10, 2 => 20 }; end.should == { 1 => 10, 2 => 20 } diff --git a/spec/ruby/language/hash_spec.rb b/spec/ruby/language/hash_spec.rb index edd9d4fbb2..668716e2e3 100644 --- a/spec/ruby/language/hash_spec.rb +++ b/spec/ruby/language/hash_spec.rb @@ -1,7 +1,7 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/hash_strings_ascii8bit', __FILE__) -require File.expand_path('../fixtures/hash_strings_utf8', __FILE__) -require File.expand_path('../fixtures/hash_strings_usascii', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/hash_strings_binary' +require_relative 'fixtures/hash_strings_utf8' +require_relative 'fixtures/hash_strings_usascii' describe "Hash literal" do it "{} should return an empty hash" do @@ -33,12 +33,12 @@ 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" h.keys.first.should == "foo" - h.keys.first.frozen?.should == true + h.keys.first.should.frozen? key.should == "oof" end @@ -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 - lambda { eval("{:a ==> 1}") }.should raise_error(SyntaxError) + -> { eval("{:a ==> 1}") }.should raise_error(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_error(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_error(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,44 @@ 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_error(TypeError, "no implicit conversion of nil into Hash") + + -> { {a: 1, **nil} }.should raise_error(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 + 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,27 +203,122 @@ describe "Hash literal" do {a: 1, **obj, c: 3}.should == {a:1, b: 2, c: 3, d: 4} end - it "raises a TypeError if any splatted elements keys are not symbols" do + it "allows splatted elements keys that are not symbols" do h = {1 => 2, b: 3} - lambda { {a: 1, **h} }.should raise_error(TypeError) + {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) - lambda { {**obj} }.should raise_error(TypeError) + -> { {**obj} }.should raise_error(TypeError) + end + + it "raises a TypeError if the object does not respond to #to_hash" do + obj = 42 + -> { {**obj} }.should raise_error(TypeError) + -> { {a: 1, **obj} }.should raise_error(TypeError) end it "does not change encoding of literal string keys during creation" do - ascii8bit_hash = HashStringsASCII8BIT.literal_hash + binary_hash = HashStringsBinary.literal_hash utf8_hash = HashStringsUTF8.literal_hash usascii_hash = HashStringsUSASCII.literal_hash - ascii8bit_hash.keys.first.encoding.should == Encoding::ASCII_8BIT - ascii8bit_hash.keys.first.should == utf8_hash.keys.first + binary_hash.keys.first.encoding.should == Encoding::BINARY + binary_hash.keys.first.should == utf8_hash.keys.first utf8_hash.keys.first.encoding.should == Encoding::UTF_8 utf8_hash.keys.first.should == usascii_hash.keys.first usascii_hash.keys.first.encoding.should == Encoding::US_ASCII end + + ruby_bug "#20280", ""..."3.4" do + it "raises a SyntaxError at parse time when Symbol key with invalid bytes" do + ScratchPad.record [] + -> { + eval 'ScratchPad << 1; {:"\xC3" => 1}' + }.should raise_error(SyntaxError, /invalid symbol/) + ScratchPad.recorded.should == [] + end + + it "raises a SyntaxError at parse time when Symbol key with invalid bytes and 'key: value' syntax used" do + ScratchPad.record [] + -> { + eval 'ScratchPad << 1; {"\xC3": 1}' + }.should raise_error(SyntaxError, /invalid symbol/) + ScratchPad.recorded.should == [] + end + end +end + +describe "The ** operator" do + 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 + + ruby_bug "#20012", ""..."3.3" do + it "makes a copy when calling a method taking a positional Hash" do + def m(h) + h.delete(:one); h + end + + h = { one: 1, two: 2 } + m(**h).should == { two: 2 } + m(**h).should_not.equal?(h) + h.should == { one: 1, two: 2 } + end + 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_error(SyntaxError, /identifier a! is not valid to get/) + end + + it "raises a SyntaxError when the hash key ends with `?`" do + -> { eval("{a?:}") }.should raise_error(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 a57a7b0bb9..47ee9c2c51 100644 --- a/spec/ruby/language/heredoc_spec.rb +++ b/spec/ruby/language/heredoc_spec.rb @@ -1,6 +1,6 @@ # -*- encoding: us-ascii -*- -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "Heredoc string" do @@ -13,6 +13,7 @@ describe "Heredoc string" do foo bar#{@ip} HERE s.should == "foo barxxx\n" + s.encoding.should == Encoding::US_ASCII end it 'allow HEREDOC with <<"identifier", interpolated' do @@ -20,6 +21,7 @@ HERE foo bar#{@ip} HERE s.should == "foo barxxx\n" + s.encoding.should == Encoding::US_ASCII end it "allows HEREDOC with <<'identifier', no interpolation" do @@ -27,6 +29,7 @@ HERE foo bar#{@ip} HERE s.should == 'foo bar#{@ip}' + "\n" + s.encoding.should == Encoding::US_ASCII end it "allows HEREDOC with <<-identifier, allowing to indent identifier, interpolated" do @@ -35,6 +38,7 @@ HERE HERE s.should == " foo barxxx\n" + s.encoding.should == Encoding::US_ASCII end it 'allows HEREDOC with <<-"identifier", allowing to indent identifier, interpolated' do @@ -43,6 +47,7 @@ HERE HERE s.should == " foo barxxx\n" + s.encoding.should == Encoding::US_ASCII end it "allows HEREDOC with <<-'identifier', allowing to indent identifier, no interpolation" do @@ -51,37 +56,64 @@ HERE HERE s.should == ' foo bar#{@ip}' + "\n" + s.encoding.should == Encoding::US_ASCII end - ruby_version_is "2.3" do - it "allows HEREDOC with <<~'identifier', allowing to indent identifier and content" do - require File.expand_path('../fixtures/squiggly_heredoc', __FILE__) - SquigglyHeredocSpecs.message.should == "character density, n.:\n The number of very weird people in the office.\n" - end - - it "trims trailing newline character for blank HEREDOC with <<~'identifier'" do - require File.expand_path('../fixtures/squiggly_heredoc', __FILE__) - SquigglyHeredocSpecs.blank.should == "" - end - - it 'allows HEREDOC with <<~identifier, interpolated' do - require File.expand_path('../fixtures/squiggly_heredoc', __FILE__) - SquigglyHeredocSpecs.unquoted.should == "unquoted interpolated\n" - end - - it 'allows HEREDOC with <<~"identifier", interpolated' do - require File.expand_path('../fixtures/squiggly_heredoc', __FILE__) - SquigglyHeredocSpecs.doublequoted.should == "doublequoted interpolated\n" - end - - it "allows HEREDOC with <<~'identifier', no interpolation" do - require File.expand_path('../fixtures/squiggly_heredoc', __FILE__) - SquigglyHeredocSpecs.singlequoted.should == "singlequoted \#{\"interpolated\"}\n" - end - - it "selects the least-indented line and removes its indentation from all the lines" do - require File.expand_path('../fixtures/squiggly_heredoc', __FILE__) - SquigglyHeredocSpecs.least_indented_on_the_last_line.should == " a\n b\nc\n" - end + it 'raises SyntaxError if quoted HEREDOC identifier is ending not on same line' do + -> { + eval %{<<"HERE\n"\nraises syntax error\nHERE} + }.should raise_error(SyntaxError) + end + + it "allows HEREDOC with <<~'identifier', allowing to indent identifier and content" do + require_relative 'fixtures/squiggly_heredoc' + SquigglyHeredocSpecs.message.should == "character density, n.:\n The number of very weird people in the office.\n" + end + + it "trims trailing newline character for blank HEREDOC with <<~'identifier'" do + require_relative 'fixtures/squiggly_heredoc' + SquigglyHeredocSpecs.blank.should == "" + end + + it 'allows HEREDOC with <<~identifier, interpolated' do + require_relative 'fixtures/squiggly_heredoc' + SquigglyHeredocSpecs.unquoted.should == "unquoted interpolated\n" + end + + it 'allows HEREDOC with <<~"identifier", interpolated' do + require_relative 'fixtures/squiggly_heredoc' + SquigglyHeredocSpecs.doublequoted.should == "doublequoted interpolated\n" + end + + it "allows HEREDOC with <<~'identifier', no interpolation" do + require_relative 'fixtures/squiggly_heredoc' + 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" + SquigglyHeredocSpecs.least_indented_on_the_last_line.should == " a\n b\nc\n" + end + + it "selects the least-indented line and removes its indentation from all the lines for <<~'identifier'" do + require_relative 'fixtures/squiggly_heredoc' + 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_error(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 284d852462..2d1a89f081 100644 --- a/spec/ruby/language/if_spec.rb +++ b/spec/ruby/language/if_spec.rb @@ -1,22 +1,20 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "The if expression" do - ruby_version_is '2.4' do - describe "accepts multiple assignments in conditional expression" do - before(:each) { ScratchPad.record([]) } - after(:each) { ScratchPad.clear } - - it 'with non-nil values' do - ary = [1, 2] - eval "if (a, b = ary); ScratchPad.record [a, b]; end" - ScratchPad.recorded.should == [1, 2] - end + describe "accepts multiple assignments in conditional expression" do + before(:each) { ScratchPad.record([]) } + after(:each) { ScratchPad.clear } + + it 'with non-nil values' do + ary = [1, 2] + eval "if (a, b = ary); ScratchPad.record [a, b]; end" + ScratchPad.recorded.should == [1, 2] + end - it 'with nil values' do - ary = nil - eval "if (a, b = ary); else; ScratchPad.record [a, b]; end" - ScratchPad.recorded.should == [nil, nil] - end + it 'with nil values' do + ary = nil + eval "if (a, b = ary); else; ScratchPad.record [a, b]; end" + ScratchPad.recorded.should == [nil, nil] end end @@ -307,6 +305,59 @@ describe "The if expression" do 6.times(&b) ScratchPad.recorded.should == [4, 5, 4, 5] end + + it "warns when Integer literals are used instead of predicates" do + -> { + eval <<~RUBY + $. = 0 + 10.times { |i| ScratchPad << i if 4..5 } + RUBY + }.should complain(/warning: integer literal in flip-flop/, verbose: true) + ScratchPad.recorded.should == [] + end + end + + describe "when a branch syntactically does not return a value" do + it "raises SyntaxError if both do not return a value" do + -> { + eval <<~RUBY + def m + a = if rand + return + else + return + end + a + end + RUBY + }.should raise_error(SyntaxError, /void value expression/) + end + + it "does not raise SyntaxError if one branch returns a value" do + eval(<<~RUBY).should == 1 + def m + a = if false # using false to make it clear that's not checked for + 42 + else + return 1 + end + a + end + m + RUBY + + eval(<<~RUBY).should == 1 + def m + a = if true # using true to make it clear that's not checked for + return 1 + else + 42 + end + a + end + m + RUBY + end end end diff --git a/spec/ruby/language/it_parameter_spec.rb b/spec/ruby/language/it_parameter_spec.rb new file mode 100644 index 0000000000..72023180d9 --- /dev/null +++ b/spec/ruby/language/it_parameter_spec.rb @@ -0,0 +1,66 @@ +require_relative '../spec_helper' + +ruby_version_is "3.4" do + 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 "is a regular local variable if there is already a 'it' local variable" do + it = 0 + proc { it }.call("a").should == 0 + end + + it "raises SyntaxError when block parameters are specified explicitly" do + -> { eval("-> () { it }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + -> { eval("-> (x) { it }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + + -> { eval("proc { || it }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + -> { eval("proc { |x| it }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + + -> { eval("lambda { || it }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + -> { eval("lambda { |x| it }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + + -> { eval("['a'].map { || it }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + -> { eval("['a'].map { |x| it }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + 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_error(ArgumentError, /wrong number of arguments/) + end + end +end diff --git a/spec/ruby/language/keyword_arguments_spec.rb b/spec/ruby/language/keyword_arguments_spec.rb new file mode 100644 index 0000000000..4f6370d419 --- /dev/null +++ b/spec/ruby/language/keyword_arguments_spec.rb @@ -0,0 +1,386 @@ +require_relative '../spec_helper' + +describe "Keyword arguments" do + def target(*args, **kwargs) + [args, kwargs] + end + + it "are separated from positional arguments" do + def m(*args, **kwargs) + [args, kwargs] + end + + empty = {} + m(**empty).should == [[], {}] + m(empty).should == [[{}], {}] + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + end + + it "when the receiving method has not keyword parameters it treats kwargs as positional" do + def m(*a) + a + end + + m(a: 1).should == [{a: 1}] + m({a: 1}).should == [{a: 1}] + end + + context "empty kwargs are treated as if they were not passed" do + it "when calling a method" do + def m(*a) + a + end + + empty = {} + m(**empty).should == [] + m(empty).should == [{}] + end + + it "when yielding to a block" do + def y(*args, **kwargs) + yield(*args, **kwargs) + end + + empty = {} + y(**empty) { |*a| a }.should == [] + y(empty) { |*a| a }.should == [{}] + end + end + + it "extra keywords are not allowed without **kwrest" do + def m(*a, kw:) + a + end + + m(kw: 1).should == [] + -> { m(kw: 1, kw2: 2) }.should raise_error(ArgumentError, 'unknown keyword: :kw2') + -> { m(kw: 1, true => false) }.should raise_error(ArgumentError, 'unknown keyword: true') + -> { m(kw: 1, a: 1, b: 2, c: 3) }.should raise_error(ArgumentError, 'unknown keywords: :a, :b, :c') + end + + it "raises ArgumentError exception when required keyword argument is not passed" do + def m(a:, b:, c:) + [a, b, c] + end + + -> { m(a: 1, b: 2) }.should raise_error(ArgumentError, /missing keyword: :c/) + -> { m() }.should raise_error(ArgumentError, /missing keywords: :a, :b, :c/) + end + + it "raises ArgumentError for missing keyword arguments even if there are extra ones" do + def m(a:) + a + end + + -> { m(b: 1) }.should raise_error(ArgumentError, /missing keyword: :a/) + end + + it "handle * and ** at the same call site" do + def m(*a) + a + end + + m(*[], **{}).should == [] + m(*[], 42, **{}).should == [42] + end + + context "**" do + ruby_version_is "3.3" do + it "copies a non-empty Hash for a method taking (*args)" do + def m(*args) + args[0] + end + + h = {a: 1} + m(**h).should_not.equal?(h) + h.should == {a: 1} + end + end + + it "copies the given Hash for a method taking (**kwargs)" do + def m(**kw) + kw + end + + empty = {} + m(**empty).should == empty + m(**empty).should_not.equal?(empty) + + h = {a: 1} + m(**h).should == h + m(**h).should_not.equal?(h) + end + end + + context "delegation" do + it "works with (*args, **kwargs)" do + def m(*args, **kwargs) + target(*args, **kwargs) + end + + empty = {} + m(**empty).should == [[], {}] + m(empty).should == [[{}], {}] + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + end + + it "works with proc { |*args, **kwargs| }" do + m = proc do |*args, **kwargs| + target(*args, **kwargs) + end + + empty = {} + m.(**empty).should == [[], {}] + m.(empty).should == [[{}], {}] + + m.(a: 1).should == [[], {a: 1}] + m.({a: 1}).should == [[{a: 1}], {}] + + # no autosplatting for |*args, **kwargs| + m.([1, 2]).should == [[[1, 2]], {}] + end + + it "works with -> (*args, **kwargs) {}" do + m = -> *args, **kwargs do + target(*args, **kwargs) + end + + empty = {} + m.(**empty).should == [[], {}] + m.(empty).should == [[{}], {}] + + m.(a: 1).should == [[], {a: 1}] + m.({a: 1}).should == [[{a: 1}], {}] + end + + it "works with (...)" do + instance_eval <<~DEF + def m(...) + target(...) + end + DEF + + empty = {} + m(**empty).should == [[], {}] + m(empty).should == [[{}], {}] + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + end + + it "works with call(*ruby2_keyword_args)" do + class << self + ruby2_keywords def m(*args) + target(*args) + end + end + + empty = {} + m(**empty).should == [[], {}] + Hash.ruby2_keywords_hash?(empty).should == false + m(empty).should == [[{}], {}] + Hash.ruby2_keywords_hash?(empty).should == false + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + + kw = {a: 1} + + m(**kw).should == [[], {a: 1}] + m(**kw)[1].should == kw + m(**kw)[1].should_not.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + Hash.ruby2_keywords_hash?(m(**kw)[1]).should == false + + m(kw).should == [[{a: 1}], {}] + m(kw)[0][0].should.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + end + + it "works with super(*ruby2_keyword_args)" do + parent = Class.new do + def m(*args, **kwargs) + [args, kwargs] + end + end + + child = Class.new(parent) do + ruby2_keywords def m(*args) + super(*args) + end + end + + obj = child.new + + empty = {} + obj.m(**empty).should == [[], {}] + Hash.ruby2_keywords_hash?(empty).should == false + obj.m(empty).should == [[{}], {}] + Hash.ruby2_keywords_hash?(empty).should == false + + obj.m(a: 1).should == [[], {a: 1}] + obj.m({a: 1}).should == [[{a: 1}], {}] + + kw = {a: 1} + + obj.m(**kw).should == [[], {a: 1}] + obj.m(**kw)[1].should == kw + obj.m(**kw)[1].should_not.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + Hash.ruby2_keywords_hash?(obj.m(**kw)[1]).should == false + + obj.m(kw).should == [[{a: 1}], {}] + obj.m(kw)[0][0].should.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + end + + it "works with zsuper" do + parent = Class.new do + def m(*args, **kwargs) + [args, kwargs] + end + end + + child = Class.new(parent) do + ruby2_keywords def m(*args) + super + end + end + + obj = child.new + + empty = {} + obj.m(**empty).should == [[], {}] + Hash.ruby2_keywords_hash?(empty).should == false + obj.m(empty).should == [[{}], {}] + Hash.ruby2_keywords_hash?(empty).should == false + + obj.m(a: 1).should == [[], {a: 1}] + obj.m({a: 1}).should == [[{a: 1}], {}] + + kw = {a: 1} + + obj.m(**kw).should == [[], {a: 1}] + obj.m(**kw)[1].should == kw + obj.m(**kw)[1].should_not.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + Hash.ruby2_keywords_hash?(obj.m(**kw)[1]).should == false + + obj.m(kw).should == [[{a: 1}], {}] + obj.m(kw)[0][0].should.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + end + + it "works with yield(*ruby2_keyword_args)" do + class << self + def y(args) + yield(*args) + end + + ruby2_keywords def m(*outer_args) + y(outer_args, &-> *args, **kwargs { target(*args, **kwargs) }) + end + end + + empty = {} + m(**empty).should == [[], {}] + Hash.ruby2_keywords_hash?(empty).should == false + m(empty).should == [[{}], {}] + Hash.ruby2_keywords_hash?(empty).should == false + + m(a: 1).should == [[], {a: 1}] + m({a: 1}).should == [[{a: 1}], {}] + + kw = {a: 1} + + m(**kw).should == [[], {a: 1}] + m(**kw)[1].should == kw + m(**kw)[1].should_not.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + Hash.ruby2_keywords_hash?(m(**kw)[1]).should == false + + m(kw).should == [[{a: 1}], {}] + m(kw)[0][0].should.equal?(kw) + Hash.ruby2_keywords_hash?(kw).should == false + end + + it "does not work with (*args)" do + class << self + def m(*args) + target(*args) + end + end + + empty = {} + m(**empty).should == [[], {}] + m(empty).should == [[{}], {}] + + m(a: 1).should == [[{a: 1}], {}] + m({a: 1}).should == [[{a: 1}], {}] + end + + 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 43e2d60ae3..ed5a1c69e8 100644 --- a/spec/ruby/language/lambda_spec.rb +++ b/spec/ruby/language/lambda_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/classes' describe "A lambda literal -> () { }" do SpecEvaluate.desc = "for definition" @@ -7,7 +7,7 @@ describe "A lambda literal -> () { }" do it "returns a Proc object when used in a BasicObject method" do klass = Class.new(BasicObject) do def create_lambda - -> () { } + -> { } end end @@ -15,11 +15,19 @@ describe "A lambda literal -> () { }" do end it "does not execute the block" do - ->() { fail }.should be_an_instance_of(Proc) + -> { fail }.should be_an_instance_of(Proc) end it "returns a lambda" do - -> () { }.lambda?.should be_true + -> { }.lambda?.should be_true + end + + it "may include a rescue clause" do + eval('-> do raise ArgumentError; rescue ArgumentError; 7; end').should be_an_instance_of(Proc) + end + + it "may include a ensure clause" do + eval('-> do 1; ensure; 2; end').should be_an_instance_of(Proc) end it "has its own scope for local variables" do @@ -101,7 +109,7 @@ describe "A lambda literal -> () { }" do @a = -> (a:) { a } ruby - lambda { @a.() }.should raise_error(ArgumentError) + -> { @a.() }.should raise_error(ArgumentError) @a.(a: 1).should == 1 end @@ -119,7 +127,7 @@ describe "A lambda literal -> () { }" do @a.().should be_nil @a.(a: 1, b: 2).should be_nil - lambda { @a.(1) }.should raise_error(ArgumentError) + -> { @a.(1) }.should raise_error(ArgumentError) end evaluate <<-ruby do @@ -143,8 +151,8 @@ describe "A lambda literal -> () { }" do ruby @a.(1, 2).should == [1, 2] - lambda { @a.() }.should raise_error(ArgumentError) - lambda { @a.(1) }.should raise_error(ArgumentError) + -> { @a.() }.should raise_error(ArgumentError) + -> { @a.(1) }.should raise_error(ArgumentError) end evaluate <<-ruby do @@ -177,8 +185,8 @@ describe "A lambda literal -> () { }" do @a.(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} h = mock("keyword splat") - h.should_receive(:to_hash).and_return({a: 1}) - @a.(h).should == {a: 1} + h.should_not_receive(:to_hash) + @a.(h).should == {} end evaluate <<-ruby do @@ -255,25 +263,26 @@ describe "A lambda literal -> () { }" do end describe "with circular optional argument reference" do - it "shadows an existing local with the same name as the argument" do - a = 1 - -> { - @proc = eval "-> (a=a) { a }" - }.should complain(/circular argument reference/) - @proc.call.should == nil + ruby_version_is ""..."3.4" do + it "raises a SyntaxError if using the argument in its default value" do + a = 1 + -> { + eval "-> (a=a) { a }" + }.should raise_error(SyntaxError) + end end - it "shadows an existing method with the same name as the argument" do - def a; 1; end - -> { - @proc = eval "-> (a=a) { a }" - }.should complain(/circular argument reference/) - @proc.call.should == nil + ruby_version_is "3.4" do + it "is nil if using the argument in its default value" do + -> { + eval "-> (a=a) { a }.call" + }.call.should == nil + end end it "calls an existing method with the same name as the argument if explicitly using ()" do def a; 1; end - -> (a=a()) { a }.call.should == 1 + -> a=a() { a }.call.should == 1 end end end @@ -302,7 +311,13 @@ describe "A lambda expression 'lambda { ... }'" do end it "requires a block" do - lambda { lambda }.should raise_error(ArgumentError) + suppress_warning do + lambda { lambda }.should raise_error(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) end context "with an implicit block" do @@ -310,14 +325,13 @@ describe "A lambda expression 'lambda { ... }'" do def meth; lambda; end end - it "can be created" do + it "raises ArgumentError" 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 + suppress_warning do + -> { + meth { 1 } + }.should raise_error(ArgumentError, /tried to create Proc object without a block/) + end end end @@ -493,8 +507,8 @@ describe "A lambda expression 'lambda { ... }'" do @a.(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} h = mock("keyword splat") - h.should_receive(:to_hash).and_return({a: 1}) - @a.(h).should == {a: 1} + h.should_not_receive(:to_hash) + @a.(h).should == {} end evaluate <<-ruby do diff --git a/spec/ruby/language/line_spec.rb b/spec/ruby/language/line_spec.rb index d9fd307dab..fcadaa71d7 100644 --- a/spec/ruby/language/line_spec.rb +++ b/spec/ruby/language/line_spec.rb @@ -1,10 +1,10 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/code_loading', __FILE__) -require File.expand_path('../shared/__LINE__', __FILE__) +require_relative '../spec_helper' +require_relative '../fixtures/code_loading' +require_relative 'shared/__LINE__' describe "The __LINE__ pseudo-variable" do it "raises a SyntaxError if assigned to" do - lambda { eval("__LINE__ = 1") }.should raise_error(SyntaxError) + -> { eval("__LINE__ = 1") }.should raise_error(SyntaxError) end before :each do diff --git a/spec/ruby/language/loop_spec.rb b/spec/ruby/language/loop_spec.rb index 4e60e0d8e6..fd17b53910 100644 --- a/spec/ruby/language/loop_spec.rb +++ b/spec/ruby/language/loop_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "The loop expression" do it "repeats the given block until a break is called" do @@ -15,7 +15,7 @@ describe "The loop expression" do inner_loop = 123 break end - lambda { inner_loop }.should raise_error(NameError) + -> { inner_loop }.should raise_error(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 2f6e3b5c3a..af9c9dbfd0 100644 --- a/spec/ruby/language/magic_comment_spec.rb +++ b/spec/ruby/language/magic_comment_spec.rb @@ -1,62 +1,93 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' -describe "Magic comment" do - it "is optional" do - eval("__ENCODING__").should be_an_instance_of(Encoding) +# See core/kernel/eval_spec.rb for more magic comments specs for eval() +describe :magic_comments, shared: true do + before :each do + @default = @method == :locale ? Encoding.find('locale') : Encoding::UTF_8 end - it "determines __ENCODING__" do - eval(<<EOS.force_encoding("US-ASCII")).should == Encoding::ASCII_8BIT -# encoding: ASCII-8BIT -__ENCODING__ -EOS + it "are optional" do + @object.call('no_magic_comment.rb').should == @default.name end - it "is case-insensitive" do - eval(<<EOS.force_encoding("US-ASCII")).should == Encoding::ASCII_8BIT -# CoDiNg: aScIi-8bIt -__ENCODING__ -EOS + it "are case-insensitive" do + @object.call('case_magic_comment.rb').should == Encoding::Big5.name end it "must be at the first line" do - eval(<<EOS.force_encoding("US-ASCII")).should == Encoding::US_ASCII - -# encoding: ASCII-8BIT -__ENCODING__ -EOS + @object.call('second_line_magic_comment.rb').should == @default.name end it "must be the first token of the line" do - eval(<<EOS.force_encoding("US-ASCII")).should == Encoding::US_ASCII -1+1 # encoding: ASCII-8BIT -__ENCODING__ -EOS - eval(<<EOS.force_encoding("US-ASCII")).should == Encoding::ASCII_8BIT - # encoding: ASCII-8BIT -__ENCODING__ -EOS + @object.call('second_token_magic_comment.rb').should == @default.name end it "can be after the shebang" do - eval(<<EOS.force_encoding("US-ASCII")).should == Encoding::ASCII_8BIT -#!/usr/bin/ruby -Ku -# encoding: ASCII-8BIT -__ENCODING__ -EOS + @object.call('shebang_magic_comment.rb').should == Encoding::Big5.name end it "can take Emacs style" do - eval(<<EOS.force_encoding("US-ASCII")).should == Encoding::ASCII_8BIT -# -*- encoding: ascii-8bit -*- -__ENCODING__ -EOS + @object.call('emacs_magic_comment.rb').should == Encoding::Big5.name end it "can take vim style" do - eval(<<EOS.force_encoding("US-ASCII")).should == Encoding::ASCII_8BIT -# vim: filetype=ruby, fileencoding=ascii-8bit, tabsize=3, shiftwidth=3 -__ENCODING__ -EOS + @object.call('vim_magic_comment.rb').should == Encoding::Big5.name + end + + it "determine __ENCODING__" do + @object.call('magic_comment.rb').should == Encoding::Big5.name + end + + it "do not cause bytes to be mangled by passing them through the wrong encoding" do + @object.call('bytes_magic_comment.rb').should == [167, 65, 166, 110].inspect + end +end + +describe "Magic comments" do + describe "in stdin" do + 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}") + } + end + + platform_is_not :windows do + describe "in an -e argument" do + it_behaves_like :magic_comments, :locale, -> file { + print_at_exit = fixture(__FILE__, "print_magic_comment_result_at_exit.rb") + # Use UTF-8, as it is the default source encoding for files + code = File.read(fixture(__FILE__, file), encoding: 'utf-8') + IO.popen([*ruby_exe, "-r", print_at_exit, "-e", code], &:read) + } + end + end + + describe "in the main file" do + it_behaves_like :magic_comments, :UTF8, -> file { + print_at_exit = fixture(__FILE__, "print_magic_comment_result_at_exit.rb") + ruby_exe(fixture(__FILE__, file), options: "-r#{print_at_exit}") + } + end + + describe "in a loaded file" do + it_behaves_like :magic_comments, :UTF8, -> file { + load fixture(__FILE__, file) + $magic_comment_result + } + end + + describe "in a required file" do + it_behaves_like :magic_comments, :UTF8, -> file { + require fixture(__FILE__, file) + $magic_comment_result + } + end + + describe "in an eval" do + it_behaves_like :magic_comments, :UTF8, -> file { + # Use UTF-8, as it is the default source encoding for files + eval(File.read(fixture(__FILE__, file), encoding: 'utf-8')) + } end end diff --git a/spec/ruby/language/match_spec.rb b/spec/ruby/language/match_spec.rb index 81604e94b2..ebf677cabc 100644 --- a/spec/ruby/language/match_spec.rb +++ b/spec/ruby/language/match_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/match_operators', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/match_operators' describe "The !~ operator" do before :each do @@ -48,6 +48,13 @@ describe "The =~ operator with named captures" do end end + describe "on syntax of 'string_literal' =~ /regexp/" do + it "does not set local variables" do + 'string literal' =~ /(?<matched>str)(?<unmatched>lit)?/ + local_variables.should == [] + end + end + describe "on syntax of string_variable =~ /regexp/" do it "does not set local variables" do @string =~ /(?<matched>foo)(?<unmatched>bar)?/ diff --git a/spec/ruby/language/metaclass_spec.rb b/spec/ruby/language/metaclass_spec.rb index b3bcd9ef18..fc83067977 100644 --- a/spec/ruby/language/metaclass_spec.rb +++ b/spec/ruby/language/metaclass_spec.rb @@ -1,6 +1,6 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/class', __FILE__) -require File.expand_path('../fixtures/metaclass', __FILE__) +require_relative '../spec_helper' +require_relative '../fixtures/class' +require_relative 'fixtures/metaclass' describe "self in a metaclass body (class << obj)" do it "is TrueClass for true" do @@ -16,11 +16,11 @@ describe "self in a metaclass body (class << obj)" do end it "raises a TypeError for numbers" do - lambda { class << 1; self; end }.should raise_error(TypeError) + -> { class << 1; self; end }.should raise_error(TypeError) end it "raises a TypeError for symbols" do - lambda { class << :symbol; self; end }.should raise_error(TypeError) + -> { class << :symbol; self; end }.should raise_error(TypeError) end it "is a singleton Class instance" do @@ -64,11 +64,11 @@ describe "A constant on a metaclass" do class << @object CONST end - lambda { CONST }.should raise_error(NameError) + -> { CONST }.should raise_error(NameError) end it "cannot be accessed via object::CONST" do - lambda do + -> do @object::CONST end.should raise_error(TypeError) end @@ -79,7 +79,7 @@ describe "A constant on a metaclass" do CONST = 100 end - lambda do + -> do @object::CONST end.should raise_error(NameError) end @@ -96,7 +96,7 @@ describe "A constant on a metaclass" do it "is not preserved when the object is duped" do @object = @object.dup - lambda do + -> do class << @object; CONST; end end.should raise_error(NameError) end diff --git a/spec/ruby/language/method_spec.rb b/spec/ruby/language/method_spec.rb index ca939dbab6..8f72bd45ed 100644 --- a/spec/ruby/language/method_spec.rb +++ b/spec/ruby/language/method_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "A method send" do evaluate <<-ruby do @@ -40,7 +40,7 @@ describe "A method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - lambda { m(*x) }.should raise_error(TypeError) + -> { m(*x) }.should raise_error(TypeError) end end @@ -74,7 +74,7 @@ describe "A method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - lambda { m(*x, 2, 3) }.should raise_error(TypeError) + -> { m(*x, 2, 3) }.should raise_error(TypeError) end end @@ -108,7 +108,7 @@ describe "A method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - lambda { m(1, *x, 2, 3) }.should raise_error(TypeError) + -> { m(1, *x, 2, 3) }.should raise_error(TypeError) end it "copies the splatted array" do @@ -153,7 +153,27 @@ describe "A method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - lambda { m(1, 2, *x) }.should raise_error(TypeError) + -> { m(1, 2, *x) }.should raise_error(TypeError) + end + end + + context "with a block argument" do + before :all do + def m(x) + if block_given? + [true, yield(x + 'b')] + else + [false] + end + end + end + + it "that refers to a proc passes the proc as the block" do + m('a', &-> y { y + 'c'}).should == [true, 'abc'] + end + + it "that is nil passes no block" do + m('a', &nil).should == [false] end end end @@ -197,7 +217,7 @@ describe "An element assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - lambda { @o[*x] = 1 }.should raise_error(TypeError) + -> { @o[*x] = 1 }.should raise_error(TypeError) end end @@ -235,7 +255,7 @@ describe "An element assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - lambda { @o[*x, 2, 3] = 4 }.should raise_error(TypeError) + -> { @o[*x, 2, 3] = 4 }.should raise_error(TypeError) end end @@ -273,7 +293,7 @@ describe "An element assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - lambda { @o[1, 2, *x, 3] = 4 }.should raise_error(TypeError) + -> { @o[1, 2, *x, 3] = 4 }.should raise_error(TypeError) end end @@ -311,7 +331,7 @@ describe "An element assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - lambda { @o[1, 2, 3, *x] = 4 }.should raise_error(TypeError) + -> { @o[1, 2, 3, *x] = 4 }.should raise_error(TypeError) end end end @@ -348,7 +368,7 @@ describe "An attribute assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - lambda { @o.send :m=, *x, 1 }.should raise_error(TypeError) + -> { @o.send :m=, *x, 1 }.should raise_error(TypeError) end end @@ -383,7 +403,7 @@ describe "An attribute assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - lambda { @o.send :m=, *x, 2, 3, 4 }.should raise_error(TypeError) + -> { @o.send :m=, *x, 2, 3, 4 }.should raise_error(TypeError) end end @@ -418,7 +438,7 @@ describe "An attribute assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - lambda { @o.send :m=, 1, 2, *x, 3, 4 }.should raise_error(TypeError) + -> { @o.send :m=, 1, 2, *x, 3, 4 }.should raise_error(TypeError) end end @@ -453,7 +473,7 @@ describe "An attribute assignment method send" do x = mock("splat argument") x.should_receive(:to_a).and_return(1) - lambda { @o.send :m=, 1, 2, 3, *x, 4 }.should raise_error(TypeError) + -> { @o.send :m=, 1, 2, 3, *x, 4 }.should raise_error(TypeError) end end end @@ -512,6 +532,15 @@ describe "A method" do end evaluate <<-ruby do + def m() end + ruby + + m().should be_nil + m(*[]).should be_nil + m(**{}).should be_nil + end + + evaluate <<-ruby do def m(*) end ruby @@ -527,15 +556,26 @@ describe "A method" do m().should == [] m(1).should == [1] m(1, 2, 3).should == [1, 2, 3] + m(*[]).should == [] + m(**{}).should == [] end evaluate <<-ruby do def m(a:) a end ruby - lambda { m() }.should raise_error(ArgumentError) + -> { m() }.should raise_error(ArgumentError) m(a: 1).should == 1 - lambda { m("a" => 1, a: 1) }.should raise_error(ArgumentError) + suppress_keyword_warning do + -> { m("a" => 1, a: 1) }.should raise_error(ArgumentError) + end + end + + evaluate <<-ruby do + def m(a:, **kw) [a, kw] end + ruby + + -> { m(b: 1) }.should raise_error(ArgumentError) end evaluate <<-ruby do @@ -552,7 +592,7 @@ describe "A method" do m().should be_nil m(a: 1, b: 2).should be_nil - lambda { m(1) }.should raise_error(ArgumentError) + -> { m(1) }.should raise_error(ArgumentError) end evaluate <<-ruby do @@ -561,7 +601,19 @@ describe "A method" do m().should == {} m(a: 1, b: 2).should == { a: 1, b: 2 } - lambda { m(2) }.should raise_error(ArgumentError) + m(*[]).should == {} + m(**{}).should == {} + 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) + end + + evaluate <<-ruby do + def m(**k); k end; + ruby + + m("a" => 1).should == { "a" => 1 } end evaluate <<-ruby do @@ -600,7 +652,7 @@ describe "A method" do m(2, 3).should be_nil m([2, 3, 4], [5, 6]).should be_nil - lambda { m a: 1 }.should raise_error(ArgumentError) + -> { m a: 1 }.should raise_error(ArgumentError) end evaluate <<-ruby do @@ -692,7 +744,9 @@ describe "A method" do ruby m(1, b: 2).should == [1, 2] - lambda { m("a" => 1, b: 2) }.should raise_error(ArgumentError) + suppress_keyword_warning do + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) + end end evaluate <<-ruby do @@ -701,7 +755,7 @@ describe "A method" do m(2).should == [2, 1] m(1, b: 2).should == [1, 2] - m("a" => 1, b: 2).should == [{"a" => 1, b: 2}, 1] + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) end evaluate <<-ruby do @@ -710,7 +764,7 @@ describe "A method" do m(1).should == 1 m(1, a: 2, b: 3).should == 1 - m("a" => 1, b: 2).should == {"a" => 1, b: 2} + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) end evaluate <<-ruby do @@ -719,7 +773,7 @@ describe "A method" do m(1).should == [1, {}] m(1, a: 2, b: 3).should == [1, {a: 2, b: 3}] - m("a" => 1, b: 2).should == [{"a" => 1, b: 2}, {}] + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) end evaluate <<-ruby do @@ -794,8 +848,8 @@ describe "A method" do def m(a=1, (*b), (*c)) [a, b, c] end ruby - lambda { m() }.should raise_error(ArgumentError) - lambda { m(2) }.should raise_error(ArgumentError) + -> { m() }.should raise_error(ArgumentError) + -> { m(2) }.should raise_error(ArgumentError) m(2, 3).should == [1, [2], [3]] m(2, [3, 4], [5, 6]).should == [2, [3, 4], [5, 6]] end @@ -836,7 +890,7 @@ describe "A method" do m(b: 2).should == [1, 2] m(2, b: 1).should == [2, 1] - m("a" => 1, b: 2).should == [{"a" => 1}, 2] + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) end evaluate <<-ruby do @@ -846,7 +900,7 @@ describe "A method" do m().should == [1, 2] m(2).should == [2, 2] m(b: 3).should == [1, 3] - m("a" => 1, b: 2).should == [{"a" => 1}, 2] + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) end evaluate <<-ruby do @@ -855,7 +909,7 @@ describe "A method" do m().should == 1 m(2, a: 1, b: 0).should == 2 - m("a" => 1, a: 2).should == {"a" => 1} + m("a" => 1, a: 2).should == 1 end evaluate <<-ruby do @@ -896,151 +950,6 @@ describe "A method" do end evaluate <<-ruby do - def m(*, a:) a end - ruby - - m(a: 1).should == 1 - m(1, 2, a: 3).should == 3 - m("a" => 1, a: 2).should == 2 - 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] - m("a" => 1, b: 2).should == [[{"a" => 1}], 2] - 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 - m("a" => 1, a: 2).should == 2 - 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] - m("a" => 1, b: 2).should == [[{"a" => 1}], 2] - - 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}) - m(h).should be_nil - - h = mock("keyword splat") - error = RuntimeError.new("error while converting to a hash") - h.should_receive(:to_hash).and_raise(error) - lambda { 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) - lambda { 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 - - evaluate <<-ruby do def m(*, &b) b end ruby @@ -1063,7 +972,9 @@ describe "A method" do ruby m(a: 1, b: 2).should == [1, 2] - lambda { m("a" => 1, a: 1, b: 2) }.should raise_error(ArgumentError) + suppress_keyword_warning do + -> { m("a" => 1, a: 1, b: 2) }.should raise_error(ArgumentError) + end end evaluate <<-ruby do @@ -1072,7 +983,9 @@ describe "A method" do m(a: 1).should == [1, 1] m(a: 1, b: 2).should == [1, 2] - lambda { m("a" => 1, a: 1, b: 2) }.should raise_error(ArgumentError) + suppress_keyword_warning do + -> { m("a" => 1, a: 1, b: 2) }.should raise_error(ArgumentError) + end end evaluate <<-ruby do @@ -1081,7 +994,7 @@ describe "A method" do m(a: 1).should == 1 m(a: 1, b: 2).should == 1 - lambda { m("a" => 1, a: 1, b: 2) }.should raise_error(ArgumentError) + m("a" => 1, a: 1, b: 2).should == 1 end evaluate <<-ruby do @@ -1090,7 +1003,7 @@ describe "A method" do m(a: 1).should == [1, {}] m(a: 1, b: 2, c: 3).should == [1, {b: 2, c: 3}] - lambda { m("a" => 1, a: 1, b: 2) }.should raise_error(ArgumentError) + m("a" => 1, a: 1, b: 2).should == [1, {"a" => 1, b: 2}] end evaluate <<-ruby do @@ -1188,21 +1101,103 @@ describe "A method" do end evaluate <<-ruby do + def m(a, **nil); a end; + ruby + + m({a: 1}).should == {a: 1} + m({"a" => 1}).should == {"a" => 1} + + -> { m(a: 1) }.should raise_error(ArgumentError, 'no keywords accepted') + -> { m(**{a: 1}) }.should raise_error(ArgumentError, 'no keywords accepted') + -> { m("a" => 1) }.should raise_error(ArgumentError, 'no keywords accepted') + end + + evaluate <<-ruby do def m(a, b = nil, c = nil, d, e: nil, **f) [a, b, c, d, e, f] end - ruby + ruby result = m(1, 2) result.should == [1, nil, nil, 2, nil, {}] result = m(1, 2, {foo: :bar}) - result.should == [1, nil, nil, 2, nil, {foo: :bar}] + result.should == [1, 2, nil, {foo: :bar}, nil, {}] result = m(1, {foo: :bar}) result.should == [1, nil, nil, {foo: :bar}, nil, {}] end end + + 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 + end + + context 'when passing an empty keyword splat to a method that does not accept keywords' do + evaluate <<-ruby do + def m(a); a; end + ruby + h = {} + + -> do + m(**h).should == {} + end.should raise_error(ArgumentError) + end + end + + context "raises ArgumentError if passing hash as keyword arguments" do + evaluate <<-ruby do + def m(a: nil); a; end + ruby + + options = {a: 1}.freeze + -> do + m(options) + end.should raise_error(ArgumentError) + end + end + + 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 + + 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_error(TypeError, "no implicit conversion of nil into Hash") + -> { m(a: 1, **nil) }.should raise_error(TypeError, "no implicit conversion of nil into Hash") + end + end + + ruby_version_is "3.4" do + it "expands nil using ** into {}" do + def m(**kw) kw; 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 describe "A method call with a space between method name and parentheses" do @@ -1223,20 +1218,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 + + ruby_version_is "3.3" do + it "supports multiple statements" do + eval("m (1; 2)").should == [2] + end + end end - context "when 2+ arguments provided" do + context "when multiple arguments are provided" do + it "assigns simple expressions" do + args = m (1), (2) + args.should == [1, 2] + end + + it "assigns expressions consisting of multiple statements" do + args = m ((0; 1)), ((2; 3)) + args.should == [1, 3] + end + end + + context "when the argument looks like an argument list" do it "raises a syntax error" do - lambda { + -> { eval("m (1, 2)") }.should raise_error(SyntaxError) - lambda { + -> { eval("m (1, 2, 3)") }.should raise_error(SyntaxError) end @@ -1294,3 +1317,333 @@ describe "An array-dereference method ([])" do end end end + +describe "An endless method definition" do + context "without arguments" do + evaluate <<-ruby do + def m() = 42 + ruby + + m.should == 42 + end + + context "without parenthesis" do + evaluate <<-ruby do + def m = 42 + ruby + + m.should == 42 + end + end + end + + context "with arguments" do + evaluate <<-ruby do + def m(a, b) = a + b + ruby + + m(1, 4).should == 5 + end + end + + context "with multiline body" do + evaluate <<-ruby do + def m(n) = + if n > 2 + m(n - 2) + m(n - 1) + else + 1 + end + ruby + + m(6).should == 8 + end + end + + # tested more thoroughly in language/delegation_spec.rb + context "with args forwarding" do + evaluate <<-ruby do + def mm(word, num:) + word * num + end + + def m(...) = mm(...) + mm(...) + ruby + + m("meow", num: 2).should == "meow" * 4 + end + end +end + +describe "Keyword arguments are now separated from positional arguments" do + context "when the method has only positional parameters" do + it "treats incoming keyword arguments as positional for compatibility" do + def foo(a, b, c, hsh) + hsh[:key] + end + + foo(1, 2, 3, key: 42).should == 42 + end + end + + context "when the method takes a ** parameter" do + it "captures the passed literal keyword arguments" do + def foo(a, b, c, **hsh) + hsh[:key] + end + + foo(1, 2, 3, key: 42).should == 42 + end + + it "captures the passed ** keyword arguments" do + def foo(a, b, c, **hsh) + hsh[:key] + end + + h = { key: 42 } + foo(1, 2, 3, **h).should == 42 + end + + it "does not convert a positional Hash to keyword arguments" do + def foo(a, b, c, **hsh) + hsh[:key] + end + + -> { + foo(1, 2, 3, { key: 42 }) + }.should raise_error(ArgumentError, 'wrong number of arguments (given 4, expected 3)') + end + end + + context "when the method takes a key: parameter" do + context "when it's called with a positional Hash and no **" do + it "raises ArgumentError" do + def foo(a, b, c, key: 1) + key + end + + -> { + foo(1, 2, 3, { key: 42 }) + }.should raise_error(ArgumentError, 'wrong number of arguments (given 4, expected 3)') + end + end + + context "when it's called with **" do + it "captures the passed keyword arguments" do + def foo(a, b, c, key: 1) + key + end + + h = { key: 42 } + foo(1, 2, 3, **h).should == 42 + end + end + end +end + +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 d5ca71b3b4..fba4aa8c6e 100644 --- a/spec/ruby/language/module_spec.rb +++ b/spec/ruby/language/module_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/module', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/module' describe "The module keyword" do it "creates a new module without semicolon" do @@ -26,37 +26,65 @@ describe "The module keyword" do it "reopens an existing module" do module ModuleSpecs; Reopened = true; end ModuleSpecs::Reopened.should be_true + ensure + ModuleSpecs.send(:remove_const, :Reopened) end - it "reopens a module included in Object" do - module IncludedModuleSpecs; Reopened = true; end - ModuleSpecs::IncludedInObject::IncludedModuleSpecs::Reopened.should be_true + 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 - lambda do + -> do module ModuleSpecs::Modules::Klass; end end.should raise_error(TypeError) end it "raises a TypeError if the constant is a String" do - lambda { module ModuleSpecs::Modules::A; end }.should raise_error(TypeError) + -> { module ModuleSpecs::Modules::A; end }.should raise_error(TypeError) end - it "raises a TypeError if the constant is a Fixnum" do - lambda { 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_error(TypeError) end it "raises a TypeError if the constant is nil" do - lambda { module ModuleSpecs::Modules::C; end }.should raise_error(TypeError) + -> { module ModuleSpecs::Modules::C; end }.should raise_error(TypeError) end it "raises a TypeError if the constant is true" do - lambda { module ModuleSpecs::Modules::D; end }.should raise_error(TypeError) + -> { module ModuleSpecs::Modules::D; end }.should raise_error(TypeError) end it "raises a TypeError if the constant is false" do - lambda { module ModuleSpecs::Modules::D; end }.should raise_error(TypeError) + -> { module ModuleSpecs::Modules::D; end }.should raise_error(TypeError) end end @@ -67,12 +95,14 @@ describe "Assigning an anonymous module to a constant" do ::ModuleSpecs_CS1 = mod mod.name.should == "ModuleSpecs_CS1" + ensure + Object.send(:remove_const, :ModuleSpecs_CS1) end - it "does not set the name of a module scoped by an anonymous module" 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 be_nil + b.name.should.end_with? '::B' end it "sets the name of contained modules when assigning a toplevel anonymous module" do @@ -87,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 67da5224dc..6fbfc4a54d 100644 --- a/spec/ruby/language/next_spec.rb +++ b/spec/ruby/language/next_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/next', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/next' describe "The next statement from within the block" do before :each do @@ -8,7 +8,7 @@ describe "The next statement from within the block" do it "ends block execution" do a = [] - lambda { + -> { a << 1 next a << 2 @@ -17,15 +17,15 @@ describe "The next statement from within the block" do end it "causes block to return nil if invoked without arguments" do - lambda { 123; next; 456 }.call.should == nil + -> { 123; next; 456 }.call.should == nil end it "causes block to return nil if invoked with an empty expression" do - lambda { next (); 456 }.call.should be_nil + -> { next (); 456 }.call.should be_nil end it "returns the argument passed" do - lambda { 123; next 234; 345 }.call.should == 234 + -> { 123; next 234; 345 }.call.should == 234 end it "returns to the invoking method" do @@ -102,14 +102,14 @@ describe "The next statement from within the block" do it "passes the value returned by a method with omitted parenthesis and passed block" do obj = NextSpecs::Block.new - lambda { next obj.method :value do |x| x end }.call.should == :value + -> { next obj.method :value do |x| x end }.call.should == :value end end describe "The next statement" do describe "in a method" do it "is invalid and raises a SyntaxError" do - lambda { + -> { eval("def m; next; end") }.should raise_error(SyntaxError) end diff --git a/spec/ruby/language/not_spec.rb b/spec/ruby/language/not_spec.rb index a0cf6b15a6..052af9b256 100644 --- a/spec/ruby/language/not_spec.rb +++ b/spec/ruby/language/not_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "The not keyword" do it "negates a `true' value" do diff --git a/spec/ruby/language/numbered_parameters_spec.rb b/spec/ruby/language/numbered_parameters_spec.rb new file mode 100644 index 0000000000..de532c326d --- /dev/null +++ b/spec/ruby/language/numbered_parameters_spec.rb @@ -0,0 +1,113 @@ +require_relative '../spec_helper' + +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 "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 "can not be used in both outer and nested blocks at the same time" do + -> { + eval("-> { _1; -> { _2 } }") + }.should raise_error(SyntaxError, /numbered parameter is already used in/m) + end + + it "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 "raises SyntaxError when block parameters are specified explicitly" do + -> { eval("-> () { _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + -> { eval("-> (x) { _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + + -> { eval("proc { || _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + -> { eval("proc { |x| _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + + -> { eval("lambda { || _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + -> { eval("lambda { |x| _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + + -> { eval("['a'].map { || _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + -> { eval("['a'].map { |x| _1 }") }.should raise_error(SyntaxError, /ordinary parameter is defined/) + end + + describe "assigning to a numbered parameter" do + it "raises SyntaxError" do + -> { eval("proc { _1 = 0 }") }.should raise_error(SyntaxError, /_1 is reserved for numbered parameter/) + end + end + + it "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 + + it "affects block parameters" do + -> { _1 }.parameters.should == [[:req, :_1]] + -> { _2 }.parameters.should == [[:req, :_1], [:req, :_2]] + + proc { _1 }.parameters.should == [[:opt, :_1]] + proc { _2 }.parameters.should == [[:opt, :_1], [:opt, :_2]] + end + + 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 + + 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 + + -> { obj.foo("a") }.should raise_error(ArgumentError, /wrong number of arguments/) + end +end diff --git a/spec/ruby/language/numbers_spec.rb b/spec/ruby/language/numbers_spec.rb index e8c82f09a7..a8e023efb6 100644 --- a/spec/ruby/language/numbers_spec.rb +++ b/spec/ruby/language/numbers_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "A number literal" do @@ -11,7 +11,7 @@ describe "A number literal" do end it "cannot have a leading underscore" do - lambda { eval("_4_2") }.should raise_error(NameError) + -> { eval("_4_2") }.should raise_error(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 - lambda { eval(".75") }.should raise_error(SyntaxError) - lambda { eval("-.75") }.should raise_error(SyntaxError) + -> { eval(".75") }.should raise_error(SyntaxError) + -> { eval("-.75") }.should raise_error(SyntaxError) end it "can have an exponent" do @@ -45,7 +45,15 @@ describe "A number literal" do eval('-3r').should == Rational(-3, 1) end - it "can be an bignum literal with trailing 'r' to represent a Rational" do + 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 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 0ab28985ed..5fe3e3671b 100644 --- a/spec/ruby/language/optional_assignments_spec.rb +++ b/spec/ruby/language/optional_assignments_spec.rb @@ -1,4 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' +require_relative '../fixtures/constants' describe 'Optional variable assignments' do describe 'using ||=' do @@ -42,9 +43,21 @@ describe 'Optional variable assignments' do a.should == 10 end + + it 'returns the new value if set to false' do + a = false + + (a ||= 20).should == 20 + end + + it 'returns the original value if truthy' do + a = 10 + + (a ||= 20).should == 10 + end end - describe 'using a accessor' do + describe 'using an accessor' do before do klass = Class.new { attr_accessor :b } @a = klass.new @@ -89,6 +102,178 @@ 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 + end + + @a.b = false + (@a.b ||= 20).should == 20 + end + + it 'returns the original value if truthy' do + def @a.b=(x) + @b = x + :v + end + + @a.b = 10 + (@a.b ||= 20).should == 10 + 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(false) + a.public_method(10).should == 10 + end + end + + describe 'using a #[]' do + before do + @a = {} + 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 '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 + + (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 + + 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 @@ -135,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 @@ -180,11 +365,221 @@ 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 + before do + @a = {} + 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 'leaves new variable unassigned' do + @a[:k] &&= 10 + + @a.key?(:k).should == false + end + + it 'leaves false' do + @a[:k] = false + @a[:k] &&= 10 + + @a[:k].should == false + end + + it 'leaves nil' do + @a[:k] = nil + @a[:k] &&= 10 + + @a[:k].should == nil + end + + it 'does not evaluate the right side when not needed' do + @a[:k] = nil + @a[:k] &&= raise('should not be executed') + @a[:k].should == nil + end + + it 'does re-assign a variable with a truthy value' do + @a[:k] = 10 + @a[:k] &&= 20 + + @a[:k].should == 20 + end + + it 'does re-assign a variable with a truthy value when using an inline rescue' do + @a[:k] = 10 + @a[:k] &&= 20 rescue 30 + + @a[:k].should == 20 + end + + it 'returns the assigned value, not the result of the []= method with ||=' do + @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 - describe 'using compunded constants' do - before do + describe 'using compounded constants' do + before :each do + Object.send(:remove_const, :A) if defined? Object::A + end + + after :each do Object.send(:remove_const, :A) if defined? Object::A end @@ -208,7 +603,7 @@ describe 'Optional variable assignments' do end it 'with &&= assignments will fail with non-existent constants' do - lambda { Object::A &&= 10 }.should raise_error(NameError) + -> { Object::A &&= 10 }.should raise_error(NameError) end it 'with operator assignments' do @@ -220,7 +615,128 @@ describe 'Optional variable assignments' do end it 'with operator assignments will fail with non-existent constants' do - lambda { Object::A += 10 }.should raise_error(NameError) + -> { Object::A += 10 }.should raise_error(NameError) + end + end +end + +describe 'Optional constant assignment' do + 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 + + 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_error(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_error(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 150d693996..fb75e788f1 100644 --- a/spec/ruby/language/or_spec.rb +++ b/spec/ruby/language/or_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "The || operator" do it "evaluates to true if any of its operands are true" do @@ -35,15 +35,15 @@ describe "The || operator" do it "has a higher precedence than 'break' in 'break true || false'" do # see also 'break true or false' below - lambda { break false || true }.call.should be_true + -> { break false || true }.call.should be_true end it "has a higher precedence than 'next' in 'next true || false'" do - lambda { next false || true }.call.should be_true + -> { next false || true }.call.should be_true end it "has a higher precedence than 'return' in 'return true || false'" do - lambda { return false || true }.call.should be_true + -> { return false || true }.call.should be_true end end @@ -77,14 +77,14 @@ describe "The or operator" do it "has a lower precedence than 'break' in 'break true or false'" do # see also 'break true || false' above - lambda { eval "break true or false" }.should raise_error(SyntaxError, /void value expression/) + -> { eval "break true or false" }.should raise_error(SyntaxError, /void value expression/) end it "has a lower precedence than 'next' in 'next true or false'" do - lambda { eval "next true or false" }.should raise_error(SyntaxError, /void value expression/) + -> { eval "next true or false" }.should raise_error(SyntaxError, /void value expression/) end it "has a lower precedence than 'return' in 'return true or false'" do - lambda { eval "return true or false" }.should raise_error(SyntaxError, /void value expression/) + -> { eval "return true or false" }.should raise_error(SyntaxError, /void value expression/) end end diff --git a/spec/ruby/language/order_spec.rb b/spec/ruby/language/order_spec.rb index d02bf04077..d550f6b3f4 100644 --- a/spec/ruby/language/order_spec.rb +++ b/spec/ruby/language/order_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "A method call" do before :each do diff --git a/spec/ruby/language/pattern_matching_spec.rb b/spec/ruby/language/pattern_matching_spec.rb new file mode 100644 index 0000000000..c1a6f0e4d6 --- /dev/null +++ b/spec/ruby/language/pattern_matching_spec.rb @@ -0,0 +1,1310 @@ +require_relative '../spec_helper' + +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 "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 "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 + + 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_error(SyntaxError, /syntax error, unexpected `in'|\(eval\):3: syntax error, unexpected keyword_in|unexpected 'in'/) + + -> { + eval <<~RUBY + case [] + in [] + when 1 == 1 + end + RUBY + }.should raise_error(SyntaxError, /syntax error, unexpected `when'|\(eval\):3: syntax error, unexpected keyword_when|unexpected 'when'/) + end + + it "checks patterns until the first matching" do + 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_error(NoMatchingPatternError, /\[0, 1\]/) + + 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 + }.should raise_error(NoMatchingPatternError, error_pattern) + end + + 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_error(NoMatchingPatternError, /\[0, 1\]/) + evals.should == 1 + end + + it "does not allow calculation or method calls in a pattern" do + -> { + eval <<~RUBY + case 0 + in 1 + 1 + true + end + RUBY + }.should raise_error(SyntaxError, /unexpected|expected a delimiter after the 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 "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 + + it "makes bound variables visible in guard" do + case [0, 1] + in [a, 1] if a >= 0 + true + end.should == true + end + + 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 "raises NoMatchingPatternError if no guarded pattern matches and no else clause" do + -> { + case [0, 1] + in [0, 1] if false + end + }.should raise_error(NoMatchingPatternError, /\[0, 1\]/) + end + end + + 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 "allows string literal with interpolation" do + x = "x" + + case "x" + in "#{x + ""}" + true + end.should == true + end + end + + 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 + + it "makes bounded variable visible outside a case statement scope" do + case 0 + in a + end + + a.should == 0 + end + + it "create local variables even if a pattern doesn't match" do + case 0 + in a + in b + in c + 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 "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 [a, a] + end + RUBY + }.should raise_error(SyntaxError, /duplicated variable name/) + end + + it "supports existing variables in a pattern specified with ^ operator" do + a = 0 + + 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 "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 + end + + describe "alternative pattern" do + it "matches if any of patterns matches" do + case 0 + in 0 | 1 | 2 + true + end.should == true + end + + it "does not support variable binding" do + -> { + eval <<~RUBY + case [0, 1] + in [0, 0] | [0, a] + end + RUBY + }.should raise_error(SyntaxError) + end + + it "support underscore prefixed variables in alternation" do + case [0, 1] + in [1, _] + false + in [0, 0] | [0, _a] + true + end.should == true + 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 + + 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 + + it "can be used as a nested pattern" do + case [1, [2, 3]] + in [1, Array => ary] + ary + end.should == [2, 3] + end + end + + 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 + + it "supports form Constant[pat, pat, ...]" do + case [0, 1, 2] + in Array[0, 1, 2] + true + end.should == true + end + + it "supports form [pat, pat, ...]" do + case [0, 1, 2] + in [0, 1, 2] + true + end.should == true + 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 + + 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 + + case obj + in [Integer, Integer] + true + end.should == true + end + + it "calls #deconstruct once for multiple patterns, caching the result" do + obj = Object.new + + def obj.deconstruct + ScratchPad << :deconstruct + [0, 1] + end + + case obj + in [1, 2] + false + in [0, 1] + true + end.should == true + + ScratchPad.recorded.should == [:deconstruct] + end + + it "calls #deconstruct even on objects that are already an array" do + obj = [1, 2] + + def obj.deconstruct + ScratchPad << :deconstruct + [3, 4] + end + + case obj + in [3, 4] + true + else + false + end.should == true + + ScratchPad.recorded.should == [:deconstruct] + end + + 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 + + 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 "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 + + it "raises TypeError if #deconstruct method does not return array" do + obj = Object.new + + def obj.deconstruct + "" + end + + -> { + case obj + in Object[] + else + end + }.should raise_error(TypeError, /deconstruct must return Array/) + end + + it "accepts a subclass of Array from #deconstruct" do + obj = Object.new + + def obj.deconstruct + Class.new(Array).new([0, 1]) + end + + case obj + in [1, 2] + false + in [0, 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 + + case obj + in Object[0] + true + else + false + end.should == false + end + + it "binds variables" do + case [0, 1, 2] + in [a, b, c] + [a, b, c] + end.should == [0, 1, 2] + end + + it "supports splat operator *rest" do + case [0, 1, 2] + in [0, *rest] + rest + end.should == [1, 2] + 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 "does match partially from the array beginning if list + , syntax used" do + case [0, 1, 2, 3] + in [0, 1, ] + true + end.should == true + + case [0, 1, 2, 3] + in 0, 1,; + true + end.should == true + end + + it "matches [] with []" do + case [] + in [] + true + end.should == true + end + + it "matches anything with *" do + case [0, 1] + in *; + true + end.should == true + 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 + + 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 "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 "supports form {id: pat, id: pat, ...}" do + case {a: 0, b: 1} + in {a: 0, b: 1} + true + end.should == true + 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 "supports a: which means a: a" do + case {a: 0, b: 1} + in Hash(a:, b:) + [a, b] + end.should == [0, 1] + + a = b = nil + + case {a: 0, b: 1} + in Hash[a:, b:] + [a, b] + end.should == [0, 1] + + a = b = nil + + case {a: 0, b: 1} + in {a:, b:} + [a, b] + end.should == [0, 1] + + a = nil + + case {a: 0, b: 1, c: 2} + in {a:, **rest} + [a, rest] + end.should == [0, {b: 1, c: 2}] + + a = b = nil + + 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 + + it "does not support non-symbol keys" do + -> { + eval <<~RUBY + case {a: 1} + in {"a" => 1} + end + RUBY + }.should raise_error(SyntaxError, /unexpected|expected a label as the key in the hash pattern/) + end + + it "does not support string interpolation in keys" do + -> { + eval <<~'RUBY' + case {a: 1} + in {"#{x}": 1} + end + RUBY + }.should raise_error(SyntaxError, /symbol literal with interpolation is not allowed|expected a label as the key in the hash pattern/) + end + + it "raise SyntaxError when keys duplicate in pattern" do + -> { + eval <<~RUBY + case {a: 1} + in {a: 1, b: 2, a: 3} + end + RUBY + }.should raise_error(SyntaxError, /duplicated key name/) + end + + 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 {a: 1} + true + end.should == true + end + + it "calls #deconstruct_keys per pattern" do + obj = Object.new + + def obj.deconstruct_keys(*) + ScratchPad << :deconstruct_keys + {a: 1} + end + + case obj + in {b: 1} + false + in {a: 1} + true + end.should == true + + ScratchPad.recorded.should == [:deconstruct_keys, :deconstruct_keys] + end + + 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 + + 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 + + it "does not match object without #deconstruct_keys method" do + obj = Object.new + obj.should_receive(:respond_to?).with(:deconstruct_keys) + + case obj + in Object[a: 1] + true + else + false + end.should == false + end + + it "does not match object if #deconstruct_keys method does not return Hash" do + obj = Object.new + + def obj.deconstruct_keys(*) + "" + end + + -> { + case obj + in Object[a: 1] + end + }.should raise_error(TypeError, /deconstruct_keys must return Hash/) + 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 + + case obj + in Object[a: 1] + true + else + false + end.should == false + end + + it "does not match object if elements of Hash returned by #deconstruct_keys method does not match values in pattern" do + obj = Object.new + + def obj.deconstruct_keys(*) + {a: 1} + end + + case obj + in Object[a: 2] + true + else + false + end.should == false + end + + 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 + + case obj + in Object[a: 1, b: 2, c: 3] + end + + ScratchPad.recorded.sort.should == [[[:a, :b, :c]]] + end + + it "passes keys specified in pattern to #deconstruct_keys method if pattern contains double splat operator **" do + obj = Object.new + + def obj.deconstruct_keys(*args) + ScratchPad << args + {a: 1, b: 2, c: 3} + end + + case obj + in Object[a: 1, b: 2, **] + end + + ScratchPad.recorded.sort.should == [[[:a, :b]]] + end + + it "passes nil to #deconstruct_keys method if pattern contains double splat operator **rest" do + obj = Object.new + + def obj.deconstruct_keys(*args) + ScratchPad << args + {a: 1, b: 2} + end + + case obj + in Object[a: 1, **rest] + end + + ScratchPad.recorded.should == [[nil]] + 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 + + 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 + + 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 "can match partially" do + case {a: 1, b: 2} + in {a: 1} + true + end.should == true + end + + it "matches {} with {}" do + case {} + in {} + true + end.should == true + end + + it "in {} only matches empty hashes" do + case {a: 1} + in {} + true + else + false + end.should == false + 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 "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 + end + end + + result = nil + Module.new do + using refinery + + result = + case [] + in [0] + true + end + end + + result.should == true + end + + it "are used for #deconstruct_keys" do + refinery = Module.new do + refine Hash do + def deconstruct_keys(_) + {a: 0} + end + end + end + + result = nil + Module.new do + using refinery + + result = + case {} + in a: 0 + true + end + end + + 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 + end + end + + result = nil + Module.new do + using refinery + + result = + case {} + in Array + true + end + end + + 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 + + it "supports pinning global variables" do + $a = /a/ + case 'abc' + in ^$a + true + end.should == true + end + + it "supports pinning expressions" do + case 'abc' + in ^(/a/) + true + end.should == true + + case 0 + in ^(0 + 0) + true + end.should == true + end + + it "supports pinning expressions in array pattern" do + case [3] + in [^(1 + 2)] + true + end.should == true + 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 + + describe "value in pattern" do + it "returns true if the pattern matches" do + (1 in 1).should == true + + (1 in Integer).should == true + + e = nil + ([1, 2] in [1, e]).should == true + e.should == 2 + + k = nil + ({k: 1} in {k:}).should == true + k.should == 1 + end + + it "returns false if the pattern does not match" do + (1 in 2).should == false + + (1 in Float).should == false + + ([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 90734022ff..5e606c16d8 100644 --- a/spec/ruby/language/precedence_spec.rb +++ b/spec/ruby/language/precedence_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/precedence', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/precedence' # Specifying the behavior of operators in combination could # lead to combinatorial explosion. A better way seems to be @@ -14,46 +14,44 @@ require File.expand_path('../fixtures/precedence', __FILE__) # 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 @@ -136,7 +134,7 @@ describe "Operators" do # Guard against the Mathn library # TODO: Make these specs not rely on specific behaviour / result values # by using mocks. - conflicts_with :Prime do + guard -> { !defined?(Math.rsqrt) } do (2*1/2).should_not == 2*(1/2) end @@ -253,12 +251,12 @@ describe "Operators" do end it "<=> == === != =~ !~ are non-associative" do - lambda { eval("1 <=> 2 <=> 3") }.should raise_error(SyntaxError) - lambda { eval("1 == 2 == 3") }.should raise_error(SyntaxError) - lambda { eval("1 === 2 === 3") }.should raise_error(SyntaxError) - lambda { eval("1 != 2 != 3") }.should raise_error(SyntaxError) - lambda { eval("1 =~ 2 =~ 3") }.should raise_error(SyntaxError) - lambda { 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_error(SyntaxError) end it "<=> == === != =~ !~ have higher precedence than &&" do @@ -292,19 +290,18 @@ describe "Operators" do end it ".. ... are non-associative" do - lambda { eval("1..2..3") }.should raise_error(SyntaxError) - lambda { eval("1...2...3") }.should raise_error(SyntaxError) + -> { eval("1..2..3") }.should raise_error(SyntaxError) + -> { eval("1...2...3") }.should raise_error(SyntaxError) end -# XXX: this is commented now due to a bug in compiler, which cannot -# distinguish between range and flip-flop operator so far. zenspider is -# currently working on a new lexer, which will be able to do that. -# As soon as it's done, these piece should be reenabled. -# -# it ".. ... have higher precedence than ? :" do -# (1..2 ? 3 : 4).should == 3 -# (1...2 ? 3 : 4).should == 3 -# 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/data_spec.rb b/spec/ruby/language/predefined/data_spec.rb index f616879527..921d236ee9 100644 --- a/spec/ruby/language/predefined/data_spec.rb +++ b/spec/ruby/language/predefined/data_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../../spec_helper', __FILE__) +require_relative '../../spec_helper' describe "The DATA constant" do it "exists when the main script contains __END__" do @@ -23,6 +23,25 @@ describe "The DATA constant" do str.chomp.should == "data only" end + it "returns a File object with the right offset" do + ruby_exe(fixture(__FILE__, "data_offset.rb")).should == "File\n121\n" + end + + it "is set even if there is no data after __END__" do + ruby_exe(fixture(__FILE__, "empty_data.rb")).should == "31\n\"\"\n" + end + + it "is set even if there is no newline after __END__" do + path = tmp("no_newline_data.rb") + code = File.binread(fixture(__FILE__, "empty_data.rb")) + touch(path, "wb") { |f| f.write code.chomp } + begin + ruby_exe(path).should == "30\n\"\"\n" + ensure + rm_r path + end + end + it "rewinds to the head of the main script" do ruby_exe(fixture(__FILE__, "data5.rb")).chomp.should == "DATA.rewind" end diff --git a/spec/ruby/language/predefined/fixtures/data2.rb b/spec/ruby/language/predefined/fixtures/data2.rb index 0f714b06d4..a764ca56d1 100644 --- a/spec/ruby/language/predefined/fixtures/data2.rb +++ b/spec/ruby/language/predefined/fixtures/data2.rb @@ -1,4 +1,3 @@ - -require File.expand_path("../data4.rb", __FILE__) +require_relative 'data4' p Object.const_defined?(:DATA) diff --git a/spec/ruby/language/predefined/fixtures/data3.rb b/spec/ruby/language/predefined/fixtures/data3.rb index 6cbf63dae6..e37313c68b 100644 --- a/spec/ruby/language/predefined/fixtures/data3.rb +++ b/spec/ruby/language/predefined/fixtures/data3.rb @@ -1,5 +1,4 @@ - -require File.expand_path("../data4.rb", __FILE__) +require_relative 'data4' puts DATA.read diff --git a/spec/ruby/language/predefined/fixtures/data_offset.rb b/spec/ruby/language/predefined/fixtures/data_offset.rb new file mode 100644 index 0000000000..9829b3f87f --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/data_offset.rb @@ -0,0 +1,12 @@ +# some comment + +foo = <<HEREDOC +some heredoc to make the +spec more interesting +HEREDOC + +p DATA.class +p DATA.pos + +__END__ +data offset diff --git a/spec/ruby/language/predefined/fixtures/empty_data.rb b/spec/ruby/language/predefined/fixtures/empty_data.rb new file mode 100644 index 0000000000..c6d9bc6f1f --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/empty_data.rb @@ -0,0 +1,3 @@ +p DATA.pos +p DATA.read +__END__ diff --git a/spec/ruby/language/predefined/fixtures/toplevel_binding_dynamic.rb b/spec/ruby/language/predefined/fixtures/toplevel_binding_dynamic.rb new file mode 100644 index 0000000000..f7809109fa --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/toplevel_binding_dynamic.rb @@ -0,0 +1,4 @@ +p TOPLEVEL_BINDING.local_variables.sort +TOPLEVEL_BINDING.local_variable_set(:dynamic_set_main, 2) +p TOPLEVEL_BINDING.local_variables.sort +main_script = 3 diff --git a/spec/ruby/language/predefined/fixtures/toplevel_binding_dynamic_required.rb b/spec/ruby/language/predefined/fixtures/toplevel_binding_dynamic_required.rb new file mode 100644 index 0000000000..7ccf329680 --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/toplevel_binding_dynamic_required.rb @@ -0,0 +1,2 @@ +TOPLEVEL_BINDING.local_variable_set(:dynamic_set_required, 1) +p TOPLEVEL_BINDING.local_variables diff --git a/spec/ruby/language/predefined/fixtures/toplevel_binding_id.rb b/spec/ruby/language/predefined/fixtures/toplevel_binding_id.rb new file mode 100644 index 0000000000..3626ea1f10 --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/toplevel_binding_id.rb @@ -0,0 +1,4 @@ +a = TOPLEVEL_BINDING.object_id +require_relative 'toplevel_binding_id_required' +c = eval('TOPLEVEL_BINDING.object_id') +p [a, $b, c].uniq.size diff --git a/spec/ruby/language/predefined/fixtures/toplevel_binding_id_required.rb b/spec/ruby/language/predefined/fixtures/toplevel_binding_id_required.rb new file mode 100644 index 0000000000..b31b6e32a0 --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/toplevel_binding_id_required.rb @@ -0,0 +1 @@ +$b = TOPLEVEL_BINDING.object_id diff --git a/spec/ruby/language/predefined/fixtures/toplevel_binding_required_before.rb b/spec/ruby/language/predefined/fixtures/toplevel_binding_required_before.rb new file mode 100644 index 0000000000..58924a5800 --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/toplevel_binding_required_before.rb @@ -0,0 +1,2 @@ +required = true +p [:required_before, TOPLEVEL_BINDING.local_variables] diff --git a/spec/ruby/language/predefined/fixtures/toplevel_binding_values.rb b/spec/ruby/language/predefined/fixtures/toplevel_binding_values.rb new file mode 100644 index 0000000000..42bd67f347 --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/toplevel_binding_values.rb @@ -0,0 +1,9 @@ +p TOPLEVEL_BINDING.local_variable_get(:a) +p TOPLEVEL_BINDING.local_variable_get(:b) +a = 1 +p TOPLEVEL_BINDING.local_variable_get(:a) +p TOPLEVEL_BINDING.local_variable_get(:b) +b = 2 +a = 3 +p TOPLEVEL_BINDING.local_variable_get(:a) +p TOPLEVEL_BINDING.local_variable_get(:b) diff --git a/spec/ruby/language/predefined/fixtures/toplevel_binding_variables.rb b/spec/ruby/language/predefined/fixtures/toplevel_binding_variables.rb new file mode 100644 index 0000000000..151f4340ef --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/toplevel_binding_variables.rb @@ -0,0 +1,4 @@ +main_script = 1 +require_relative 'toplevel_binding_variables_required' +eval('eval_var = 3') +p TOPLEVEL_BINDING.local_variables diff --git a/spec/ruby/language/predefined/fixtures/toplevel_binding_variables_required.rb b/spec/ruby/language/predefined/fixtures/toplevel_binding_variables_required.rb new file mode 100644 index 0000000000..614547fe16 --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/toplevel_binding_variables_required.rb @@ -0,0 +1,2 @@ +required = 2 +p [:required_after, TOPLEVEL_BINDING.local_variables] diff --git a/spec/ruby/language/predefined/toplevel_binding_spec.rb b/spec/ruby/language/predefined/toplevel_binding_spec.rb new file mode 100644 index 0000000000..69ac28618c --- /dev/null +++ b/spec/ruby/language/predefined/toplevel_binding_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../spec_helper' + +describe "The TOPLEVEL_BINDING constant" do + it "only includes local variables defined in the main script, not in required files or eval" do + binding_toplevel_variables = ruby_exe(fixture(__FILE__, "toplevel_binding_variables.rb")) + binding_toplevel_variables.should == "[:required_after, [:main_script]]\n[:main_script]\n" + end + + it "has no local variables in files required before the main script" do + required = fixture(__FILE__, 'toplevel_binding_required_before.rb') + out = ruby_exe("a=1; p TOPLEVEL_BINDING.local_variables.sort; b=2", options: "-r#{required}") + out.should == "[:required_before, []]\n[:a, :b]\n" + end + + it "merges local variables of the main script with dynamically-defined Binding variables" do + required = fixture(__FILE__, 'toplevel_binding_dynamic_required.rb') + out = ruby_exe(fixture(__FILE__, 'toplevel_binding_dynamic.rb'), options: "-r#{required}") + out.should == <<EOS +[:dynamic_set_required] +[:dynamic_set_required, :main_script] +[:dynamic_set_main, :dynamic_set_required, :main_script] +EOS + end + + it "gets updated variables values as they are defined and set" do + out = ruby_exe(fixture(__FILE__, "toplevel_binding_values.rb")) + out.should == "nil\nnil\n1\nnil\n3\n2\n" + end + + it "is always the same object for all top levels" do + binding_toplevel_id = ruby_exe(fixture(__FILE__, "toplevel_binding_id.rb")) + binding_toplevel_id.should == "1\n" + end +end diff --git a/spec/ruby/language/predefined_spec.rb b/spec/ruby/language/predefined_spec.rb index f827fb2eb5..fc1667a38f 100644 --- a/spec/ruby/language/predefined_spec.rb +++ b/spec/ruby/language/predefined_spec.rb @@ -1,4 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) +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,56 +8,52 @@ 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 $9 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 $9 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) - $~.object_id.should == md.object_id + $~.should equal md /bar/ =~ 'bar' $~.should be_kind_of(MatchData) - $~.object_id.should_not == md.object_id + $~.should_not equal md end it "is set to nil if the last match was unsuccessful" do /foo/ =~ 'foo' - $~.nil?.should == false + $~.should_not.nil? /foo/ =~ 'bar' - $~.nil?.should == true + $~.should.nil? end it "is set at the method-scoped level rather than block-scoped" 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 @@ -92,8 +89,8 @@ describe "Predefined global $~" do $~ = /foo/.match("foo") $~.should be_an_instance_of(MatchData) - lambda { $~ = Object.new }.should raise_error(TypeError) - lambda { $~ = 1 }.should raise_error(TypeError) + -> { $~ = Object.new }.should raise_error(TypeError, 'wrong argument type Object (expected MatchData)') + -> { $~ = 1 }.should raise_error(TypeError, 'wrong argument type Integer (expected MatchData)') end it "changes the value of derived capture globals when assigned" do @@ -136,11 +133,22 @@ describe "Predefined global $&" do $&.should == 'foo' end - with_feature :encoding do - 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) - end + it "sets the encoding to the encoding of the source String" do + "abc".dup.force_encoding(Encoding::EUC_JP) =~ /b/ + $&.encoding.should equal(Encoding::EUC_JP) + end + + it "is read-only" do + -> { + eval %q{$& = ""} + }.should raise_error(SyntaxError, /Can't set variable \$&/) + end + + it "is read-only when aliased" do + alias $predefined_spec_ampersand $& + -> { + $predefined_spec_ampersand = "" + }.should raise_error(NameError, '$predefined_spec_ampersand is a read-only variable') end end @@ -151,16 +159,27 @@ describe "Predefined global $`" do $`.should == 'bar' end - with_feature :encoding do - 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) - end + it "sets the encoding to the encoding of the source String" do + "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) - end + it "sets an empty result to the encoding of the source String" do + "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_error(SyntaxError, /Can't set variable \$`/) + end + + it "is read-only when aliased" do + alias $predefined_spec_backquote $` + -> { + $predefined_spec_backquote = "" + }.should raise_error(NameError, '$predefined_spec_backquote is a read-only variable') end end @@ -171,16 +190,27 @@ describe "Predefined global $'" do $'.should == 'baz' end - with_feature :encoding do - 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) - end + it "sets the encoding to the encoding of the source String" do + "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) - end + it "sets an empty result to the encoding of the source String" do + "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_error(SyntaxError, /Can't set variable \$'/) + end + + it "is read-only when aliased" do + alias $predefined_spec_single_quote $' + -> { + $predefined_spec_single_quote = "" + }.should raise_error(NameError, '$predefined_spec_single_quote is a read-only variable') end end @@ -196,11 +226,22 @@ describe "Predefined global $+" do $+.should == 'a' end - with_feature :encoding do - 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) - end + it "sets the encoding to the encoding of the source String" do + "abc".dup.force_encoding(Encoding::EUC_JP) =~ /(b)/ + $+.encoding.should equal(Encoding::EUC_JP) + end + + it "is read-only" do + -> { + eval %q{$+ = ""} + }.should raise_error(SyntaxError, /Can't set variable \$\+/) + end + + it "is read-only when aliased" do + alias $predefined_spec_plus $+ + -> { + $predefined_spec_plus = "" + }.should raise_error(NameError, '$predefined_spec_plus is a read-only variable') end end @@ -225,11 +266,9 @@ describe "Predefined globals $1..N" do test("-").should == nil end - with_feature :encoding do - 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) - end + it "sets the encoding to the encoding of the source String" do + "abc".dup.force_encoding(Encoding::EUC_JP) =~ /(b)/ + $1.encoding.should equal(Encoding::EUC_JP) end end @@ -243,12 +282,12 @@ describe "Predefined global $stdout" do end it "raises TypeError error if assigned to nil" do - lambda { $stdout = nil }.should raise_error(TypeError) + -> { $stdout = nil }.should raise_error(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') - lambda { $stdout = obj }.should raise_error(TypeError) + -> { $stdout = obj }.should raise_error(TypeError) obj.stub!(:write) $stdout = obj @@ -257,6 +296,22 @@ describe "Predefined global $stdout" do 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_error(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 @@ -408,6 +463,22 @@ describe "Predefined global $!" do $!.should == nil end + it "should be cleared when an exception is rescued even when a non-local return from block" do + def foo + [ 1 ].each do + begin + raise StandardError.new('err') + rescue => e + $!.should == e + return + end + end + end + + foo + $!.should == nil + end + it "should not be cleared when an exception is not rescued" do e = StandardError.new begin @@ -500,44 +571,112 @@ 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 be_an_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 be_an_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_error(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 + @verbose, $VERBOSE = $VERBOSE, nil @dollar_slash = $/ @dollar_dash_zero = $-0 end @@ -545,14 +684,42 @@ describe "Predefined global $/" do after :each do $/ = @dollar_slash $-0 = @dollar_dash_zero + $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 be_an_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 @@ -562,7 +729,6 @@ describe "Predefined global $/" do ($/ = "xyz").should == "xyz" end - it "changes $-0" do $/ = "xyz" $-0.should equal($/) @@ -572,20 +738,25 @@ describe "Predefined global $/" do obj = mock("$/ value") obj.should_not_receive(:to_str) - lambda { $/ = obj }.should raise_error(TypeError) + -> { $/ = obj }.should raise_error(TypeError, 'value of $/ must be String') end - it "raises a TypeError if assigned a Fixnum" do - lambda { $/ = 1 }.should raise_error(TypeError) + it "raises a TypeError if assigned an Integer" do + -> { $/ = 1 }.should raise_error(TypeError, 'value of $/ must be String') end it "raises a TypeError if assigned a boolean" do - lambda { $/ = true }.should raise_error(TypeError) + -> { $/ = true }.should raise_error(TypeError, 'value of $/ must be String') + end + + it "warns if assigned non-nil" do + -> { $/ = "_" }.should complain(/warning: (?:non-nil )?[`']\$\/' is deprecated/) end end describe "Predefined global $-0" do before :each do + @verbose, $VERBOSE = $VERBOSE, nil @dollar_slash = $/ @dollar_dash_zero = $-0 end @@ -593,12 +764,41 @@ describe "Predefined global $-0" do after :each do $/ = @dollar_slash $-0 = @dollar_dash_zero + $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 be_an_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 @@ -619,15 +819,62 @@ describe "Predefined global $-0" do obj = mock("$-0 value") obj.should_not_receive(:to_str) - lambda { $-0 = obj }.should raise_error(TypeError) + -> { $-0 = obj }.should raise_error(TypeError, 'value of $-0 must be String') end - it "raises a TypeError if assigned a Fixnum" do - lambda { $-0 = 1 }.should raise_error(TypeError) + it "raises a TypeError if assigned an Integer" do + -> { $-0 = 1 }.should raise_error(TypeError, 'value of $-0 must be String') end it "raises a TypeError if assigned a boolean" do - lambda { $-0 = true }.should raise_error(TypeError) + -> { $-0 = true }.should raise_error(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 be_nil + end + + it "returns the value assigned" do + ($\ = "xyz").should == "xyz" + end + + it "does not call #to_str to convert the object to a String" do + obj = mock("$\\ value") + obj.should_not_receive(:to_str) + + -> { $\ = obj }.should raise_error(TypeError, 'value of $\ must be String') + end + + it "raises a TypeError if assigned not String" do + -> { $\ = 1 }.should raise_error(TypeError, 'value of $\ must be String') + -> { $\ = true }.should raise_error(TypeError, 'value of $\ must be String') + end + + it "warns if assigned non-nil" do + -> { $\ = "_" }.should complain(/warning: (?:non-nil )?[`']\$\\' is deprecated/) end end @@ -641,7 +888,48 @@ describe "Predefined global $," do end it "raises TypeError if assigned a non-String" do - lambda { $, = Object.new }.should raise_error(TypeError) + -> { $, = Object.new }.should raise_error(TypeError, 'value of $, must be String') + end + + it "warns if assigned non-nil" do + -> { $, = "_" }.should complain(/warning: (?:non-nil )?[`']\$,' is deprecated/) + end +end + +describe "Predefined global $." do + it "can be assigned an Integer" do + $. = 123 + $..should == 123 + end + + it "can be assigned a Float" do + $. = 123.5 + $..should == 123 + end + + it "should call #to_int to convert the object to an Integer" do + obj = mock("good-value") + obj.should_receive(:to_int).and_return(321) + + $. = obj + $..should == 321 + end + + it "raises TypeError if object can't be converted to an Integer" do + obj = mock("bad-value") + obj.should_receive(:to_int).and_return('abc') + + -> { $. = obj }.should raise_error(TypeError) + end +end + +describe "Predefined global $;" do + after :each do + $; = nil + end + + it "warns if assigned non-nil" do + -> { $; = "_" }.should complain(/warning: (?:non-nil )?[`']\$;' is deprecated/) end end @@ -675,7 +963,7 @@ describe "Predefined global $_" do match.should == "bar\n" $_.should == match - eval 'match = stdin.gets' + match = stdin.gets match.should == "baz\n" $_.should == match @@ -714,56 +1002,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] -$SAFE Fixnum The current safe level. This variable’s value may never be - reduced by assignment. [thread] (Not implemented in Rubinius) -$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 @@ -782,60 +1066,78 @@ describe "Execution variable $:" do it "can be changed via <<" do $: << "foo" $:.should include("foo") + ensure + $:.delete("foo") end it "is read-only" do - lambda { + -> { $: = [] - }.should raise_error(NameError) + }.should raise_error(NameError, '$: is a read-only variable') - lambda { + -> { $LOAD_PATH = [] - }.should raise_error(NameError) + }.should raise_error(NameError, '$LOAD_PATH is a read-only variable') - lambda { + -> { $-I = [] - }.should raise_error(NameError) + }.should raise_error(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 be_true + $:[0...idx].all? { |p| !p.instance_variable_defined?(:@gem_prelude_index) }.should be_true end end describe "Global variable $\"" do it "is an alias for $LOADED_FEATURES" do - $".object_id.should == $LOADED_FEATURES.object_id + $".should equal $LOADED_FEATURES end it "is read-only" do - lambda { + -> { $" = [] - }.should raise_error(NameError) + }.should raise_error(NameError, '$" is a read-only variable') - lambda { + -> { $LOADED_FEATURES = [] - }.should raise_error(NameError) + }.should raise_error(NameError, '$LOADED_FEATURES is a read-only variable') end end describe "Global variable $<" do it "is read-only" do - lambda { + -> { $< = nil - }.should raise_error(NameError) + }.should raise_error(NameError, '$< is a read-only variable') end end describe "Global variable $FILENAME" do it "is read-only" do - lambda { + -> { $FILENAME = "-" - }.should raise_error(NameError) + }.should raise_error(NameError, '$FILENAME is a read-only variable') end end describe "Global variable $?" do it "is read-only" do - lambda { + -> { $? = nil - }.should raise_error(NameError) + }.should raise_error(NameError, '$? is a read-only variable') end it "is thread-local" do @@ -846,19 +1148,19 @@ end describe "Global variable $-a" do it "is read-only" do - lambda { $-a = true }.should raise_error(NameError) + -> { $-a = true }.should raise_error(NameError, '$-a is a read-only variable') end end describe "Global variable $-l" do it "is read-only" do - lambda { $-l = true }.should raise_error(NameError) + -> { $-l = true }.should raise_error(NameError, '$-l is a read-only variable') end end describe "Global variable $-p" do it "is read-only" do - lambda { $-p = true }.should raise_error(NameError) + -> { $-p = true }.should raise_error(NameError, '$-p is a read-only variable') end end @@ -880,6 +1182,18 @@ describe "Global variable $-d" do end describe "Global variable $VERBOSE" do + before :each do + @verbose = $VERBOSE + end + + after :each do + $VERBOSE = @verbose + end + + it "is false by default" do + $VERBOSE.should be_false + end + it "converts truthy values to true" do [true, 1, 0, [], ""].each do |true_value| $VERBOSE = true_value @@ -934,7 +1248,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 @@ -957,26 +1271,24 @@ describe "Global variable $0" do end it "raises a TypeError when not given an object that can be coerced to a String" do - lambda { $0 = nil }.should raise_error(TypeError) + -> { $0 = nil }.should raise_error(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 @@ -999,7 +1311,7 @@ describe "The predefined standard object nil" do end it "raises a SyntaxError if assigned to" do - lambda { eval("nil = true") }.should raise_error(SyntaxError) + -> { eval("nil = true") }.should raise_error(SyntaxError, /Can't assign to nil/) end end @@ -1009,7 +1321,7 @@ describe "The predefined standard object true" do end it "raises a SyntaxError if assigned to" do - lambda { eval("true = false") }.should raise_error(SyntaxError) + -> { eval("true = false") }.should raise_error(SyntaxError, /Can't assign to true/) end end @@ -1019,85 +1331,61 @@ describe "The predefined standard object false" do end it "raises a SyntaxError if assigned to" do - lambda { eval("false = nil") }.should raise_error(SyntaxError) + -> { eval("false = nil") }.should raise_error(SyntaxError, /Can't assign to false/) end end describe "The self pseudo-variable" do it "raises a SyntaxError if assigned to" do - lambda { eval("self = 1") }.should raise_error(SyntaxError) + -> { eval("self = 1") }.should raise_error(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. -FALSE FalseClass Synonym for false. -NIL NilClass Synonym for nil. -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. -=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 - ruby_version_is ""..."2.4" do - it "includes TRUE" do - Object.const_defined?(:TRUE).should == true - TRUE.should equal(true) - end - - it "includes FALSE" do - Object.const_defined?(:FALSE).should == true - FALSE.should equal(false) - end - - it "includes NIL" do - Object.const_defined?(:NIL).should == true - NIL.should equal(nil) + describe "TRUE" do + it "is no longer defined" do + Object.const_defined?(:TRUE).should == false end end - ruby_version_is "2.4" do - it "includes TRUE" do - Object.const_defined?(:TRUE).should == true - -> { - TRUE.should equal(true) - }.should complain(/constant ::TRUE is deprecated/) - end - - it "includes FALSE" do - Object.const_defined?(:FALSE).should == true - -> { - FALSE.should equal(false) - }.should complain(/constant ::FALSE is deprecated/) + describe "FALSE" do + it "is no longer defined" do + Object.const_defined?(:FALSE).should == false end + end - it "includes NIL" do - Object.const_defined?(:NIL).should == true - -> { - NIL.should equal(nil) - }.should complain(/constant ::NIL is deprecated/) + describe "NIL" do + it "is no longer defined" do + Object.const_defined?(:NIL).should == false end end @@ -1128,22 +1416,21 @@ describe "The predefined global constants" do it "includes TOPLEVEL_BINDING" do Object.const_defined?(:TOPLEVEL_BINDING).should == true end - end -with_feature :encoding do - describe "The predefined global constant" do - before :each do - @external = Encoding.default_external - @internal = Encoding.default_internal - end +describe "The predefined global constant" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + end - after :each do - Encoding.default_external = @external - Encoding.default_internal = @internal - end + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + end - describe "STDIN" do + describe "STDIN" do + 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 @@ -1153,19 +1440,6 @@ with_feature :encoding do STDIN.external_encoding.should equal(Encoding::ISO_8859_16) end - it "has the encodings set by #set_encoding" do - code = "STDIN.set_encoding Encoding::IBM775, Encoding::IBM866; " \ - "p [STDIN.external_encoding.name, STDIN.internal_encoding.name]" - ruby_exe(code).chomp.should == %{["IBM775", "IBM866"]} - end - - it "retains the encoding set by #set_encoding when Encoding.default_external is changed" do - code = "STDIN.set_encoding Encoding::IBM775, Encoding::IBM866; " \ - "Encoding.default_external = Encoding::ISO_8859_16;" \ - "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 @@ -1176,65 +1450,125 @@ with_feature :encoding do end end - describe "STDOUT" do - it "has nil for the external encoding" do - STDOUT.external_encoding.should be_nil - end + it "has the encodings set by #set_encoding" do + code = "STDIN.set_encoding Encoding::IBM775, Encoding::IBM866; " \ + "p [STDIN.external_encoding.name, STDIN.internal_encoding.name]" + ruby_exe(code).chomp.should == %{["IBM775", "IBM866"]} + 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 - end + it "retains the encoding set by #set_encoding when Encoding.default_external is changed" do + code = "STDIN.set_encoding Encoding::IBM775, Encoding::IBM866; " \ + "Encoding.default_external = Encoding::ISO_8859_16;" \ + "p [STDIN.external_encoding.name, STDIN.internal_encoding.name]" + ruby_exe(code).chomp.should == %{["IBM775", "IBM866"]} + end + end - it "has the encodings set by #set_encoding" do - code = "STDOUT.set_encoding Encoding::IBM775, Encoding::IBM866; " \ - "p [STDOUT.external_encoding.name, STDOUT.internal_encoding.name]" - ruby_exe(code).chomp.should == %{["IBM775", "IBM866"]} - end + describe "STDOUT" do + it "has nil for the external encoding" do + STDOUT.external_encoding.should be_nil + end - it "has nil for the internal encoding" do - STDOUT.internal_encoding.should be_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 + 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 - end + it "has the encodings set by #set_encoding" do + code = "STDOUT.set_encoding Encoding::IBM775, Encoding::IBM866; " \ + "p [STDOUT.external_encoding.name, STDOUT.internal_encoding.name]" + ruby_exe(code).chomp.should == %{["IBM775", "IBM866"]} end - describe "STDERR" do - it "has nil for the external encoding" do - STDERR.external_encoding.should be_nil - end + it "has nil for the internal encoding" do + STDOUT.internal_encoding.should be_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 - 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 + end + end - it "has the encodings set by #set_encoding" do - code = "STDERR.set_encoding Encoding::IBM775, Encoding::IBM866; " \ - "p [STDERR.external_encoding.name, STDERR.internal_encoding.name]" - ruby_exe(code).chomp.should == %{["IBM775", "IBM866"]} - end + describe "STDERR" do + it "has nil for the external encoding" do + STDERR.external_encoding.should be_nil + end - it "has nil for the internal encoding" do - STDERR.internal_encoding.should be_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 + 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 - end + it "has the encodings set by #set_encoding" do + code = "STDERR.set_encoding Encoding::IBM775, Encoding::IBM866; " \ + "p [STDERR.external_encoding.name, STDERR.internal_encoding.name]" + ruby_exe(code).chomp.should == %{["IBM775", "IBM866"]} end - describe "ARGV" do - it "contains Strings encoded in locale Encoding" do - code = fixture __FILE__, "argv_encoding.rb" - result = ruby_exe(code, args: "a b") - encoding = Encoding.default_external - result.chomp.should == %{["#{encoding}", "#{encoding}"]} - end + it "has nil for the internal encoding" do + STDERR.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 + STDERR.internal_encoding.should be_nil end end + + describe "ARGV" do + it "contains Strings encoded in locale Encoding" do + code = fixture __FILE__, "argv_encoding.rb" + result = ruby_exe(code, args: "a b") + encoding = Encoding.default_external + result.chomp.should == %{["#{encoding}", "#{encoding}"]} + 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 be_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 796c0c1711..b04aa25c9e 100644 --- a/spec/ruby/language/private_spec.rb +++ b/spec/ruby/language/private_spec.rb @@ -1,15 +1,15 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/private', __FILE__) +require_relative '../spec_helper' +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) - lambda { a.bar }.should raise_error(NoMethodError) + -> { a.bar }.should raise_error(NoMethodError) b = Private::B.new b.methods.should_not include(:bar) - lambda { b.bar }.should raise_error(NoMethodError) + -> { b.bar }.should raise_error(NoMethodError) end # def expr.meth() methods are always public @@ -22,7 +22,7 @@ describe "The private keyword" do c.methods.should include(:baz) c.baz Private::B.public_class_method1.should == 1 - lambda { Private::B.private_class_method1 }.should raise_error(NoMethodError) + -> { Private::B.private_class_method1 }.should raise_error(NoMethodError) end it "is no longer in effect when the class is closed" do @@ -34,7 +34,7 @@ describe "The private keyword" do it "changes visibility of previously called method" do klass = Class.new do def foo - "foo" + "foo" end end f = klass.new @@ -42,12 +42,12 @@ describe "The private keyword" do klass.class_eval do private :foo end - lambda { f.foo }.should raise_error(NoMethodError) + -> { f.foo }.should raise_error(NoMethodError) end - it "changes visiblity of previously called methods with same send/call site" do + it "changes visibility of previously called methods with same send/call site" do g = ::Private::G.new - lambda { + -> { 2.times do g.foo module ::Private @@ -61,7 +61,7 @@ describe "The private keyword" do it "changes the visibility of the existing method in the subclass" do ::Private::A.new.foo.should == 'foo' - lambda {::Private::H.new.foo}.should raise_error(NoMethodError) + -> { ::Private::H.new.foo }.should raise_error(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 bbef318826..ca9a13aa61 100644 --- a/spec/ruby/language/proc_spec.rb +++ b/spec/ruby/language/proc_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "A Proc" do it "captures locals from the surrounding scope" do @@ -21,7 +21,7 @@ describe "A Proc" do @l.call.should == 1 end - it "raises an ArgumentErro if a value is passed" do + it "raises an ArgumentError if a value is passed" do lambda { @l.call(0) }.should raise_error(ArgumentError) end end @@ -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,6 +161,18 @@ describe "A Proc" do end end + describe "taking |*a, b| arguments" do + it "assigns [] to the argument when passed no values" do + proc { |*a, b| [a, b] }.call.should == [[], nil] + end + end + + describe "taking |a, *b, c| arguments" do + it "assigns [] to the argument when passed no values" do + proc { |a, *b, c| [a, b, c] }.call.should == [nil, [], nil] + end + end + describe "taking |a, | arguments" do before :each do @l = lambda { |a, | a } @@ -217,4 +229,21 @@ describe "A Proc" do lambda { @l.call(obj) }.should raise_error(TypeError) end end + + describe "taking |*a, **kw| arguments" do + before :each do + @p = proc { |*a, **kw| [a, kw] } + end + + it 'does not autosplat keyword arguments' do + @p.call([1, {a: 1}]).should == [[[1, {a: 1}]], {}] + end + end + + describe "taking |required keyword arguments, **kw| arguments" do + it "raises ArgumentError for missing required argument" do + p = proc { |a:, **kw| [a, kw] } + -> { p.call() }.should raise_error(ArgumentError) + end + end end diff --git a/spec/ruby/language/range_spec.rb b/spec/ruby/language/range_spec.rb new file mode 100644 index 0000000000..ccc9f55537 --- /dev/null +++ b/spec/ruby/language/range_spec.rb @@ -0,0 +1,30 @@ +require_relative '../spec_helper' +require_relative 'fixtures/classes' + +describe "Literal Ranges" do + it "creates range object" do + (1..10).should == Range.new(1, 10) + end + + it "creates range with excluded right boundary" do + (1...10).should == Range.new(1, 10, true) + end + + 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 + + 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 53fd30b4f2..57532553b3 100644 --- a/spec/ruby/language/redo_spec.rb +++ b/spec/ruby/language/redo_spec.rb @@ -1,9 +1,9 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "The redo statement" do it "restarts block execution if used within block" do a = [] - lambda { + -> { a << 1 redo if a.size < 2 a << 2 @@ -58,7 +58,7 @@ describe "The redo statement" do describe "in a method" do it "is invalid and raises a SyntaxError" do - lambda { + -> { eval("def m; redo; end") }.should raise_error(SyntaxError) end diff --git a/spec/ruby/language/regexp/anchors_spec.rb b/spec/ruby/language/regexp/anchors_spec.rb index c6a620a221..cdc06c0b4d 100644 --- a/spec/ruby/language/regexp/anchors_spec.rb +++ b/spec/ruby/language/regexp/anchors_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative '../fixtures/classes' describe "Regexps with anchors" do it "supports ^ (line start anchor)" do @@ -124,10 +124,10 @@ 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 @@ -145,10 +145,10 @@ describe "Regexps with anchors" do /foo\B/.match("foo").should be_nil /foo\B/.match("foo\n").should be_nil LanguageSpecs.white_spaces.scan(/./).each do |c| - /foo\B/.match("foo" + c).should be_nil + /foo\B/.match("foo" + c).should be_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 be_nil end /foo\B/.match("foo\0").should be_nil end diff --git a/spec/ruby/language/regexp/back-references_spec.rb b/spec/ruby/language/regexp/back-references_spec.rb index 607f4463fd..627c8daace 100644 --- a/spec/ruby/language/regexp/back-references_spec.rb +++ b/spec/ruby/language/regexp/back-references_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative '../fixtures/classes' describe "Regexps with back-references" do it "saves match data in the $~ pseudo-global variable" do @@ -7,7 +7,7 @@ describe "Regexps with back-references" do $~.to_a.should == ["ll"] end - it "saves captures in numbered $[1-9] variables" do + it "saves captures in numbered $[1-N] variables" do "1234567890" =~ /(1)(2)(3)(4)(5)(6)(7)(8)(9)(0)/ $~.to_a.should == ["1234567890", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0"] $1.should == "1" @@ -19,6 +19,16 @@ describe "Regexps with back-references" do $7.should == "7" $8.should == "8" $9.should == "9" + $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 @@ -45,4 +55,95 @@ describe "Regexps with back-references" do 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_error(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_error(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_error(RegexpError) + -> { Regexp.new("(?<a>a)\\k<1>") }.should raise_error(RegexpError) + -> { Regexp.new("(a)(?<a>a)\\1") }.should raise_error(RegexpError) + -> { Regexp.new("(a)(?<a>a)\\k<1>") }.should raise_error(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_error(RegexpError) + -> { Regexp.new("(?<a+b>a)\\k<a+b>") }.should raise_error(RegexpError) + -> { Regexp.new("(?<a+1>a)\\k<a+1>") }.should raise_error(RegexpError) + -> { Regexp.new("(?<a->a)\\k<a->") }.should raise_error(RegexpError) + -> { Regexp.new("(?<a-b>a)\\k<a-b>") }.should raise_error(RegexpError) + -> { Regexp.new("(?<a-1>a)\\k<a-1>") }.should raise_error(RegexpError) + + -> { Regexp.new("(?<a+>a)(?(<a+>)a|b)") }.should raise_error(RegexpError) + -> { Regexp.new("(?<a+b>a)(?(<a+b>)a|b)") }.should raise_error(RegexpError) + -> { Regexp.new("(?<a+1>a)(?(<a+1>)a|b)") }.should raise_error(RegexpError) + -> { Regexp.new("(?<a->a)(?(<a->)a|b)") }.should raise_error(RegexpError) + -> { Regexp.new("(?<a-b>a)(?(<a-b>)a|b)") }.should raise_error(RegexpError) + -> { Regexp.new("(?<a-1>a)(?(<a-1>)a|b)") }.should raise_error(RegexpError) + + -> { Regexp.new("(?<a+>a)(?('a+')a|b)") }.should raise_error(RegexpError) + -> { Regexp.new("(?<a+b>a)(?('a+b')a|b)") }.should raise_error(RegexpError) + -> { Regexp.new("(?<a+1>a)(?('a+1')a|b)") }.should raise_error(RegexpError) + -> { Regexp.new("(?<a->a)(?('a-')a|b)") }.should raise_error(RegexpError) + -> { Regexp.new("(?<a-b>a)(?('a-b')a|b)") }.should raise_error(RegexpError) + -> { Regexp.new("(?<a-1>a)(?('a-1')a|b)") }.should raise_error(RegexpError) + end end diff --git a/spec/ruby/language/regexp/character_classes_spec.rb b/spec/ruby/language/regexp/character_classes_spec.rb index ce66d8e65f..018757db41 100644 --- a/spec/ruby/language/regexp/character_classes_spec.rb +++ b/spec/ruby/language/regexp/character_classes_spec.rb @@ -1,6 +1,6 @@ # coding: utf-8 -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative '../fixtures/classes' describe "Regexp with character classes" do it "supports \\w (word character)" 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 - lambda { eval('/[[:alpha:]-[:digit:]]/') }.should raise_error(SyntaxError) # can't use character class as a start value of range + -> { eval('/[[:alpha:]-[:digit:]]/') }.should raise_error(SyntaxError) # can't use character class as a start value of range end it "matches ASCII characters with [[:ascii:]]" do @@ -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 be_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 be_nil end it "doesn't match Unicode control characters with [[:alpha:]]" do @@ -226,7 +226,7 @@ describe "Regexp with character classes" do 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 @@ -562,6 +562,13 @@ 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 end @@ -609,25 +616,32 @@ describe "Regexp with character classes" do "루비(Ruby)".match(/\p{Hangul}+/u).to_a.should == ["루비"] end - ruby_version_is "2.4" do - 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 == ["🦊"] + it "supports negated property condition" do + "a".match(eval("/\P{L}/")).should be_nil + "1".match(eval("/\P{N}/")).should be_nil + end + + it "raises a RegexpError for an unterminated unicode property" do + -> { Regexp.new('\p{') }.should raise_error(RegexpError) + end + + it "supports \\X (unicode 9.0 with UTR #51 workarounds)" do + # simple emoji without any fancy modifier or ZWJ + /\X/.match("\u{1F98A}").to_a.should == ["🦊"] - # skin tone modifier - /\X/.match("\u{1F918}\u{1F3FD}").to_a.should == ["🤘🏽"] + # skin tone modifier + /\X/.match("\u{1F918}\u{1F3FD}").to_a.should == ["🤘🏽"] - # emoji joined with ZWJ - /\X/.match("\u{1F3F3}\u{FE0F}\u{200D}\u{1F308}").to_a.should == ["🏳️🌈"] - /\X/.match("\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}").to_a.should == ["👩👩👧👦"] + # emoji joined with ZWJ + /\X/.match("\u{1F3F3}\u{FE0F}\u{200D}\u{1F308}").to_a.should == ["🏳️🌈"] + /\X/.match("\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}").to_a.should == ["👩👩👧👦"] - # without the ZWJ - /\X+/.match("\u{1F3F3}\u{FE0F}\u{1F308}").to_a.should == ["🏳️🌈"] - /\X+/.match("\u{1F469}\u{1F469}\u{1F467}\u{1F466}").to_a.should == ["👩👩👧👦"] + # without the ZWJ + /\X+/.match("\u{1F3F3}\u{FE0F}\u{1F308}").to_a.should == ["🏳️🌈"] + /\X+/.match("\u{1F469}\u{1F469}\u{1F467}\u{1F466}").to_a.should == ["👩👩👧👦"] - # both of the ZWJ combined - /\X+/.match("\u{1F3F3}\u{FE0F}\u{200D}\u{1F308}\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}") - .to_a.should == ["🏳️🌈👩👩👧👦"] - end + # both of the ZWJ combined + /\X+/.match("\u{1F3F3}\u{FE0F}\u{200D}\u{1F308}\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}") + .to_a.should == ["🏳️🌈👩👩👧👦"] end end 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 1f62244a28..ceb9cf823a 100644 --- a/spec/ruby/language/regexp/encoding_spec.rb +++ b/spec/ruby/language/regexp/encoding_spec.rb @@ -1,21 +1,21 @@ -# -*- encoding: binary -*- -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/classes', __FILE__) +# 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,35 +38,47 @@ 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 - it 'uses ASCII-8BIT as /n encoding if not all chars are 7-bit' do - /\xFF/n.encoding.should == Encoding::ASCII_8BIT + it 'uses BINARY when is not initialized' do + Regexp.allocate.encoding.should == Encoding::BINARY + end + + it 'uses BINARY as /n encoding if not all chars are 7-bit' do + /\xFF/n.encoding.should == Encoding::BINARY end it 'preserves US-ASCII as /n encoding through interpolation if all chars are 7-bit' do /.#{/./}/n.encoding.should == Encoding::US_ASCII end - it 'preserves ASCII-8BIT as /n encoding through interpolation if all chars are 7-bit' do - /\xFF#{/./}/n.encoding.should == Encoding::ASCII_8BIT + it 'preserves BINARY as /n encoding through interpolation if all chars are 7-bit' do + /\xFF#{/./}/n.encoding.should == Encoding::BINARY 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 @@ -78,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 @@ -100,4 +112,41 @@ describe "Regexps with encoding modifiers" do it "selects last of multiple encoding specifiers" do /foo/ensuensuens.should == /foo/s end + + it "raises Encoding::CompatibilityError when trying match against different encodings" do + -> { /\A[[:space:]]*\z/.match(" ".encode("UTF-16LE")) }.should raise_error(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) + end + + it "raises Encoding::CompatibilityError when trying =~ against different encodings" do + -> { /\A[[:space:]]*\z/ =~ " ".encode("UTF-16LE") }.should raise_error(Encoding::CompatibilityError) + end + + it "raises Encoding::CompatibilityError when the regexp has a fixed, non-ASCII-compatible encoding" do + -> { Regexp.new("".dup.force_encoding("UTF-16LE"), Regexp::FIXEDENCODING) =~ " ".encode("UTF-8") }.should raise_error(Encoding::CompatibilityError) + end + + it "raises Encoding::CompatibilityError when the regexp has a fixed encoding and the match string has non-ASCII characters" do + -> { Regexp.new("".dup.force_encoding("US-ASCII"), Regexp::FIXEDENCODING) =~ "\303\251".dup.force_encoding('UTF-8') }.should raise_error(Encoding::CompatibilityError) + end + + it "raises ArgumentError when trying to match a broken String" do + s = "\x80".dup.force_encoding('UTF-8') + -> { s =~ /./ }.should raise_error(ArgumentError, "invalid byte sequence in UTF-8") + end + + it "computes the Regexp Encoding for each interpolated Regexp instance" do + make_regexp = -> str { /#{str}/ } + + r = make_regexp.call("été".dup.force_encoding(Encoding::UTF_8)) + r.should.fixed_encoding? + r.encoding.should == 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 end diff --git a/spec/ruby/language/regexp/escapes_spec.rb b/spec/ruby/language/regexp/escapes_spec.rb index 50ac22e51e..541998b937 100644 --- a/spec/ruby/language/regexp/escapes_spec.rb +++ b/spec/ruby/language/regexp/escapes_spec.rb @@ -1,9 +1,11 @@ -# -*- encoding: binary -*- -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/classes', __FILE__) +# 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 /\x0AA/.match("\nA").to_a.should == ["\nA"] /\xAG/.match("\nG").to_a.should == ["\nG"] # Non-matches - lambda { eval('/\xG/') }.should raise_error(SyntaxError) + -> { eval('/\xG/') }.should raise_error(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"] @@ -67,15 +139,31 @@ describe "Regexps with escape characters" do /\cJ/.match("\r").should be_nil # Parsing precedence - /\cJ+/.match("\n\n").to_a.should == ["\n\n"] # Quantifers apply to entire escape sequence + /\cJ+/.match("\n\n").to_a.should == ["\n\n"] # Quantifiers apply to entire escape sequence /\\cJ/.match("\\cJ").to_a.should == ["\\cJ"] - lambda { eval('/[abc\x]/') }.should raise_error(SyntaxError) # \x is treated as a escape sequence even inside a character class + -> { eval('/[abc\x]/') }.should raise_error(SyntaxError) # \x is treated as a escape sequence even inside a character class # Syntax error - lambda { eval('/\c/') }.should raise_error(SyntaxError) + -> { eval('/\c/') }.should raise_error(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 443cab7ee0..313858f714 100644 --- a/spec/ruby/language/regexp/grouping_spec.rb +++ b/spec/ruby/language/regexp/grouping_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative '../fixtures/classes' describe "Regexps with grouping" do it "support ()" do @@ -12,7 +12,7 @@ describe "Regexps with grouping" do end it "raises a SyntaxError when parentheses aren't balanced" do - lambda { eval "/(hay(st)ack/" }.should raise_error(SyntaxError) + -> { eval "/(hay(st)ack/" }.should raise_error(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_error(RegexpError) + -> { Regexp.new("(?<-a>a)") }.should raise_error(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 5536c718f1..6951fd38ca 100644 --- a/spec/ruby/language/regexp/interpolation_spec.rb +++ b/spec/ruby/language/regexp/interpolation_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative '../fixtures/classes' describe "Regexps with interpolation" do @@ -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 = "" - lambda { /(#{s}/ }.should raise_error(RegexpError) + -> { /(#{s}/ }.should raise_error(RegexpError) s = "(" - lambda { /#{s}/ }.should raise_error(RegexpError) + -> { /#{s}/ }.should raise_error(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 a7052a941c..2f5522bc8a 100644 --- a/spec/ruby/language/regexp/modifiers_spec.rb +++ b/spec/ruby/language/regexp/modifiers_spec.rb @@ -1,7 +1,7 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative '../fixtures/classes' -describe "Regexps with modifers" do +describe "Regexps with modifiers" do it "supports /i (case-insensitive)" do /foo/i.match("FOO").to_a.should == ["FOO"] end @@ -36,14 +36,12 @@ describe "Regexps with modifers" do /foo/imox.match("foo").to_a.should == ["foo"] /foo/imoximox.match("foo").to_a.should == ["foo"] - lambda { eval('/foo/a') }.should raise_error(SyntaxError) + -> { eval('/foo/a') }.should raise_error(SyntaxError) end - ruby_version_is "2.4" do - it "supports (?~) (absent operator)" do - Regexp.new("(?~foo)").match("hello").to_a.should == ["hello"] - "foo".scan(Regexp.new("(?~foo)")).should == ["fo","o",""] - end + it "supports (?~) (absent operator)" do + Regexp.new("(?~foo)").match("hello").to_a.should == ["hello"] + "foo".scan(Regexp.new("(?~foo)")).should == ["fo","o",""] end it "supports (?imx-imx) (inline modifiers)" do @@ -78,7 +76,7 @@ describe "Regexps with modifers" do /(?i-i)foo/.match("FOO").should be_nil /(?ii)foo/.match("FOO").to_a.should == ["FOO"] /(?-)foo/.match("foo").to_a.should == ["foo"] - lambda { eval('/(?o)/') }.should raise_error(SyntaxError) + -> { eval('/(?o)/') }.should raise_error(SyntaxError) end it "supports (?imx-imx:expr) (scoped inline modifiers)" do @@ -98,7 +96,7 @@ describe "Regexps with modifers" do /(?i-i:foo)/.match("FOO").should be_nil /(?ii:foo)/.match("FOO").to_a.should == ["FOO"] /(?-:)foo/.match("foo").to_a.should == ["foo"] - lambda { eval('/(?o:)/') }.should raise_error(SyntaxError) + -> { eval('/(?o:)/') }.should raise_error(SyntaxError) end it "supports . with /m" do @@ -106,7 +104,7 @@ describe "Regexps with modifers" do /./m.match("\n").to_a.should == ["\n"] end - it "supports ASII/Unicode modifiers" do + it "supports ASCII/Unicode modifiers" do eval('/(?a)[[:alpha:]]+/').match("a\u3042").to_a.should == ["a"] eval('/(?d)[[:alpha:]]+/').match("a\u3042").to_a.should == ["a\u3042"] eval('/(?u)[[:alpha:]]+/').match("a\u3042").to_a.should == ["a\u3042"] diff --git a/spec/ruby/language/regexp/repetition_spec.rb b/spec/ruby/language/regexp/repetition_spec.rb index 2fc8a74a47..d76619688f 100644 --- a/spec/ruby/language/regexp/repetition_spec.rb +++ b/spec/ruby/language/regexp/repetition_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/classes', __FILE__) +require_relative '../../spec_helper' +require_relative '../fixtures/classes' describe "Regexps with repetition" do it "supports * (0 or more of previous subexpression)" do @@ -34,24 +34,105 @@ describe "Regexps with repetition" do /.([0-9]){3,5}?foo/.match("9876543210foo").to_a.should == ["543210foo", "0"] end - ruby_version_is ""..."2.4" do - it "does not treat {m,n}+ as possessive" do + it "does not treat {m,n}+ as possessive" do + -> { @regexp = eval "/foo(A{0,1}+)Abar/" - @regexp.match("fooAAAbar").to_a.should == ["fooAAAbar", "AA"] - end - end - - ruby_version_is "2.4" do - it "does not treat {m,n}+ as possessive" do - -> { - @regexp = eval "/foo(A{0,1}+)Abar/" - }.should complain(/nested repeat operato/) - @regexp.match("fooAAAbar").to_a.should == ["fooAAAbar", "AA"] - end + }.should complain(/nested repeat operator/) + @regexp.match("fooAAAbar").to_a.should == ["fooAAAbar", "AA"] end it "supports ? (0 or 1 of previous subexpression)" 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 d4b0c81128..ce344b5b05 100644 --- a/spec/ruby/language/regexp_spec.rb +++ b/spec/ruby/language/regexp_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/classes' describe "Literal Regexps" do it "matches against $_ (last input) in a conditional if no explicit matchee provided" do @@ -18,6 +18,10 @@ describe "Literal Regexps" do /Hello/.should be_kind_of(Regexp) end + it "is frozen" do + /Hello/.should.frozen? + end + it "caches the Regexp object" do rs = [] 2.times do |i| @@ -27,7 +31,7 @@ describe "Literal Regexps" do end it "throws SyntaxError for malformed literals" do - lambda { eval('/(/') }.should raise_error(SyntaxError) + -> { eval('/(/') }.should raise_error(SyntaxError) end ############################################################################# @@ -54,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| - lambda { eval("%r#{p0} foo #{p0}") }.should raise_error(SyntaxError) + -> { eval("%r#{p0} foo #{p0}") }.should raise_error(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 - lambda { eval('%ra foo a') }.should raise_error(SyntaxError) + -> { eval('%ra foo a') }.should raise_error(SyntaxError) end it "disallows spaces after %r and delimiter" do - lambda { eval('%r !foo!') }.should raise_error(SyntaxError) + -> { eval('%r !foo!') }.should raise_error(SyntaxError) end it "allows unescaped / to be used with %r" do @@ -90,14 +94,13 @@ describe "Literal Regexps" do /./.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 possesive + /(?>foo*)obar/.match("foooooooobar").should be_nil # it is possessive end it "supports (?# )" do @@ -109,6 +112,13 @@ describe "Literal Regexps" do /foo.(?<=\d)/.match("fooA foo1").to_a.should == ["foo1"] end + 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 /foo.(?<!\d)/.match("foo1 fooA").to_a.should == ["fooA"] end @@ -147,4 +157,11 @@ describe "Literal Regexps" do pattern.should_not =~ 'fooF' pattern.should_not =~ 'T' 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 + char_uppercase = "\u{104B0}" # OSAGE CAPITAL LETTER A + /[[:upper:]]/.match(char_uppercase).to_s.should == char_uppercase + end end diff --git a/spec/ruby/language/rescue_spec.rb b/spec/ruby/language/rescue_spec.rb index 0dc8894740..6be3bfd023 100644 --- a/spec/ruby/language/rescue_spec.rb +++ b/spec/ruby/language/rescue_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/rescue', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/rescue' class SpecificExampleException < StandardError end @@ -23,11 +23,127 @@ describe "The rescue keyword" do end.should == :caught end - it "can capture the raised exception in a local variable" do - begin - raise SpecificExampleException, "some text" - rescue SpecificExampleException => e - e.message.should == "some text" + describe 'can capture the raised exception' do + before :all do + require_relative 'fixtures/rescue_captures' + end + + it 'in a local variable' do + RescueSpecs::LocalVariableCaptor.should_capture_exception + end + + it 'in a class variable' do + RescueSpecs::ClassVariableCaptor.should_capture_exception + end + + it 'in a constant' do + RescueSpecs::ConstantCaptor.should_capture_exception + end + + it 'in a global variable' do + RescueSpecs::GlobalVariableCaptor.should_capture_exception + end + + it 'in an instance variable' do + RescueSpecs::InstanceVariableCaptor.should_capture_exception + end + + it 'using a safely navigated setter method' 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 be_nil + end + + it 'using a setter method' do + RescueSpecs::SetterCaptor.should_capture_exception + end + + it 'using a square brackets setter' do + RescueSpecs::SquareBracketsCaptor.should_capture_exception + 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 @@ -54,7 +170,7 @@ describe "The rescue keyword" do end it "can rescue multiple raised exceptions with a single rescue block" do - [lambda{raise ArbitraryException}, lambda{raise SpecificExampleException}].map do |block| + [->{raise ArbitraryException}, ->{raise SpecificExampleException}].map do |block| begin block.call rescue SpecificExampleException, ArbitraryException @@ -72,7 +188,7 @@ describe "The rescue keyword" do end caught_it.should be_true caught = [] - [lambda{raise ArbitraryException}, lambda{raise SpecificExampleException}].each do |block| + [->{raise ArbitraryException}, ->{raise SpecificExampleException}].each do |block| begin block.call rescue *exception_list @@ -85,6 +201,18 @@ describe "The rescue keyword" do end end + it "converts the splatted list of exceptions using #to_a" do + exceptions = mock("to_a") + exceptions.should_receive(:to_a).and_return(exception_list) + caught_it = false + begin + raise SpecificExampleException, "not important" + rescue *exceptions + caught_it = true + end + caught_it.should be_true + end + it "can combine a splatted list of exceptions with a literal list of exceptions" do caught_it = false begin @@ -94,7 +222,7 @@ describe "The rescue keyword" do end caught_it.should be_true caught = [] - [lambda{raise ArbitraryException}, lambda{raise SpecificExampleException}].each do |block| + [->{raise ArbitraryException}, ->{raise SpecificExampleException}].each do |block| begin block.call rescue ArbitraryException, *exception_list @@ -108,7 +236,7 @@ describe "The rescue keyword" do end it "will only rescue the specified exceptions when doing a splat rescue" do - lambda do + -> do begin raise OtherCustomException, "not rescued!" rescue *exception_list @@ -142,6 +270,19 @@ describe "The rescue keyword" do ScratchPad.recorded.should == [:standard_error] end + it "rescues the exception in the deepest rescue block declared to handle the appropriate exception type" do + begin + begin + RescueSpecs.raise_standard_error + rescue ArgumentError + end + rescue StandardError => e + e.backtrace.first.should =~ /:in [`'](?:RescueSpecs\.)?raise_standard_error'/ + else + fail("exception wasn't handled by the correct rescue block") + end + end + it "will execute an else block only if no exceptions were raised" do result = begin ScratchPad << :one @@ -195,8 +336,8 @@ describe "The rescue keyword" do ScratchPad.recorded.should == [:one, :else_ran, :ensure_ran, :outside_begin] end - it "will execute an else block even without rescue and ensure" do - lambda { + it "raises SyntaxError when else is used without rescue and ensure" do + -> { eval <<-ruby begin ScratchPad << :begin @@ -204,9 +345,7 @@ describe "The rescue keyword" do ScratchPad << :else end ruby - }.should complain(/else without rescue is useless/) - - ScratchPad.recorded.should == [:begin, :else] + }.should raise_error(SyntaxError, /else without rescue is useless/) end it "will not execute an else block if an exception was raised" do @@ -265,7 +404,7 @@ describe "The rescue keyword" do end it "will not rescue errors raised in an else block in the rescue block above it" do - lambda do + -> do begin ScratchPad << :one rescue Exception @@ -300,7 +439,7 @@ describe "The rescue keyword" do [ Exception.new, NoMemoryError.new, ScriptError.new, SecurityError.new, SignalException.new('INT'), SystemExit.new, SystemStackError.new ].each do |exception| - lambda { + -> { begin raise exception rescue @@ -332,7 +471,7 @@ describe "The rescue keyword" do it "only accepts Module or Class in rescue clauses" do rescuer = 42 - lambda { + -> { begin raise "error" rescue rescuer @@ -344,7 +483,7 @@ describe "The rescue keyword" do it "only accepts Module or Class in splatted rescue clauses" do rescuer = [42] - lambda { + -> { begin raise "error" rescue *rescuer @@ -355,11 +494,23 @@ describe "The rescue keyword" do end it "evaluates rescue expressions only when needed" do - invalid_rescuer = Object.new begin - :foo - rescue invalid_rescuer - end.should == :foo + ScratchPad << :foo + rescue -> { ScratchPad << :bar; StandardError }.call + end + + ScratchPad.recorded.should == [:foo] + end + + it "suppresses exception from block when raises one from rescue expression" do + -> { + begin + raise "from block" + rescue (raise "from rescue expression") + end + }.should raise_error(RuntimeError, "from rescue expression") { |e| + e.cause.message.should == "from block" + } end it "should splat the handling Error classes" do @@ -383,7 +534,7 @@ describe "The rescue keyword" do end it "does not allow rescue in {} block" do - lambda { + -> { eval <<-ruby lambda { raise SpecificExampleException @@ -394,35 +545,40 @@ describe "The rescue keyword" do }.should raise_error(SyntaxError) end - ruby_version_is "2.5" do - it "allows rescue in 'do end' block" do - lambda = eval <<-ruby - lambda do - raise SpecificExampleException - rescue SpecificExampleException - ScratchPad << :caught - end.call - ruby + it "allows rescue in 'do end' block" do + lambda = eval <<-ruby + lambda do + raise SpecificExampleException + rescue SpecificExampleException + ScratchPad << :caught + end.call + ruby - ScratchPad.recorded.should == [:caught] - end + ScratchPad.recorded.should == [:caught] end - ruby_version_is ""..."2.4" do - it "fails when using 'rescue' in method arguments" do - lambda { eval '1.+ (1 rescue 1)' }.should raise_error(SyntaxError) - end + it "allows 'rescue' in method arguments" do + two = eval '1.+ (raise("Error") rescue 1)' + two.should == 2 end - ruby_version_is "2.4" do - it "allows 'rescue' in method arguments" do - two = eval '1.+ (raise("Error") rescue 1)' - two.should == 2 - 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 == 2 + end - it "requires the 'rescue' in method arguments to be wrapped in parens" do - lambda { eval '1.+(1 rescue 1)' }.should raise_error(SyntaxError) - eval('1.+((1 rescue 1))').should == 2 + 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 @@ -433,7 +589,7 @@ describe "The rescue keyword" do end it "doesn't except rescue expression" do - lambda { + -> { eval <<-ruby a = 1 rescue RuntimeError 2 ruby @@ -444,9 +600,17 @@ describe "The rescue keyword" do a = raise(StandardError) rescue 1 a.should == 1 - lambda { + -> { a = raise(Exception) rescue 1 }.should raise_error(Exception) end + + it "rescues with multiple assignment" do + + a, b = raise rescue [1, 2] + + 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..6c40e34ccc --- /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_error(SyntaxError) + #{name} = :local_variable + RUBY + end + + if name == "defined?" + it "can't be used as an instance variable name" do + -> { eval(<<~RUBY) }.should raise_error(SyntaxError) + @#{name} = :instance_variable + RUBY + end + + it "can't be used as a class variable name" do + -> { eval(<<~RUBY) }.should raise_error(SyntaxError) + class C + @@#{name} = :class_variable + end + RUBY + end + + it "can't be used as a global variable name" do + -> { eval(<<~RUBY) }.should raise_error(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_error(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_error(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 96e69b763a..669d5f0ff5 100644 --- a/spec/ruby/language/retry_spec.rb +++ b/spec/ruby/language/retry_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "The retry statement" do it "re-executes the closest block" do @@ -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 - lambda { eval 'retry' }.should raise_error(SyntaxError) + it "raises a SyntaxError when used outside of a rescue statement" do + -> { eval 'retry' }.should raise_error(SyntaxError) + -> { eval 'begin; retry; end' }.should raise_error(SyntaxError) + -> { eval 'def m; retry; end' }.should raise_error(SyntaxError) + -> { eval 'module RetrySpecs; retry; end' }.should raise_error(SyntaxError) end end diff --git a/spec/ruby/language/return_spec.rb b/spec/ruby/language/return_spec.rb index ba4bbfb5f3..a62ed1242d 100644 --- a/spec/ruby/language/return_spec.rb +++ b/spec/ruby/language/return_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/return', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/return' describe "The return keyword" do it "returns any object directly" do @@ -159,7 +159,7 @@ describe "The return keyword" do end it "executes the ensure clause when begin/ensure are inside a lambda" do - lambda do + -> do begin return ensure @@ -176,15 +176,15 @@ describe "The return keyword" do end it "causes lambda to return nil if invoked without any arguments" do - lambda { return; 456 }.call.should be_nil + -> { return; 456 }.call.should be_nil end it "causes lambda to return nil if invoked with an empty expression" do - lambda { return (); 456 }.call.should be_nil + -> { return (); 456 }.call.should be_nil end it "causes lambda to return the value passed to return" do - lambda { return 123; 456 }.call.should == 123 + -> { return 123; 456 }.call.should == 123 end it "causes the method that lexically encloses the block to return" do @@ -250,233 +250,240 @@ describe "The return keyword" do end end - ruby_version_is '2.4.2' do - describe "at top level" do - before :each do - @filename = tmp("top_return.rb") - ScratchPad.record [] - end + describe "at top level" do + before :each do + @filename = tmp("top_return.rb") + ScratchPad.record [] + end - after do - rm_r @filename - end + after do + rm_r @filename + end - it "stops file execution" do - ruby_exe(<<-END_OF_CODE).should == "before return\n" - puts "before return" - return + it "stops file execution" do + ruby_exe(<<-END_OF_CODE).should == "before return\n" + puts "before return" + return + + puts "after return" + END_OF_CODE + + $?.exitstatus.should == 0 + end + + describe "within if" do + it "is allowed" do + File.write(@filename, <<-END_OF_CODE) + ScratchPad << "before if" + if true + return + end - puts "after return" + ScratchPad << "after if" END_OF_CODE - $?.exitstatus.should == 0 + load @filename + ScratchPad.recorded.should == ["before if"] end + end - describe "within if" do - it "is allowed" do - File.write(@filename, <<-END_OF_CODE) - ScratchPad << "before if" - if true - return - end + describe "within while loop" do + it "is allowed" do + File.write(@filename, <<-END_OF_CODE) + ScratchPad << "before while" + while true + return + end - ScratchPad << "after if" - END_OF_CODE + ScratchPad << "after while" + END_OF_CODE - load @filename - ScratchPad.recorded.should == ["before if"] - end + load @filename + ScratchPad.recorded.should == ["before while"] end + end - describe "within while loop" do - it "is allowed" do - File.write(@filename, <<-END_OF_CODE) - ScratchPad << "before while" - while true - return - end + describe "within a begin" do + it "is allowed in begin block" do + File.write(@filename, <<-END_OF_CODE) + ScratchPad << "before begin" + begin + return + end - ScratchPad << "after while" - END_OF_CODE + ScratchPad << "after begin" + END_OF_CODE - load @filename - ScratchPad.recorded.should == ["before while"] - end + load @filename + ScratchPad.recorded.should == ["before begin"] end - describe "within a begin" do - it "is allowed in begin block" do - File.write(@filename, <<-END_OF_CODE) - ScratchPad << "before begin" - begin - return - end + it "is allowed in ensure block" do + File.write(@filename, <<-END_OF_CODE) + ScratchPad << "before begin" + begin + ensure + return + end - ScratchPad << "after begin" - END_OF_CODE + ScratchPad << "after begin" + END_OF_CODE - load @filename - ScratchPad.recorded.should == ["before begin"] - end + load @filename + ScratchPad.recorded.should == ["before begin"] + end - it "is allowed in ensure block" do - File.write(@filename, <<-END_OF_CODE) - ScratchPad << "before begin" - begin - ensure - return - end + it "is allowed in rescue block" do + File.write(@filename, <<-END_OF_CODE) + ScratchPad << "before begin" + begin + raise + rescue RuntimeError + return + end - ScratchPad << "after begin" - END_OF_CODE + ScratchPad << "after begin" + END_OF_CODE - load @filename - ScratchPad.recorded.should == ["before begin"] - end + load @filename + ScratchPad.recorded.should == ["before begin"] + end - it "is allowed in rescue block" do - File.write(@filename, <<-END_OF_CODE) - ScratchPad << "before begin" - begin - raise - rescue RuntimeError - return - end + it "fires ensure block before returning" do + ruby_exe(<<-END_OF_CODE).should == "within ensure\n" + begin + return + ensure + puts "within ensure" + end - ScratchPad << "after begin" - END_OF_CODE + puts "after begin" + END_OF_CODE + end - load @filename - ScratchPad.recorded.should == ["before begin"] - end + it "fires ensure block before returning while loads file" do + File.write(@filename, <<-END_OF_CODE) + ScratchPad << "before begin" + begin + return + ensure + ScratchPad << "within ensure" + end - it "fires ensure block before returning" do - ruby_exe(<<-END_OF_CODE).should == "within ensure\n" - begin - return - ensure - puts "within ensure" - end + ScratchPad << "after begin" + END_OF_CODE - puts "after begin" - END_OF_CODE - end + load @filename + ScratchPad.recorded.should == ["before begin", "within ensure"] + end - ruby_bug "#14061", "2.4"..."2.6" do - it "fires ensure block before returning while loads file" do - File.write(@filename, <<-END_OF_CODE) - ScratchPad << "before begin" - begin - return - ensure - ScratchPad << "within ensure" - end - - ScratchPad << "after begin" - END_OF_CODE - - load @filename - ScratchPad.recorded.should == ["before begin", "within ensure"] + it "swallows exception if returns in ensure block" do + File.write(@filename, <<-END_OF_CODE) + begin + raise + ensure + ScratchPad << "before return" + return end - end + END_OF_CODE - it "swallows exception if returns in ensure block" do - File.write(@filename, <<-END_OF_CODE) - begin - raise - ensure - ScratchPad << "before return" - return - end - END_OF_CODE - - load @filename - ScratchPad.recorded.should == ["before return"] - end + load @filename + ScratchPad.recorded.should == ["before return"] end + end - describe "within a block" do - it "is allowed" do - File.write(@filename, <<-END_OF_CODE) - ScratchPad << "before call" - proc { return }.call + describe "within a block" do + it "is allowed" do + File.write(@filename, <<-END_OF_CODE) + ScratchPad << "before call" + proc { return }.call - ScratchPad << "after call" - END_OF_CODE + ScratchPad << "after call" + END_OF_CODE - load @filename - ScratchPad.recorded.should == ["before call"] - end + load @filename + ScratchPad.recorded.should == ["before call"] end + end - describe "within a class" do - ruby_version_is ""..."2.5" do - it "is allowed" do - File.write(@filename, <<-END_OF_CODE) - class A - ScratchPad << "before return" - return - - ScratchPad << "after return" - end - END_OF_CODE + describe "within a class" do + it "raises a SyntaxError" do + File.write(@filename, <<-END_OF_CODE) + class ReturnSpecs::A + ScratchPad << "before return" + return - load @filename - ScratchPad.recorded.should == ["before return"] + ScratchPad << "after return" end - end - - ruby_version_is "2.5" do - it "raises a SyntaxError" do - File.write(@filename, <<-END_OF_CODE) - class A - ScratchPad << "before return" - return - - ScratchPad << "after return" - end - END_OF_CODE + END_OF_CODE - -> { load @filename }.should raise_error(SyntaxError) - end - end + -> { load @filename }.should raise_error(SyntaxError) end + end - describe "file loading" do - it "stops file loading and execution" do - File.write(@filename, <<-END_OF_CODE) + describe "within a block within a class" do + it "is not allowed" do + File.write(@filename, <<-END_OF_CODE) + class ReturnSpecs::A ScratchPad << "before return" - return + 1.times { return } ScratchPad << "after return" - END_OF_CODE + end + END_OF_CODE - load @filename - ScratchPad.recorded.should == ["before return"] - end + -> { load @filename }.should raise_error(LocalJumpError) end + end - describe "file requiring" do - it "stops file loading and execution" do - File.write(@filename, <<-END_OF_CODE) - ScratchPad << "before return" + describe "within BEGIN" do + it "is allowed" do + File.write(@filename, <<-END_OF_CODE) + BEGIN { + ScratchPad << "before call" return - ScratchPad << "after return" - END_OF_CODE + ScratchPad << "after call" + } + END_OF_CODE - require @filename - ScratchPad.recorded.should == ["before return"] - end + load @filename + ScratchPad.recorded.should == ["before call"] end + end - describe "return with argument" do - # https://bugs.ruby-lang.org/issues/14062 - it "does not affect exit status" do - ruby_exe(<<-END_OF_CODE).should == "" - return 10 - END_OF_CODE + describe "file loading" do + it "stops file loading and execution" do + File.write(@filename, <<-END_OF_CODE) + ScratchPad << "before return" + return + ScratchPad << "after return" + END_OF_CODE - $?.exitstatus.should == 0 - end + load @filename + ScratchPad.recorded.should == ["before return"] + end + end + + describe "file requiring" do + it "stops file loading and execution" do + File.write(@filename, <<-END_OF_CODE) + ScratchPad << "before return" + return + ScratchPad << "after return" + END_OF_CODE + + require @filename + ScratchPad.recorded.should == ["before return"] + end + end + + describe "return with argument" 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 + + 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 a8b29dc5a3..b1e28c3963 100644 --- a/spec/ruby/language/safe_navigator_spec.rb +++ b/spec/ruby/language/safe_navigator_spec.rb @@ -1,101 +1,147 @@ -require File.expand_path("../../spec_helper", __FILE__) +require_relative '../spec_helper' -ruby_version_is "2.3" do - describe "Safe navigator" do - it "requires a method name to be provided" do - lambda { eval("obj&. {}") }.should raise_error(SyntaxError) +describe "Safe navigator" do + it "requires a method name to be provided" do + -> { eval("obj&. {}") }.should raise_error(SyntaxError) + end + + context "when context is nil" do + it "always returns nil" do + nil&.unknown.should == nil + [][10]&.unknown.should == nil end - context "when context is nil" do - it "always returns nil" do - eval("nil&.unknown").should == nil - eval("[][10]&.unknown").should == nil - end + it "can be chained" do + nil&.one&.two&.three.should == nil + end - it "can be chained" do - eval("nil&.one&.two&.three").should == nil - end + it "doesn't evaluate arguments" do + obj = Object.new + obj.should_not_receive(:m) + nil&.unknown(obj.m) { obj.m } + end + end - it "doesn't evaluate arguments" do - obj = Object.new - obj.should_not_receive(:m) - eval("nil&.unknown(obj.m) { obj.m }") - end + context "when context is false" do + it "calls the method" do + false&.to_s.should == "false" + + -> { false&.unknown }.should raise_error(NoMethodError) end + end - context "when context is false" do - it "calls the method" do - eval("false&.to_s").should == "false" + context "when context is truthy" do + it "calls the method" do + 1&.to_s.should == "1" - lambda { eval("false&.unknown") }.should raise_error(NoMethodError) - end + -> { 1&.unknown }.should raise_error(NoMethodError) end + end - context "when context is truthy" do - it "calls the method" do - eval("1&.to_s").should == "1" + it "takes a list of arguments" do + [1,2,3]&.first(2).should == [1,2] + end + + it "takes a block" do + [1,2]&.map { |i| i * 2 }.should == [2, 4] + end - lambda { eval("1&.unknown") }.should raise_error(NoMethodError) + it "allows assignment methods" do + klass = Class.new do + attr_reader :foo + def foo=(val) + @foo = val + 42 end end + obj = klass.new - it "takes a list of arguments" do - eval("[1,2,3]&.first(2)").should == [1,2] - end + (obj&.foo = 3).should == 3 + obj.foo.should == 3 - it "takes a block" do - eval("[1,2]&.map { |i| i * 2 }").should == [2, 4] - end + obj = nil + (obj&.foo = 3).should == nil + end - it "allows assignment methods" do - klass = Class.new do - attr_reader :foo - def foo=(val) - @foo = val - 42 - end - end - obj = klass.new + it "allows assignment operators" do + klass = Class.new do + attr_reader :m - eval("obj&.foo = 3").should == 3 - obj.foo.should == 3 + def initialize + @m = 0 + end - obj = nil - eval("obj&.foo = 3").should == nil + def m=(v) + @m = v + 42 + end end - it "allows assignment operators" do - klass = Class.new do - attr_accessor :m + obj = klass.new - def initialize - @m = 0 - end - end + obj&.m += 3 + obj.m.should == 3 + + obj = nil + (obj&.m += 3).should == nil + end - obj = klass.new + it "allows ||= operator" do + klass = Class.new do + attr_reader :m - eval("obj&.m += 3") - obj.m.should == 3 + def initialize + @m = false + end - obj = nil - eval("obj&.m += 3").should == nil + def m=(v) + @m = v + 42 + end end - it "does not call the operator method lazily with an assignment operator" do - klass = Class.new do - attr_writer :foo - def foo - nil - 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 - obj = klass.new + end + + obj = klass.new + + (obj&.m &&= false).should == false + obj.m.should == false - lambda { - eval("obj&.foo += 3") - }.should raise_error(NoMethodError) { |e| - e.name.should == :+ - } + obj = nil + (obj&.m &&= false).should == nil + obj.should == nil + end + + it "does not call the operator method lazily with an assignment operator" do + klass = Class.new do + attr_writer :foo + def foo + nil + end end + obj = klass.new + + -> { + obj&.foo += 3 + }.should raise_error(NoMethodError) { |e| + e.name.should == :+ + } end end diff --git a/spec/ruby/language/safe_spec.rb b/spec/ruby/language/safe_spec.rb new file mode 100644 index 0000000000..03ae96148e --- /dev/null +++ b/spec/ruby/language/safe_spec.rb @@ -0,0 +1,11 @@ +require_relative '../spec_helper' + +describe "The $SAFE variable" do + 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 646a700785..5d6340ffc5 100644 --- a/spec/ruby/language/send_spec.rb +++ b/spec/ruby/language/send_spec.rb @@ -1,15 +1,15 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/send', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/send' # Why so many fixed arg tests? JRuby and I assume other Ruby impls have # separate call paths for simple fixed arity methods. Testing up to five # will verify special and generic arity code paths for all impls. # # Method naming conventions: -# M - Manditory Args +# M - Mandatory Args # O - Optional Arg # R - Rest Arg -# Q - Post Manditory Args +# Q - Post Mandatory Args specs = LangSendSpecs @@ -20,7 +20,7 @@ describe "Invoking a method" do end it "raises ArgumentError if the method has a positive arity" do - lambda { + -> { specs.fooM1 }.should raise_error(ArgumentError) end @@ -36,14 +36,14 @@ describe "Invoking a method" do end it "raises ArgumentError if the methods arity doesn't match" do - lambda { + -> { specs.fooM1(1,2) }.should raise_error(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 @@ -52,7 +52,7 @@ describe "Invoking a method" do end it "raises ArgumentError if extra arguments are passed" do - lambda { + -> { specs.fooM0O1(2,3) }.should raise_error(ArgumentError) end @@ -64,13 +64,13 @@ describe "Invoking a method" do end it "raises an ArgumentError if there are no values for the mandatory args" do - lambda { + -> { specs.fooM1O1 }.should raise_error(ArgumentError) end it "raises an ArgumentError if too many values are passed" do - lambda { + -> { specs.fooM1O1(1,2,3) }.should raise_error(ArgumentError) end @@ -106,8 +106,26 @@ 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_error(TypeError, "can't convert LangSendSpecs::RawToProc to 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_error(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 - lambda { + -> { eval "specs.oneb(10, &l){ 42 }" }.should raise_error(SyntaxError) end @@ -195,12 +213,20 @@ describe "Invoking a method" do end it "raises NameError if invoked as a vcall" do - lambda { no_such_method }.should raise_error NameError + -> { no_such_method }.should raise_error NameError + 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") } end it "raises NoMethodError if invoked as an unambiguous method call" do - lambda { no_such_method() }.should raise_error NoMethodError - lambda { no_such_method(1,2,3) }.should raise_error NoMethodError + -> { no_such_method() }.should raise_error NoMethodError + -> { no_such_method(1,2,3) }.should raise_error NoMethodError + 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") } end end @@ -250,10 +276,10 @@ describe "Invoking a private setter method" do end describe "Invoking a private getter method" do - it "does not permit self as a receiver" do + it "permits self as a receiver" do receiver = LangSendSpecs::PrivateGetter.new - lambda { receiver.call_self_foo }.should raise_error(NoMethodError) - lambda { receiver.call_self_foo_or_equals(6) }.should raise_error(NoMethodError) + receiver.call_self_foo_or_equals(6) + receiver.call_self_foo.should == 6 end end @@ -403,6 +429,29 @@ describe "Invoking a method" do specs.rest_len(0,*a,4,*5,6,7,*c,-1).should == 11 end + 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] + 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 + def self.m(*args, &block) + [args, block] + end + + args = [1, nil] + m(*args, 2, &args.pop).should == [[1, nil, 2], nil] + end + it "expands an array to arguments grouped in parentheses" do specs.destructure2([40,2]).should == 42 end diff --git a/spec/ruby/language/singleton_class_spec.rb b/spec/ruby/language/singleton_class_spec.rb index 837f479440..45e1f7f3ad 100644 --- a/spec/ruby/language/singleton_class_spec.rb +++ b/spec/ruby/language/singleton_class_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../../fixtures/class', __FILE__) +require_relative '../spec_helper' +require_relative '../fixtures/class' describe "A singleton class" do it "is TrueClass for true" do @@ -14,12 +14,12 @@ describe "A singleton class" do nil.singleton_class.should == NilClass end - it "raises a TypeError for Fixnum's" do - lambda { 1.singleton_class }.should raise_error(TypeError) + it "raises a TypeError for Integer's" do + -> { 1.singleton_class }.should raise_error(TypeError) end it "raises a TypeError for symbols" do - lambda { :symbol.singleton_class }.should raise_error(TypeError) + -> { :symbol.singleton_class }.should raise_error(TypeError) end it "is a singleton Class instance" do @@ -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 - lambda { bignum_value.singleton_class.superclass.should == Bignum }.should raise_error(TypeError) + -> { bignum_value.singleton_class }.should raise_error(TypeError) end end @@ -112,11 +112,11 @@ describe "A constant on a singleton class" do class << @object CONST end - lambda { CONST }.should raise_error(NameError) + -> { CONST }.should raise_error(NameError) end it "cannot be accessed via object::CONST" do - lambda do + -> do @object::CONST end.should raise_error(TypeError) end @@ -127,7 +127,7 @@ describe "A constant on a singleton class" do CONST = 100 end - lambda do + -> do @object::CONST end.should raise_error(NameError) end @@ -143,7 +143,7 @@ describe "A constant on a singleton class" do it "is not preserved when the object is duped" do @object = @object.dup - lambda do + -> do class << @object; CONST; end end.should raise_error(NameError) end @@ -280,14 +280,38 @@ end describe "Instantiating a singleton class" do it "raises a TypeError when new is called" do - lambda { + -> { Object.new.singleton_class.new }.should raise_error(TypeError) end it "raises a TypeError when allocate is called" do - lambda { + -> { Object.new.singleton_class.allocate }.should raise_error(TypeError) end end + +describe "Frozen properties" do + it "is frozen if the object it is created from is frozen" do + o = Object.new + o.freeze + klass = o.singleton_class + klass.frozen?.should == true + end + + it "will be frozen if the object it is created from becomes frozen" do + o = Object.new + klass = o.singleton_class + klass.frozen?.should == false + o.freeze + klass.frozen?.should == true + end + + it "will be unfrozen if the frozen object is cloned with freeze set to false" do + o = Object.new + o.freeze + o2 = o.clone(freeze: false) + o2.singleton_class.frozen?.should == false + end +end diff --git a/spec/ruby/language/source_encoding_spec.rb b/spec/ruby/language/source_encoding_spec.rb new file mode 100644 index 0000000000..7135bc0a70 --- /dev/null +++ b/spec/ruby/language/source_encoding_spec.rb @@ -0,0 +1,61 @@ +require_relative '../spec_helper' + +describe "Source files" do + + describe "encoded in UTF-8 without a BOM" do + it "can be parsed" do + ruby_exe(fixture(__FILE__, "utf8-nobom.rb"), args: "2>&1").should == "hello\n" + end + end + + describe "encoded in UTF-8 with a BOM" do + it "can be parsed" do + ruby_exe(fixture(__FILE__, "utf8-bom.rb"), args: "2>&1").should == "hello\n" + end + end + + describe "encoded in UTF-16 LE without a BOM" 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 + + describe "encoded in UTF-16 LE with a BOM" do + it "are invalid because they contain an invalid UTF-8 sequence before the encoding comment" do + bom = "\xFF\xFE".b + source = "# encoding: utf-16le\nputs 'hello'\n" + source = bom + source.bytes.zip([0]*source.bytesize).flatten.pack('C*') + path = tmp("utf16-le-bom.rb") + + touch(path, "wb") { |f| f.write source } + begin + ruby_exe(path, args: "2>&1", exit_status: 1).should =~ /invalid multibyte char/ + ensure + rm_r path + end + end + end + + describe "encoded in UTF-16 BE without a BOM" do + it "are parsed as empty because they contain a NUL byte before the encoding comment" do + ruby_exe(fixture(__FILE__, "utf16-be-nobom.rb"), args: "2>&1").should == "" + end + end + + describe "encoded in UTF-16 BE with a BOM" do + it "are invalid because they contain an invalid UTF-8 sequence before the encoding comment" do + bom = "\xFE\xFF".b + source = "# encoding: utf-16be\nputs 'hello'\n" + source = bom + ([0]*source.bytesize).zip(source.bytes).flatten.pack('C*') + path = tmp("utf16-be-bom.rb") + + touch(path, "wb") { |f| f.write source } + begin + ruby_exe(path, args: "2>&1", exit_status: 1).should =~ /invalid multibyte char/ + ensure + rm_r path + end + end + end + +end diff --git a/spec/ruby/language/string_spec.rb b/spec/ruby/language/string_spec.rb index dbec2652ed..f287731bed 100644 --- a/spec/ruby/language/string_spec.rb +++ b/spec/ruby/language/string_spec.rb @@ -1,6 +1,7 @@ -# -*- encoding: binary -*- +# encoding: binary -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/class_with_class_variable' # TODO: rewrite these horrid specs. it "are..." seriously?! @@ -27,11 +28,21 @@ 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' end + it "does not interpolate invalid variable names" do + "#@".should == '#@' + "#$%".should == '#$%' + end + it "has characters [.(=?!# end simple # interpolation" do "#@ip[".should == 'xxx[' "#@ip.".should == 'xxx.' @@ -42,24 +53,13 @@ describe "Ruby character strings" do "#@ip#@ip".should == 'xxxxxx' end - 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 + it "don't get confused by partial interpolation character sequences" do + "#@".should == '#@' + "#@ ".should == '#@ ' + "#@@".should == '#@@' + "#@@ ".should == '#@@ ' + "#$ ".should == '#$ ' + "#\$".should == '#$' end it "allows using non-alnum characters as string delimiters" do @@ -186,11 +186,11 @@ describe "Ruby character strings" do # TODO: spec other source encodings describe "with ASCII_8BIT source encoding" do it "produces an ASCII string when escaping ASCII characters via \\u" do - "\u0000".encoding.should == Encoding::ASCII_8BIT + "\u0000".encoding.should == Encoding::BINARY end it "produces an ASCII string when escaping ASCII characters via \\u{}" do - "\u{0000}".encoding.should == Encoding::ASCII_8BIT + "\u{0000}".encoding.should == Encoding::BINARY end it "produces a UTF-8-encoded string when escaping non-ASCII characters via \\u" do @@ -224,59 +224,78 @@ describe "Ruby String literals" do long_string_literals.should == "Beautiful is better than ugly.Explicit is better than implicit." end - ruby_version_is "2.3" do - describe "with a magic frozen comment" do - it "produce the same object each time" do - ruby_exe(fixture(__FILE__, "freeze_magic_comment_one_literal.rb")).chomp.should == "true" - end + describe "with a magic frozen comment" do + it "produce the same object each time" do + ruby_exe(fixture(__FILE__, "freeze_magic_comment_one_literal.rb")).chomp.should == "true" + end - it "produce the same object for literals with the same content" do - ruby_exe(fixture(__FILE__, "freeze_magic_comment_two_literals.rb")).chomp.should == "true" - end + it "produce the same object for literals with the same content" do + ruby_exe(fixture(__FILE__, "freeze_magic_comment_two_literals.rb")).chomp.should == "true" + end - it "produce the same object for literals with the same content in different files" do - ruby_exe(fixture(__FILE__, "freeze_magic_comment_across_files.rb")).chomp.should == "true" - end + it "produce the same object for literals with the same content in different files" 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 + 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 - it "produce different objects for literals with the same content in different files if they have different encodings" do - ruby_exe(fixture(__FILE__, "freeze_magic_comment_across_files_diff_enc.rb")).chomp.should == "true" + 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 + ruby_exe(fixture(__FILE__, "freeze_magic_comment_across_files_diff_enc.rb")).chomp.should == "true" + end end end -with_feature :encoding do - describe "Ruby String interpolation" do - it "creates a String having an Encoding compatible with all components" do - a = "\u3042" - b = "abc".encode("ascii-8bit") +describe "Ruby String interpolation" do + it "permits an empty expression" do + s = "#{}" # rubocop:disable Lint/EmptyInterpolation + s.should.empty? + s.should_not.frozen? + end + + it "returns a string with the source encoding by default" do + "a#{"b"}c".encoding.should == Encoding::BINARY + 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 - str = "#{a} x #{b}" + it "returns a string with the source encoding, even if the components have another encoding" do + a = "abc".dup.force_encoding("euc-jp") + "#{a}".encoding.should == Encoding::BINARY - str.should == "\xe3\x81\x82\x20\x78\x20\x61\x62\x63".force_encoding("utf-8") - str.encoding.should == Encoding::UTF_8 - end + b = "abc".encode("utf-8") + "#{b}".encoding.should == Encoding::BINARY + end - it "creates a String having the Encoding of the components when all are the same Encoding" do - a = "abc".force_encoding("euc-jp") - b = "def".force_encoding("euc-jp") - str = '"#{a} x #{b}"'.force_encoding("euc-jp") + it "raises an Encoding::CompatibilityError if the Encodings are not compatible" do + a = "\u3042" + b = "\xff".dup.force_encoding "binary" - result = eval(str) - result.should == "\x61\x62\x63\x20\x78\x20\x64\x65\x66".force_encoding("euc-jp") - result.encoding.should == Encoding::EUC_JP - end + -> { "#{a} #{b}" }.should raise_error(Encoding::CompatibilityError) + end - it "raises an Encoding::CompatibilityError if the Encodings are not compatible" do - a = "\u3042" - b = "\xff".force_encoding "ascii-8bit" + it "creates a non-frozen String" do + code = <<~'RUBY' + "a#{6*7}c" + RUBY + eval(code).should_not.frozen? + end - lambda { "#{a} #{b}" }.should raise_error(Encoding::CompatibilityError) - end + it "creates a non-frozen String when # frozen-string-literal: true is used" do + code = <<~'RUBY' + # frozen-string-literal: true + "a#{6*7}c" + RUBY + eval(code).should_not.frozen? end end diff --git a/spec/ruby/language/super_spec.rb b/spec/ruby/language/super_spec.rb index 3d3f5d6f74..7d9e896d8b 100644 --- a/spec/ruby/language/super_spec.rb +++ b/spec/ruby/language/super_spec.rb @@ -1,76 +1,76 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/super', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/super' describe "The super keyword" do it "calls the method on the calling class" do - Super::S1::A.new.foo([]).should == ["A#foo","A#bar"] - Super::S1::A.new.bar([]).should == ["A#bar"] - Super::S1::B.new.foo([]).should == ["B#foo","A#foo","B#bar","A#bar"] - Super::S1::B.new.bar([]).should == ["B#bar","A#bar"] + SuperSpecs::S1::A.new.foo([]).should == ["A#foo","A#bar"] + SuperSpecs::S1::A.new.bar([]).should == ["A#bar"] + SuperSpecs::S1::B.new.foo([]).should == ["B#foo","A#foo","B#bar","A#bar"] + SuperSpecs::S1::B.new.bar([]).should == ["B#bar","A#bar"] end it "searches the full inheritance chain" do - Super::S2::B.new.foo([]).should == ["B#foo","A#baz"] - Super::S2::B.new.baz([]).should == ["A#baz"] - Super::S2::C.new.foo([]).should == ["B#foo","C#baz","A#baz"] - Super::S2::C.new.baz([]).should == ["C#baz","A#baz"] + SuperSpecs::S2::B.new.foo([]).should == ["B#foo","A#baz"] + SuperSpecs::S2::B.new.baz([]).should == ["A#baz"] + SuperSpecs::S2::C.new.foo([]).should == ["B#foo","C#baz","A#baz"] + SuperSpecs::S2::C.new.baz([]).should == ["C#baz","A#baz"] end it "searches class methods" do - Super::S3::A.new.foo([]).should == ["A#foo"] - Super::S3::A.foo([]).should == ["A.foo"] - Super::S3::A.bar([]).should == ["A.bar","A.foo"] - Super::S3::B.new.foo([]).should == ["A#foo"] - Super::S3::B.foo([]).should == ["B.foo","A.foo"] - Super::S3::B.bar([]).should == ["B.bar","A.bar","B.foo","A.foo"] + SuperSpecs::S3::A.new.foo([]).should == ["A#foo"] + SuperSpecs::S3::A.foo([]).should == ["A.foo"] + SuperSpecs::S3::A.bar([]).should == ["A.bar","A.foo"] + SuperSpecs::S3::B.new.foo([]).should == ["A#foo"] + SuperSpecs::S3::B.foo([]).should == ["B.foo","A.foo"] + SuperSpecs::S3::B.bar([]).should == ["B.bar","A.bar","B.foo","A.foo"] end it "calls the method on the calling class including modules" do - Super::MS1::A.new.foo([]).should == ["ModA#foo","ModA#bar"] - Super::MS1::A.new.bar([]).should == ["ModA#bar"] - Super::MS1::B.new.foo([]).should == ["B#foo","ModA#foo","ModB#bar","ModA#bar"] - Super::MS1::B.new.bar([]).should == ["ModB#bar","ModA#bar"] + SuperSpecs::MS1::A.new.foo([]).should == ["ModA#foo","ModA#bar"] + SuperSpecs::MS1::A.new.bar([]).should == ["ModA#bar"] + SuperSpecs::MS1::B.new.foo([]).should == ["B#foo","ModA#foo","ModB#bar","ModA#bar"] + SuperSpecs::MS1::B.new.bar([]).should == ["ModB#bar","ModA#bar"] end it "searches the full inheritance chain including modules" do - Super::MS2::B.new.foo([]).should == ["ModB#foo","A#baz"] - Super::MS2::B.new.baz([]).should == ["A#baz"] - Super::MS2::C.new.baz([]).should == ["C#baz","A#baz"] - Super::MS2::C.new.foo([]).should == ["ModB#foo","C#baz","A#baz"] + SuperSpecs::MS2::B.new.foo([]).should == ["ModB#foo","A#baz"] + SuperSpecs::MS2::B.new.baz([]).should == ["A#baz"] + SuperSpecs::MS2::C.new.baz([]).should == ["C#baz","A#baz"] + SuperSpecs::MS2::C.new.foo([]).should == ["ModB#foo","C#baz","A#baz"] end it "can resolve to different methods in an included module method" do - Super::MultiSuperTargets::A.new.foo.should == :BaseA - Super::MultiSuperTargets::B.new.foo.should == :BaseB + SuperSpecs::MultiSuperTargets::A.new.foo.should == :BaseA + SuperSpecs::MultiSuperTargets::B.new.foo.should == :BaseB end it "searches class methods including modules" do - Super::MS3::A.new.foo([]).should == ["A#foo"] - Super::MS3::A.foo([]).should == ["ModA#foo"] - Super::MS3::A.bar([]).should == ["ModA#bar","ModA#foo"] - Super::MS3::B.new.foo([]).should == ["A#foo"] - Super::MS3::B.foo([]).should == ["B.foo","ModA#foo"] - Super::MS3::B.bar([]).should == ["B.bar","ModA#bar","B.foo","ModA#foo"] + SuperSpecs::MS3::A.new.foo([]).should == ["A#foo"] + SuperSpecs::MS3::A.foo([]).should == ["ModA#foo"] + SuperSpecs::MS3::A.bar([]).should == ["ModA#bar","ModA#foo"] + SuperSpecs::MS3::B.new.foo([]).should == ["A#foo"] + SuperSpecs::MS3::B.foo([]).should == ["B.foo","ModA#foo"] + SuperSpecs::MS3::B.bar([]).should == ["B.bar","ModA#bar","B.foo","ModA#foo"] end it "searches BasicObject from a module for methods defined there" do - Super::IncludesFromBasic.new.__send__(:foobar).should == 43 + SuperSpecs::IncludesFromBasic.new.__send__(:foobar).should == 43 end it "searches BasicObject through another module for methods defined there" do - Super::IncludesIntermediate.new.__send__(:foobar).should == 42 + SuperSpecs::IncludesIntermediate.new.__send__(:foobar).should == 42 end it "calls the correct method when the method visibility is modified" do - Super::MS4::A.new.example.should == 5 + SuperSpecs::MS4::A.new.example.should == 5 end it "calls the correct method when the superclass argument list is different from the subclass" do - Super::S4::A.new.foo([]).should == ["A#foo"] - Super::S4::B.new.foo([],"test").should == ["B#foo(a,test)", "A#foo"] + SuperSpecs::S4::A.new.foo([]).should == ["A#foo"] + 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 - lambda {sub_normal.new.foo}.should raise_error(NoMethodError, /super/) - lambda {sub_zsuper.new.foo}.should raise_error(NoMethodError, /super/) + -> {sub_normal.new.foo}.should raise_error(NoMethodError, /super/) + -> {sub_zsuper.new.foo}.should raise_error(NoMethodError, /super/) end it "uses given block even if arguments are passed explicitly" do @@ -102,16 +102,66 @@ describe "The super keyword" do c2.new.m(:dump) { :value }.should == :value end + it "can pass an explicit block" do + c1 = Class.new do + def m(v) + yield(v) + end + end + c2 = Class.new(c1) do + def m(v) + block = -> w { yield(w + 'b') } + super(v, &block) + end + end + + c2.new.m('a') { |x| x + 'c' }.should == 'abc' + end + + it "can pass no block using &nil" do + c1 = Class.new do + def m(v) + block_given? + end + end + c2 = Class.new(c1) do + def m(v) + super(v, &nil) + end + end + + c2.new.m('a') { raise }.should be_false + end + + it "uses block argument given to method when used in a block" do + c1 = Class.new do + def m + yield + end + end + c2 = Class.new(c1) do + def m(v) + ary = [] + 1.times do + ary << super() + end + ary + end + end + + c2.new.m(:dump) { :value }.should == [ :value ] + end + it "calls the superclass method when in a block" do - Super::S6.new.here.should == :good + SuperSpecs::S6.new.here.should == :good end it "calls the superclass method when initial method is defined_method'd" do - Super::S7.new.here.should == :good + SuperSpecs::S7.new.here.should == :good end it "can call through a define_method multiple times (caching check)" do - obj = Super::S7.new + obj = SuperSpecs::S7.new 2.times do obj.here.should == :good @@ -150,25 +200,44 @@ describe "The super keyword" do end end - lambda { klass.new.a(:a_called) }.should raise_error(RuntimeError) + -> { klass.new.a(:a_called) }.should raise_error(RuntimeError) + end + + it "is able to navigate to super, when a method is defined dynamically on the singleton class" do + foo_class = Class.new do + def bar + "bar" + end + end + + mixin_module = Module.new do + def bar + "super_" + super + end + end + + foo = foo_class.new + foo.singleton_class.define_method(:bar, mixin_module.instance_method(:bar)) + + foo.bar.should == "super_bar" end # Rubinius ticket github#157 it "calls method_missing when a superclass method is not found" do - Super::MM_B.new.is_a?(Hash).should == false + SuperSpecs::MM_B.new.is_a?(Hash).should == false end # Rubinius ticket github#180 it "respects the original module a method is aliased from" do - Super::Alias3.new.name3.should == [:alias2, :alias1] + SuperSpecs::Alias3.new.name3.should == [:alias2, :alias1] end it "sees the included version of a module a method is alias from" do - Super::AliasWithSuper::Trigger.foo.should == [:b, :a] + SuperSpecs::AliasWithSuper::Trigger.foo.should == [:b, :a] end it "find super from a singleton class" do - obj = Super::SingletonCase::Foo.new + obj = SuperSpecs::SingletonCase::Foo.new def obj.foobar(array) array << :singleton super @@ -177,87 +246,123 @@ describe "The super keyword" do end it "finds super on other objects if a singleton class aliased the method" do - orig_obj = Super::SingletonAliasCase::Foo.new + orig_obj = SuperSpecs::SingletonAliasCase::Foo.new orig_obj.alias_on_singleton orig_obj.new_foobar([]).should == [:foo, :base] - Super::SingletonAliasCase::Foo.new.foobar([]).should == [:foo, :base] + SuperSpecs::SingletonAliasCase::Foo.new.foobar([]).should == [:foo, :base] end it "passes along modified rest args when they weren't originally empty" do - Super::RestArgsWithSuper::B.new.a("bar").should == ["bar", "foo"] + SuperSpecs::RestArgsWithSuper::B.new.a("bar").should == ["bar", "foo"] end it "passes along modified rest args when they were originally empty" do - Super::RestArgsWithSuper::B.new.a.should == ["foo"] + SuperSpecs::RestArgsWithSuper::B.new.a.should == ["foo"] + end + + # https://bugs.ruby-lang.org/issues/14279 + it "passes along reassigned rest args" do + SuperSpecs::ZSuperWithRestReassigned::B.new.a("bar").should == ["foo"] + end + + # https://bugs.ruby-lang.org/issues/14279 + it "wraps into array and passes along reassigned rest args with non-array scalar value" do + SuperSpecs::ZSuperWithRestReassignedWithScalar::B.new.a("bar").should == ["foo"] end it "invokes methods from a chain of anonymous modules" do - Super::AnonymousModuleIncludedTwice.new.a([]).should == ["anon", "anon", "non-anon"] + SuperSpecs::AnonymousModuleIncludedTwice.new.a([]).should == ["anon", "anon", "non-anon"] end it "without explicit arguments can accept a block but still pass the original arguments" do - Super::ZSuperWithBlock::B.new.a.should == 14 + SuperSpecs::ZSuperWithBlock::B.new.a.should == 14 end it "passes along block via reference to method expecting a reference" do - Super::ZSuperWithBlock::B.new.b.should == [14, 15] + SuperSpecs::ZSuperWithBlock::B.new.b.should == [14, 15] end it "passes along a block via reference to a method that yields" do - Super::ZSuperWithBlock::B.new.c.should == 16 + SuperSpecs::ZSuperWithBlock::B.new.c.should == 16 end it "without explicit arguments passes optional arguments that have a default value" do - Super::ZSuperWithOptional::B.new.m(1, 2).should == 14 + SuperSpecs::ZSuperWithOptional::B.new.m(1, 2).should == 14 end it "without explicit arguments passes optional arguments that have a non-default value" do - Super::ZSuperWithOptional::B.new.m(1, 2, 3).should == 3 + SuperSpecs::ZSuperWithOptional::B.new.m(1, 2, 3).should == 3 end it "without explicit arguments passes optional arguments that have a default value but were modified" do - Super::ZSuperWithOptional::C.new.m(1, 2).should == 100 + SuperSpecs::ZSuperWithOptional::C.new.m(1, 2).should == 100 end it "without explicit arguments passes optional arguments that have a non-default value but were modified" do - Super::ZSuperWithOptional::C.new.m(1, 2, 3).should == 100 + SuperSpecs::ZSuperWithOptional::C.new.m(1, 2, 3).should == 100 end it "without explicit arguments passes rest arguments" do - Super::ZSuperWithRest::B.new.m(1, 2, 3).should == [1, 2, 3] + SuperSpecs::ZSuperWithRest::B.new.m(1, 2, 3).should == [1, 2, 3] end it "without explicit arguments passes rest arguments including any modifications" do - Super::ZSuperWithRest::B.new.m_modified(1, 2, 3).should == [1, 14, 3] + SuperSpecs::ZSuperWithRest::B.new.m_modified(1, 2, 3).should == [1, 14, 3] end it "without explicit arguments passes arguments and rest arguments" do - Super::ZSuperWithRestAndOthers::B.new.m(1, 2, 3, 4, 5).should == [3, 4, 5] + SuperSpecs::ZSuperWithRestAndOthers::B.new.m(1, 2, 3, 4, 5).should == [3, 4, 5] + SuperSpecs::ZSuperWithRestAndOthers::B.new.m(1, 2).should == [] + end + + it "without explicit arguments passes arguments, rest arguments, and post arguments" do + SuperSpecs::ZSuperWithRestAndPost::B.new.m(1, 2, 3, 4, 5).should == [1, 2, 3] + SuperSpecs::ZSuperWithRestOthersAndPost::B.new.m(1, 2, 3, 4, 5).should == [2, 3, 4] + SuperSpecs::ZSuperWithRestAndPost::B.new.m(1, 2).should == [] + SuperSpecs::ZSuperWithRestOthersAndPost::B.new.m(1, 2).should == [] + end + + it "without explicit arguments passes arguments, rest arguments including modifications, and post arguments" do + SuperSpecs::ZSuperWithRestAndPost::B.new.m_modified(1, 2, 3, 4, 5).should == [1, 14, 3] + SuperSpecs::ZSuperWithRestOthersAndPost::B.new.m_modified(1, 2, 3, 4, 5).should == [2, 14, 4] + SuperSpecs::ZSuperWithRestAndPost::B.new.m_modified(1, 2).should == [nil, 14] + SuperSpecs::ZSuperWithRestOthersAndPost::B.new.m_modified(1, 2).should == [nil, 14] end it "without explicit arguments passes arguments and rest arguments including any modifications" do - Super::ZSuperWithRestAndOthers::B.new.m_modified(1, 2, 3, 4, 5).should == [3, 14, 5] + SuperSpecs::ZSuperWithRestAndOthers::B.new.m_modified(1, 2, 3, 4, 5).should == [3, 14, 5] end it "without explicit arguments that are '_'" do - Super::ZSuperWithUnderscores::B.new.m(1, 2).should == [1, 2] + 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 - Super::ZSuperWithUnderscores::B.new.m_modified(1, 2).should == [14, 2] + 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 = Super::Keywords::RequiredArguments.new - @opts = Super::Keywords::OptionalArguments.new - @etc = Super::Keywords::PlaceholderArguments.new + @req = SuperSpecs::Keywords::RequiredArguments.new + @opts = SuperSpecs::Keywords::OptionalArguments.new + @etc = SuperSpecs::Keywords::PlaceholderArguments.new - @req_and_opts = Super::Keywords::RequiredAndOptionalArguments.new - @req_and_etc = Super::Keywords::RequiredAndPlaceholderArguments.new - @opts_and_etc = Super::Keywords::OptionalAndPlaceholderArguments.new + @req_and_opts = SuperSpecs::Keywords::RequiredAndOptionalArguments.new + @req_and_etc = SuperSpecs::Keywords::RequiredAndPlaceholderArguments.new + @opts_and_etc = SuperSpecs::Keywords::OptionalAndPlaceholderArguments.new - @req_and_opts_and_etc = Super::Keywords::RequiredAndOptionalAndPlaceholderArguments.new + @req_and_opts_and_etc = SuperSpecs::Keywords::RequiredAndOptionalAndPlaceholderArguments.new end it 'does not pass any arguments to the parent when none are given' do @@ -293,15 +398,15 @@ describe "The super keyword" do describe 'when using regular and keyword arguments' do before :each do - @req = Super::RegularAndKeywords::RequiredArguments.new - @opts = Super::RegularAndKeywords::OptionalArguments.new - @etc = Super::RegularAndKeywords::PlaceholderArguments.new + @req = SuperSpecs::RegularAndKeywords::RequiredArguments.new + @opts = SuperSpecs::RegularAndKeywords::OptionalArguments.new + @etc = SuperSpecs::RegularAndKeywords::PlaceholderArguments.new - @req_and_opts = Super::RegularAndKeywords::RequiredAndOptionalArguments.new - @req_and_etc = Super::RegularAndKeywords::RequiredAndPlaceholderArguments.new - @opts_and_etc = Super::RegularAndKeywords::OptionalAndPlaceholderArguments.new + @req_and_opts = SuperSpecs::RegularAndKeywords::RequiredAndOptionalArguments.new + @req_and_etc = SuperSpecs::RegularAndKeywords::RequiredAndPlaceholderArguments.new + @opts_and_etc = SuperSpecs::RegularAndKeywords::OptionalAndPlaceholderArguments.new - @req_and_opts_and_etc = Super::RegularAndKeywords::RequiredAndOptionalAndPlaceholderArguments.new + @req_and_opts_and_etc = SuperSpecs::RegularAndKeywords::RequiredAndOptionalAndPlaceholderArguments.new end it 'passes only required regular arguments to the parent when no optional keyword arguments are given' do @@ -337,7 +442,7 @@ describe "The super keyword" do describe 'when using splat and keyword arguments' do before :each do - @all = Super::SplatAndKeywords::AllArguments.new + @all = SuperSpecs::SplatAndKeywords::AllArguments.new end it 'does not pass any arguments to the parent when none are given' do diff --git a/spec/ruby/language/symbol_spec.rb b/spec/ruby/language/symbol_spec.rb index 90540f7d1d..0801d3223e 100644 --- a/spec/ruby/language/symbol_spec.rb +++ b/spec/ruby/language/symbol_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "A Symbol literal" do it "is a ':' followed by any number of valid characters" do @@ -38,7 +38,7 @@ describe "A Symbol literal" do it 'inherits the encoding of the magic comment and can have a binary encoding' do ruby_exe(fixture(__FILE__, "binary_symbol.rb")) - .should == "[105, 108, 95, 195, 169, 116, 97, 105, 116]\nASCII-8BIT\n" + .should == "[105, 108, 95, 195, 169, 116, 97, 105, 116]\n#{Encoding::BINARY.name}\n" end it "may contain '::' in the string" do @@ -96,11 +96,13 @@ describe "A Symbol literal" do %I{a b #{"c"}}.should == [:a, :b, :c] end - it "with invalid bytes raises an EncodingError at parse time" do - ScratchPad.record [] - -> { - eval 'ScratchPad << 1; :"\xC3"' - }.should raise_error(EncodingError, /invalid/) - ScratchPad.recorded.should == [] + ruby_bug "#20280", ""..."3.4" do + it "raises an SyntaxError at parse time when Symbol with invalid bytes" do + ScratchPad.record [] + -> { + eval 'ScratchPad << 1; :"\xC3"' + }.should raise_error(SyntaxError, /invalid symbol/) + ScratchPad.recorded.should == [] + end end end diff --git a/spec/ruby/language/throw_spec.rb b/spec/ruby/language/throw_spec.rb index 92f699350c..d723843688 100644 --- a/spec/ruby/language/throw_spec.rb +++ b/spec/ruby/language/throw_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "The throw keyword" do it "abandons processing" do @@ -45,7 +45,7 @@ describe "The throw keyword" do end it "does not convert strings to a symbol" do - lambda { catch(:exit) { throw "exit" } }.should raise_error(ArgumentError) + -> { catch(:exit) { throw "exit" } }.should raise_error(ArgumentError) end it "unwinds stack from within a method" do @@ -59,13 +59,13 @@ describe "The throw keyword" do end it "unwinds stack from within a lambda" do - c = lambda { throw :foo, :msg } + c = -> { throw :foo, :msg } catch(:foo) { c.call }.should == :msg end it "raises an ArgumentError if outside of scope of a matching catch" do - lambda { throw :test, 5 }.should raise_error(ArgumentError) - lambda { catch(:different) { throw :test, 5 } }.should raise_error(ArgumentError) + -> { throw :test, 5 }.should raise_error(ArgumentError) + -> { catch(:different) { throw :test, 5 } }.should raise_error(ArgumentError) end it "raises an UncaughtThrowError if used to exit a thread" do diff --git a/spec/ruby/language/undef_spec.rb b/spec/ruby/language/undef_spec.rb index 9e788f2a09..268c0b84c3 100644 --- a/spec/ruby/language/undef_spec.rb +++ b/spec/ruby/language/undef_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "The undef keyword" do describe "undefines a method" do @@ -14,35 +14,42 @@ describe "The undef keyword" do @undef_class.class_eval do undef meth end - lambda { @obj.meth(5) }.should raise_error(NoMethodError) + -> { @obj.meth(5) }.should raise_error(NoMethodError) end it "with a simple symbol" do @undef_class.class_eval do undef :meth end - lambda { @obj.meth(5) }.should raise_error(NoMethodError) + -> { @obj.meth(5) }.should raise_error(NoMethodError) end it "with a single quoted symbol" do @undef_class.class_eval do undef :'meth' end - lambda { @obj.meth(5) }.should raise_error(NoMethodError) + -> { @obj.meth(5) }.should raise_error(NoMethodError) end it "with a double quoted symbol" do @undef_class.class_eval do undef :"meth" end - lambda { @obj.meth(5) }.should raise_error(NoMethodError) + -> { @obj.meth(5) }.should raise_error(NoMethodError) end - it "with a interpolated symbol" do + it "with an interpolated symbol" do @undef_class.class_eval do undef :"#{'meth'}" end - lambda { @obj.meth(5) }.should raise_error(NoMethodError) + -> { @obj.meth(5) }.should raise_error(NoMethodError) + end + + it "with an interpolated symbol when interpolated expression is not a String literal" do + @undef_class.class_eval do + undef :"#{'meth'.to_sym}" + end + -> { @obj.meth(5) }.should raise_error(NoMethodError) end end @@ -61,8 +68,8 @@ describe "The undef keyword" do it "raises a NameError when passed a missing name" do Class.new do - lambda { - undef not_exist + -> { + undef not_exist }.should raise_error(NameError) { |e| # a NameError and not a NoMethodError e.class.should == NameError diff --git a/spec/ruby/language/unless_spec.rb b/spec/ruby/language/unless_spec.rb index 681f0adfdd..98acdc083b 100644 --- a/spec/ruby/language/unless_spec.rb +++ b/spec/ruby/language/unless_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' describe "The unless expression" do it "evaluates the unless body when the expression is false" do diff --git a/spec/ruby/language/until_spec.rb b/spec/ruby/language/until_spec.rb index 08898644ce..78c289ff56 100644 --- a/spec/ruby/language/until_spec.rb +++ b/spec/ruby/language/until_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' # until bool-expr [do] # body @@ -220,7 +220,7 @@ describe "The until modifier with begin .. end block" do a.should == [0, 1, 2, 4] end - it "restart the current iteration without reevaluting condition with redo" do + it "restart the current iteration without reevaluating condition with redo" do a = [] i = 0 j = 0 diff --git a/spec/ruby/language/variables_spec.rb b/spec/ruby/language/variables_spec.rb index 81ba54840a..e134271939 100644 --- a/spec/ruby/language/variables_spec.rb +++ b/spec/ruby/language/variables_spec.rb @@ -1,5 +1,50 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/variables', __FILE__) +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 @@ -46,7 +91,7 @@ describe "Multiple assignment" do x = mock("multi-assign single RHS") x.should_receive(:to_ary).and_return(1) - lambda { a, b, c = x }.should raise_error(TypeError) + -> { a, b, c = x }.should raise_error(TypeError) end it "does not call #to_a to convert an Object RHS when assigning a simple MLHS" do @@ -127,7 +172,7 @@ describe "Multiple assignment" do x = mock("multi-assign splat") x.should_receive(:to_ary).and_return(1) - lambda { *a = x }.should raise_error(TypeError) + -> { *a = x }.should raise_error(TypeError) end it "does not call #to_ary on an Array subclass" do @@ -160,7 +205,7 @@ describe "Multiple assignment" do x = mock("multi-assign splat") x.should_receive(:to_ary).and_return(1) - lambda { a, *b, c = x }.should raise_error(TypeError) + -> { a, *b, c = x }.should raise_error(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) - lambda { a, b = x.m }.should raise_error(TypeError) + -> { a, b = x.m }.should raise_error(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 @@ -354,6 +418,16 @@ describe "Multiple assignment" do a.should be_an_instance_of(Array) end + it "unfreezes the array returned from calling 'to_a' on the splatted value" do + obj = Object.new + def obj.to_a + [1,2].freeze + end + res = *obj + res.should == [1,2] + res.should_not.frozen? + end + it "consumes values for an anonymous splat" do a = 1 (* = *a).should == [1] @@ -407,7 +481,7 @@ describe "Multiple assignment" do x = mock("multi-assign RHS splat") x.should_receive(:to_a).and_return(1) - lambda { *a = *x }.should raise_error(TypeError) + -> { *a = *x }.should raise_error(TypeError) end it "does not call #to_ary to convert an Object RHS with a single splat LHS" do @@ -453,7 +527,7 @@ describe "Multiple assignment" do x = mock("multi-assign splat") x.should_receive(:to_a).and_return(1) - lambda { a = *x }.should raise_error(TypeError) + -> { a = *x }.should raise_error(TypeError) end it "calls #to_a to convert an Object splat RHS when assigned to a simple MLHS" do @@ -468,7 +542,7 @@ describe "Multiple assignment" do x = mock("multi-assign splat") x.should_receive(:to_a).and_return(1) - lambda { a, b, c = *x }.should raise_error(TypeError) + -> { a, b, c = *x }.should raise_error(TypeError) end it "does not call #to_ary to convert an Object splat RHS when assigned to a simple MLHS" do @@ -491,7 +565,7 @@ describe "Multiple assignment" do x = mock("multi-assign splat") x.should_receive(:to_a).and_return(1) - lambda { a, *b, c = *x }.should raise_error(TypeError) + -> { a, *b, c = *x }.should raise_error(TypeError) end it "does not call #to_ary to convert an Object RHS with a MLHS" do @@ -524,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 @@ -569,7 +645,7 @@ describe "Multiple assignment" do x = mock("multi-assign splat MRHS") x.should_receive(:to_a).and_return(1) - lambda { a, *b = 1, *x }.should raise_error(TypeError) + -> { a, *b = 1, *x }.should raise_error(TypeError) end it "does not call #to_ary to convert a splatted Object as part of a MRHS with a splat MRHS" do @@ -592,7 +668,7 @@ describe "Multiple assignment" do x = mock("multi-assign splat MRHS") x.should_receive(:to_a).and_return(1) - lambda { a, *b = *x, 1 }.should raise_error(TypeError) + -> { a, *b = *x, 1 }.should raise_error(TypeError) end it "does not call #to_ary to convert a splatted Object with a splat MRHS" do @@ -641,7 +717,7 @@ describe "Multiple assignment" do x = mock("multi-assign mixed RHS") x.should_receive(:to_ary).and_return(x) - lambda { a, (b, c), d = 1, x, 3, 4 }.should raise_error(TypeError) + -> { a, (b, c), d = 1, x, 3, 4 }.should raise_error(TypeError) end it "calls #to_a to convert a splatted Object value in a MRHS" do @@ -665,7 +741,7 @@ describe "Multiple assignment" do x = mock("multi-assign mixed splatted RHS") x.should_receive(:to_ary).and_return(x) - lambda { a, *b, (c, d) = 1, 2, 3, *x }.should raise_error(TypeError) + -> { a, *b, (c, d) = 1, 2, 3, *x }.should raise_error(TypeError) end it "does not call #to_ary to convert an Object when the position receiving the value is a simple variable" do @@ -705,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 @@ -718,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 @@ -758,3 +851,80 @@ describe "A local variable assigned only within a conditional block" do end end end + +describe 'Local variable shadowing' do + 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) + + result.should == [3, 3, 3] + end +end + +describe 'Allowed characters' do + it 'allows non-ASCII lowercased characters at the beginning' do + result = nil + + eval <<-CODE + def test + μ = 1 + end + + result = test + CODE + + result.should == 1 + end + + it 'parses a non-ASCII upcased character as a constant identifier' do + -> do + eval <<-CODE + def test + ἍBB = 1 + end + CODE + end.should raise_error(SyntaxError, /dynamic constant assignment/) + end +end + +describe "Instance variables" do + context "when instance variable is uninitialized" do + it "doesn't warn about accessing uninitialized instance variable" do + obj = Object.new + def obj.foobar; a = @a; end + + -> { obj.foobar }.should_not complain(verbose: true) + end + + it "doesn't warn at lazy initialization" do + obj = Object.new + def obj.foobar; @a ||= 42; end + + -> { obj.foobar }.should_not complain(verbose: true) + end + end + + describe "global variable" do + context "when global variable is uninitialized" do + it "warns about accessing uninitialized global variable in verbose mode" do + obj = Object.new + def obj.foobar; a = $specs_uninitialized_global_variable; end + + -> { obj.foobar }.should complain(/warning: global variable [`']\$specs_uninitialized_global_variable' not initialized/, verbose: true) + end + + it "doesn't warn at lazy initialization" do + obj = Object.new + def obj.foobar; $specs_uninitialized_global_variable_lazy ||= 42; end + + -> { obj.foobar }.should_not complain(verbose: true) + end + end + end +end diff --git a/spec/ruby/language/while_spec.rb b/spec/ruby/language/while_spec.rb index 00e948e41f..e172453ca6 100644 --- a/spec/ruby/language/while_spec.rb +++ b/spec/ruby/language/while_spec.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../spec_helper', __FILE__) +require_relative '../spec_helper' # while bool-expr [do] # body @@ -330,7 +330,7 @@ describe "The while modifier with begin .. end block" do a.should == [0, 1, 2, 4] end - it "restarts the current iteration without reevaluting condition with redo" do + it "restarts the current iteration without reevaluating condition with redo" do a = [] i = 0 j = 0 diff --git a/spec/ruby/language/yield_spec.rb b/spec/ruby/language/yield_spec.rb index 663110cbe6..e125cf8e73 100644 --- a/spec/ruby/language/yield_spec.rb +++ b/spec/ruby/language/yield_spec.rb @@ -1,5 +1,5 @@ -require File.expand_path('../../spec_helper', __FILE__) -require File.expand_path('../fixtures/yield', __FILE__) +require_relative '../spec_helper' +require_relative 'fixtures/yield' # Note that these specs use blocks defined as { |*a| ... } to capture the # arguments with which the block is invoked. This is slightly confusing @@ -13,18 +13,22 @@ describe "The yield call" do describe "taking no arguments" do it "raises a LocalJumpError when the method is not passed a block" do - lambda { @y.z }.should raise_error(LocalJumpError) + -> { @y.z }.should raise_error(LocalJumpError) end it "ignores assignment to the explicit block argument and calls the passed block" do @y.ze { 42 }.should == 42 end + + it "does not pass a named block to the block being yielded to" do + @y.z() { |&block| block == nil }.should == true + end end describe "taking a single argument" do describe "when no block is given" do it "raises a LocalJumpError" do - lambda { @y.s(1) }.should raise_error(LocalJumpError) + -> { @y.s(1) }.should raise_error(LocalJumpError) end end @@ -48,40 +52,38 @@ describe "The yield call" do describe "yielding to a lambda" do it "passes an empty Array when the argument is an empty Array" do - @y.s([], &lambda { |*a| a }).should == [[]] + @y.s([], &-> *a { a }).should == [[]] end it "passes nil as a value" do - @y.s(nil, &lambda { |*a| a }).should == [nil] + @y.s(nil, &-> *a { a }).should == [nil] end it "passes a single value" do - @y.s(1, &lambda { |*a| a }).should == [1] + @y.s(1, &-> *a { a }).should == [1] end it "passes a single, multi-value Array" do - @y.s([1, 2, 3], &lambda { |*a| a }).should == [[1, 2, 3]] + @y.s([1, 2, 3], &-> *a { a }).should == [[1, 2, 3]] end it "raises an ArgumentError if too few arguments are passed" do - lambda { - @y.s(1, &lambda { |a,b| [a,b] }) + -> { + @y.s(1, &-> a, b { [a,b] }) }.should raise_error(ArgumentError) end - ruby_bug "#12705", "2.2"..."2.5" do - it "should not destructure an Array into multiple arguments" do - lambda { - @y.s([1, 2], &lambda { |a,b| [a,b] }) - }.should raise_error(ArgumentError) - end + it "should not destructure an Array into multiple arguments" do + -> { + @y.s([1, 2], &-> a, b { [a,b] }) + }.should raise_error(ArgumentError) end end end describe "taking multiple arguments" do it "raises a LocalJumpError when the method is not passed a block" do - lambda { @y.m(1, 2, 3) }.should raise_error(LocalJumpError) + -> { @y.m(1, 2, 3) }.should raise_error(LocalJumpError) end it "passes the arguments to the block" do @@ -93,21 +95,21 @@ describe "The yield call" do end it "raises an ArgumentError if too many arguments are passed to a lambda" do - lambda { - @y.m(1, 2, 3, &lambda { |a| }) + -> { + @y.m(1, 2, 3, &-> a { }) }.should raise_error(ArgumentError) end it "raises an ArgumentError if too few arguments are passed to a lambda" do - lambda { - @y.m(1, 2, 3, &lambda { |a,b,c,d| }) + -> { + @y.m(1, 2, 3, &-> a, b, c, d { }) }.should raise_error(ArgumentError) end end describe "taking a single splatted argument" do it "raises a LocalJumpError when the method is not passed a block" do - lambda { @y.r(0) }.should raise_error(LocalJumpError) + -> { @y.r(0) }.should raise_error(LocalJumpError) end it "passes a single value" do @@ -139,7 +141,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 - lambda { @y.rs(1, 2, [3, 4]) }.should raise_error(LocalJumpError) + -> { @y.rs(1, 2, [3, 4]) }.should raise_error(LocalJumpError) end it "passes the arguments to the block" do @@ -164,7 +166,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 - lambda { @y.rs(1, 2, [3, 4]) }.should raise_error(LocalJumpError) + -> { @y.rs(1, 2, [3, 4]) }.should raise_error(LocalJumpError) end it "passes the arguments to the block" do @@ -172,8 +174,47 @@ describe "The yield call" do end end + describe "taking a splat and a keyword argument" do + it "passes it as an array of the values and a hash" do + @y.k([1, 2]) { |*a| a }.should == [1, 2, {:b=>true}] + end + end + it "uses captured block of a block used in define_method" do @y.deep(2).should == 4 end +end +describe "Using yield in a singleton class literal" do + it 'raises a SyntaxError' do + code = <<~RUBY + class << Object.new + yield + end + RUBY + + -> { eval(code) }.should raise_error(SyntaxError, /Invalid yield/) + end +end + +describe "Using yield in non-lambda block" do + it 'raises a SyntaxError' do + code = <<~RUBY + 1.times { yield } + RUBY + + -> { eval(code) }.should raise_error(SyntaxError, /Invalid yield/) + end +end + +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_error(SyntaxError, /Invalid yield/) + end end |
