diff options
Diffstat (limited to 'spec/ruby/language')
163 files changed, 27584 insertions, 0 deletions
diff --git a/spec/ruby/language/BEGIN_spec.rb b/spec/ruby/language/BEGIN_spec.rb new file mode 100644 index 0000000000..5aef5a1d7c --- /dev/null +++ b/spec/ruby/language/BEGIN_spec.rb @@ -0,0 +1,41 @@ +require_relative '../spec_helper' + +describe "The BEGIN keyword" do + before :each do + ScratchPad.record [] + end + + it "runs in a shared scope" do + eval("BEGIN { var_in_begin = 'foo' }; var_in_begin").should == "foo" + end + + it "accesses variables outside the eval scope" do + outside_var = 'foo' + eval("BEGIN { var_in_begin = outside_var }; var_in_begin").should == "foo" + end + + it "must appear in a top-level context" do + -> { 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 + eval "ScratchPad << 'foo'; BEGIN { ScratchPad << 'bar' }" + + ScratchPad.recorded.should == ['bar', 'foo'] + end + + it "runs multiple begins in FIFO order" do + eval "BEGIN { ScratchPad << 'foo' }; BEGIN { ScratchPad << 'bar' }" + + ScratchPad.recorded.should == ['foo', 'bar'] + end + + it "returns the top-level script's filename for __FILE__" do + ruby_exe(fixture(__FILE__, "begin_file.rb")).chomp.should =~ /begin_file\.rb$/ + end +end 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 new file mode 100644 index 0000000000..ae08e17fb1 --- /dev/null +++ b/spec/ruby/language/README @@ -0,0 +1,30 @@ +There are numerous possible way of categorizing the entities and concepts that +make up a programming language. Ruby has a fairly large number of reserved +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 Integer. + +Behavioral specifications describe the behavior of concrete entities. Rather +than using concepts of computation to organize these spec files, we use +entities of the Ruby language. Consider looking at any syntactic element of a +Ruby program. With (almost) no ambiguity, one can identify it as a literal, +reserved word, variable, etc. There is a spec file that corresponds to each +literal construct and most reserved words, with the exceptions noted below. +There are also several files that are more difficult to classify: all +predefined variables, constants, and objects (predefined_spec.rb), the +precedence of all operators (precedence_spec.rb), the behavior of assignment +to variables (variables_spec.rb), the behavior of subprocess execution +(execution_spec.rb), the behavior of the raise method as it impacts the +execution of a Ruby program (raise_spec.rb), and the block entities like +'begin', 'do', ' { ... }' (block_spec.rb). + +Several reserved words and other entities are combined with the primary +reserved word or entity to which they are related: + +false, true, nil, self predefined_spec.rb +in for_spec.rb +then, elsif if_spec.rb +when case_spec.rb +catch throw_spec.rb diff --git a/spec/ruby/language/alias_spec.rb b/spec/ruby/language/alias_spec.rb new file mode 100644 index 0000000000..61fddb0184 --- /dev/null +++ b/spec/ruby/language/alias_spec.rb @@ -0,0 +1,294 @@ +require_relative '../spec_helper' + +class AliasObject + attr :foo + attr_reader :bar + attr_accessor :baz + + def prep; @foo = 3; @bar = 4; end + def value; 5; end + def false_value; 6; end + def self.klass_method; 7; end +end + +describe "The alias keyword" do + before :each do + @obj = AliasObject.new + @meta = class << @obj;self;end + end + + it "creates a new name for an existing method" do + @meta.class_eval do + alias __value value + end + @obj.__value.should == 5 + end + + it "works with a simple symbol on the left-hand side" do + @meta.class_eval do + alias :a value + end + @obj.a.should == 5 + end + + it "works with a single quoted symbol on the left-hand side" do + @meta.class_eval do + alias :'a' value + end + @obj.a.should == 5 + end + + 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 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 + end + @obj.a.should == 5 + end + + it "works with a single quoted symbol on the right-hand side" do + @meta.class_eval do + alias a :'value' + end + @obj.a.should == 5 + end + + 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 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 + alias __value value + end + (@obj.methods - original_methods).map {|m| m.to_s }.should == ["__value"] + end + + it "adds the new method to the list of public methods" do + original_methods = @obj.public_methods + @meta.class_eval do + alias __value value + end + (@obj.public_methods - original_methods).map {|m| m.to_s }.should == ["__value"] + end + + it "overwrites an existing method with the target name" do + @meta.class_eval do + alias false_value value + end + @obj.false_value.should == 5 + end + + it "is reversible" do + @meta.class_eval do + alias __value value + alias value false_value + end + @obj.value.should == 6 + + @meta.class_eval do + alias value __value + end + @obj.value.should == 5 + end + + it "operates on the object's metaclass when used in instance_eval" do + @obj.instance_eval do + alias __value value + end + + @obj.__value.should == 5 + -> { AliasObject.new.__value }.should raise_error(NoMethodError) + end + + it "operates on the class/module metaclass when used in instance_eval" do + AliasObject.instance_eval do + alias __klass_method klass_method + end + + AliasObject.__klass_method.should == 7 + -> { Object.__klass_method }.should raise_error(NoMethodError) + end + + it "operates on the class/module metaclass when used in instance_exec" do + AliasObject.instance_exec do + alias __klass_method2 klass_method + end + + AliasObject.__klass_method2.should == 7 + -> { Object.__klass_method2 }.should raise_error(NoMethodError) + end + + it "operates on methods defined via attr, attr_reader, and attr_accessor" do + @obj.prep + @obj.instance_eval do + alias afoo foo + alias abar bar + alias abaz baz + end + + @obj.afoo.should == 3 + @obj.abar.should == 4 + @obj.baz = 5 + @obj.abaz.should == 5 + end + + it "operates on methods with splat arguments" do + class AliasObject2;end + AliasObject2.class_eval do + def test(*args) + 4 + end + def test_with_check(*args) + test_without_check(*args) + end + alias test_without_check test + alias test test_with_check + end + AliasObject2.new.test(1,2,3,4,5).should == 4 + end + + it "operates on methods with splat arguments on eigenclasses" do + @meta.class_eval do + def test(*args) + 4 + end + def test_with_check(*args) + test_without_check(*args) + end + alias test_without_check test + alias test test_with_check + end + @obj.test(1,2,3,4,5).should == 4 + end + + it "operates on methods with splat arguments defined in a superclass" do + alias_class = Class.new + alias_class.class_eval do + def test(*args) + 4 + end + def test_with_check(*args) + test_without_check(*args) + end + end + sub = Class.new(alias_class) do + alias test_without_check test + alias test test_with_check + end + sub.new.test(1,2,3,4,5).should == 4 + end + + it "operates on methods with splat arguments defined in a superclass using text block for class eval" do + subclass = Class.new(AliasObject) + AliasObject.class_eval <<-code + def test(*args) + 4 + end + def test_with_check(*args) + test_without_check(*args) + end + alias test_without_check test + alias test test_with_check + code + subclass.new.test("testing").should == 4 + end + + 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) + + -> do + :blah.instance_eval do + alias :foo :to_s + end + end.should raise_error(TypeError) + end + + it "on top level defines the alias on Object" do + # because it defines on the default definee / current module + ruby_exe("def foo; end; alias bla foo; print method(:bla).owner").should == "Object" + end + + it "raises a NameError when passed a missing name" do + -> { @meta.class_eval { alias undef_method not_exist } }.should raise_error(NameError) { |e| + # 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 new file mode 100644 index 0000000000..55a2a3103a --- /dev/null +++ b/spec/ruby/language/and_spec.rb @@ -0,0 +1,80 @@ +require_relative '../spec_helper' + +describe "The '&&' statement" do + + it "short-circuits evaluation at the first condition to be false" do + x = nil + true && false && x = 1 + x.should be_nil + end + + it "evaluates to the first condition not to be true" do + value = nil + (value && nil).should == nil + (value && false).should == nil + value = false + (value && nil).should == false + (value && false).should == false + + ("yes" && 1 && nil && true).should == nil + ("yes" && 1 && false && true).should == false + end + + it "evaluates to the last condition if all are true" do + ("yes" && 1).should == 1 + (1 && "yes").should == "yes" + end + + it "evaluates the full set of chained conditions during assignment" do + x, y = nil + x = 1 && y = 2 + # "1 && y = 2" is evaluated and then assigned to x + x.should == 2 + end + + it "treats empty expressions as nil" do + (() && true).should be_nil + (true && ()).should be_nil + (() && ()).should be_nil + end + +end + +describe "The 'and' statement" do + it "short-circuits evaluation at the first condition to be false" do + x = nil + true and false and x = 1 + x.should be_nil + end + + it "evaluates to the first condition not to be true" do + value = nil + (value and nil).should == nil + (value and false).should == nil + value = false + (value and nil).should == false + (value and false).should == false + + ("yes" and 1 and nil and true).should == nil + ("yes" and 1 and false and true).should == false + end + + it "evaluates to the last condition if all are true" do + ("yes" and 1).should == 1 + (1 and "yes").should == "yes" + end + + it "when used in assignment, evaluates and assigns expressions individually" do + x, y = nil + x = 1 and y = 2 + # evaluates (x=1) and (y=2) + x.should == 1 + end + + it "treats empty expressions as nil" do + (() and true).should be_nil + (true and ()).should be_nil + (() and ()).should be_nil + end + +end diff --git a/spec/ruby/language/array_spec.rb b/spec/ruby/language/array_spec.rb new file mode 100644 index 0000000000..2583cffbf7 --- /dev/null +++ b/spec/ruby/language/array_spec.rb @@ -0,0 +1,162 @@ +require_relative '../spec_helper' +require_relative 'fixtures/array' + +describe "Array literals" do + it "[] should return a new array populated with the given elements" do + array = [1, 'a', nil] + array.should be_kind_of(Array) + array[0].should == 1 + array[1].should == 'a' + array[2].should == nil + end + + it "[] treats empty expressions as nil elements" do + array = [0, (), 2, (), 4] + array.should be_kind_of(Array) + array[0].should == 0 + array[1].should == nil + array[2].should == 2 + array[3].should == nil + array[4].should == 4 + end + + it "[] accepts a literal hash without curly braces as its only parameter" do + ["foo" => :bar, baz: 42].should == [{"foo" => :bar, baz: 42}] + end + + it "[] accepts a literal hash without curly braces as its last parameter" do + ["foo", "bar" => :baz].should == ["foo", {"bar" => :baz}] + [1, 2, 3 => 6, 4 => 24].should == [1, 2, {3 => 6, 4 => 24}] + end + + it "[] treats splatted nil as no element" do + [*nil].should == [] + [1, *nil].should == [1] + [1, 2, *nil].should == [1, 2] + [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 + it "%w() transforms unquoted barewords into an array" do + a = 3 + %w(a #{3+a} 3).should == ["a", '#{3+a}', "3"] + end + + it "%W() transforms unquoted barewords into an array, supporting interpolation" do + a = 3 + %W(a #{3+a} 3).should == ["a", '6', "3"] + end + + it "%W() always treats interpolated expressions as a single word" do + a = "hello world" + %W(a b c #{a} d e).should == ["a", "b", "c", "hello world", "d", "e"] + end + + it "treats consecutive whitespace characters the same as one" do + %w(a b c d).should == ["a", "b", "c", "d"] + %W(hello + world).should == ["hello", "world"] + end + + it "treats whitespace as literals characters when escaped by a backslash" do + %w(a b\ c d e).should == ["a", "b c", "d", "e"] + %w(a b\ +c d).should == ["a", "b\nc", "d"] + %W(a\ b\tc).should == ["a ", "b\tc"] + %W(white\ \ \ \ \ space).should == ["white ", " ", " ", " space"] + end +end + +describe "The unpacking splat operator (*)" do + it "when applied to a literal nested array, unpacks its elements into the containing array" do + [1, 2, *[3, 4, 5]].should == [1, 2, 3, 4, 5] + end + + it "when applied to a nested referenced array, unpacks its elements into the containing array" do + splatted_array = [3, 4, 5] + [1, 2, *splatted_array].should == [1, 2, 3, 4, 5] + end + + it "returns a new array containing the same values when applied to an array inside an empty array" do + splatted_array = [3, 4, 5] + [*splatted_array].should == splatted_array + [*splatted_array].should_not equal(splatted_array) + end + + it "unpacks the start and count arguments in an array slice assignment" do + alphabet_1 = ['a'..'z'].to_a + alphabet_2 = alphabet_1.dup + start_and_count_args = [1, 10] + + alphabet_1[1, 10] = 'a' + alphabet_2[*start_and_count_args] = 'a' + + alphabet_1.should == alphabet_2 + end + + it "unpacks arguments as if they were listed statically" do + static = [1,2,3,4] + receiver = static.dup + args = [0,1] + static[0,1] = [] + static.should == [2,3,4] + receiver[*args] = [] + receiver.should == static + end + + it "unpacks a literal array into arguments in a method call" do + tester = ArraySpec::Splat.new + tester.unpack_3args(*[1, 2, 3]).should == [1, 2, 3] + tester.unpack_4args(1, 2, *[3, 4]).should == [1, 2, 3, 4] + tester.unpack_4args("a", %w(b c), *%w(d e)).should == ["a", ["b", "c"], "d", "e"] + end + + it "unpacks a referenced array into arguments in a method call" do + args = [1, 2, 3] + tester = ArraySpec::Splat.new + tester.unpack_3args(*args).should == [1, 2, 3] + tester.unpack_4args(0, *args).should == [0, 1, 2, 3] + end + + it "when applied to a non-Array value attempts to coerce it to Array if the object respond_to?(:to_a)" do + obj = mock("pseudo-array") + obj.should_receive(:to_a).and_return([2, 3, 4]) + [1, *obj].should == [1, 2, 3, 4] + end + + it "when applied to a non-Array value uses it unchanged if it does not respond_to?(:to_a)" do + obj = Object.new + obj.should_not respond_to(:to_a) + [1, *obj].should == [1, obj] + end + + it "when applied to a BasicObject coerces it to Array if it respond_to?(:to_a)" do + obj = BasicObject.new + def obj.to_a; [2, 3, 4]; end + [1, *obj].should == [1, 2, 3, 4] + end + + it "can be used before other non-splat elements" do + a = [1, 2] + [0, *a, 3].should == [0, 1, 2, 3] + end + + it "can be used multiple times in the same containing array" do + a = [1, 2] + b = [1, 0] + [*a, 3, *a, *b].should == [1, 2, 3, 1, 2, 1, 0] + end +end + +describe "The packing splat operator (*)" do + +end 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 new file mode 100644 index 0000000000..cc003b8946 --- /dev/null +++ b/spec/ruby/language/block_spec.rb @@ -0,0 +1,1138 @@ +require_relative '../spec_helper' +require_relative 'fixtures/block' + +describe "A block yielded a single" do + before :all do + def m(a) yield a end + end + + context "Array" do + it "assigns the Array to a single argument" do + m([1, 2]) { |a| a }.should == [1, 2] + end + + it "receives the identical Array object" do + ary = [1, 2] + m(ary) { |a| a }.should equal(ary) + end + + it "assigns the Array to a single rest argument" do + m([1, 2, 3]) { |*a| a }.should == [[1, 2, 3]] + end + + it "assigns the first element to a single argument with trailing comma" do + m([1, 2]) { |a, | a }.should == 1 + end + + it "assigns elements to required arguments" do + m([1, 2, 3]) { |a, b| [a, b] }.should == [1, 2] + end + + it "assigns nil to unassigned required arguments" do + m([1, 2]) { |a, *b, c, d| [a, b, c, d] }.should == [1, [], 2, nil] + end + + it "assigns elements to optional arguments" do + m([1, 2]) { |a=5, b=4, c=3| [a, b, c] }.should == [1, 2, 3] + end + + 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 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 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 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 "does not autosplat single argument to required arguments when a keyword rest argument is present" do + m([1, 2]) { |a, **k| [a, k] }.should == [[1, 2], {}] + end + + it "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 + + it "does not treat final Hash as keyword arguments and does not autosplat" do + result = m(["a" => 1, a: 10]) { |a=nil, **b| [a, b] } + result.should == [[{"a" => 1, a: 10}], {}] + end + + it "does not call #to_hash on final argument to get keyword arguments and does not autosplat" do + suppress_keyword_warning do + obj = mock("coerce block keyword arguments") + obj.should_not_receive(:to_hash) + + result = m([obj]) { |a=nil, **b| [a, b] } + result.should == [[obj], {}] + end + end + + it "does not call #to_hash on the argument when optional argument and keyword argument accepted and does not autosplat" do + obj = mock("coerce block keyword arguments") + obj.should_not_receive(:to_hash) + + result = m([obj]) { |a=nil, **b| [a, b] } + result.should == [[obj], {}] + end + + 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 + + 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 "does not call #to_hash on the last element if keyword arguments are present" do + obj = mock("destructure block keyword arguments") + obj.should_not_receive(:to_hash) + + result = m([1, 2, 3, obj]) { |a, *b, c, **k| [a, b, c, k] } + result.should == [1, [2, 3], obj, {}] + end + + it "does not call #to_hash on the last element when there are more arguments than parameters" do + x = mock("destructure matching block keyword argument") + x.should_not_receive(:to_hash) + + result = m([1, 2, 3, {y: 9}, 4, 5, x]) { |a, b=5, c, **k| [a, b, c, k] } + result.should == [1, 2, 3, {}] + end + + it "does not call #to_ary on the Array" do + ary = [1, 2] + ary.should_not_receive(:to_ary) + + m(ary) { |a, b, c| [a, b, c] }.should == [1, 2, nil] + end + end + + context "Object" do + it "calls #to_ary on the object when taking multiple arguments" do + obj = mock("destructure block arguments") + obj.should_receive(:to_ary).and_return([1, 2]) + + m(obj) { |a, b, c| [a, b, c] }.should == [1, 2, nil] + end + + it "does not call #to_ary when not taking any arguments" do + obj = mock("destructure block arguments") + obj.should_not_receive(:to_ary) + + m(obj) { 1 }.should == 1 + end + + it "does not call #to_ary on the object when taking a single argument" do + obj = mock("destructure block arguments") + obj.should_not_receive(:to_ary) + + m(obj) { |a| a }.should == obj + end + + it "does not call #to_ary on the object when taking a single rest argument" do + obj = mock("destructure block arguments") + obj.should_not_receive(:to_ary) + + m(obj) { |*a| a }.should == [obj] + end + + it "receives the object if #to_ary returns nil" do + obj = mock("destructure block arguments") + obj.should_receive(:to_ary).and_return(nil) + + m(obj) { |a, b, c| [a, b, c] }.should == [obj, nil, nil] + end + + it "receives the object if it does not respond to #to_ary" do + obj = Object.new + + m(obj) { |a, b, c| [a, b, c] }.should == [obj, nil, nil] + end + + it "calls #respond_to? to check if object has method #to_ary" do + obj = mock("destructure block arguments") + obj.should_receive(:respond_to?).with(:to_ary, true).and_return(true) + obj.should_receive(:to_ary).and_return([1, 2]) + + m(obj) { |a, b, c| [a, b, c] }.should == [1, 2, nil] + end + + it "receives the object if it does not respond to #respond_to?" do + obj = BasicObject.new + + m(obj) { |a, b, c| [a, b, c] }.should == [obj, nil, nil] + end + + it "calls #to_ary on the object when it is defined dynamically" do + obj = Object.new + def obj.method_missing(name, *args, &block) + if name == :to_ary + [1, 2] + else + super + end + end + def obj.respond_to_missing?(name, include_private) + name == :to_ary + end + + m(obj) { |a, b, c| [a, b, c] }.should == [1, 2, nil] + end + + it "raises a TypeError if #to_ary does not return an Array" do + obj = mock("destructure block arguments") + obj.should_receive(:to_ary).and_return(1) + + -> { m(obj) { |a, b| } }.should raise_error(TypeError) + end + + it "raises error transparently if #to_ary raises error on its own" do + obj = Object.new + def obj.to_ary; raise "Exception raised in #to_ary" end + + -> { m(obj) { |a, b| } }.should raise_error(RuntimeError, "Exception raised in #to_ary") + end + end +end + +# TODO: rewrite +describe "A block" do + before :each do + @y = BlockSpecs::Yielder.new + end + + it "captures locals from the surrounding scope" do + var = 1 + @y.z { var }.should == 1 + end + + it "allows for a leading space before the arguments" do + res = @y.s (:a){ 1 } + res.should == 1 + end + + it "allows to define a block variable with the same name as the enclosing block" do + o = BlockSpecs::OverwriteBlockVariable.new + o.z { 1 }.should == 1 + end + + it "does not capture a local when an argument has the same name" do + var = 1 + @y.s(2) { |var| var }.should == 2 + var.should == 1 + end + + it "does not capture a local when the block argument has the same name" do + var = 1 + proc { |&var| + var.call(2) + }.call { |x| x }.should == 2 + var.should == 1 + end + + describe "taking zero arguments" do + it "does not raise an exception when no values are yielded" do + @y.z { 1 }.should == 1 + end + + 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 + it "does not raise an exception when no values are yielded" do + @y.z { || 1 }.should == 1 + end + + 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 + it "assigns nil to the argument when no values are yielded" do + @y.z { |a| a }.should be_nil + end + + it "assigns the value yielded to the argument" do + @y.s(1) { |a| a }.should == 1 + end + + it "does not call #to_ary to convert a single yielded object to an Array" do + obj = mock("block yield to_ary") + obj.should_not_receive(:to_ary) + + @y.s(obj) { |a| a }.should equal(obj) + end + + it "assigns the first value yielded to the argument" do + @y.m(1, 2) { |a| a }.should == 1 + end + + 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 "assigns nil to the arguments when no values are yielded" do + @y.z { |a, b| [a, b] }.should == [nil, nil] + end + + it "assigns one value yielded to the first argument" do + @y.s(1) { |a, b| [a, b] }.should == [1, nil] + end + + it "assigns the first two values yielded to the arguments" do + @y.m(1, 2, 3) { |a, b| [a, b] }.should == [1, 2] + end + + it "does not destructure an Array value as one of several values yielded" do + @y.m([1, 2], 3, 4) { |a, b| [a, b] }.should == [[1, 2], 3] + end + + it "assigns 'nil' and 'nil' to the arguments when a single, empty Array is yielded" do + @y.s([]) { |a, b| [a, b] }.should == [nil, nil] + end + + it "assigns the element of a single element Array to the first argument" do + @y.s([1]) { |a, b| [a, b] }.should == [1, nil] + @y.s([nil]) { |a, b| [a, b] }.should == [nil, nil] + @y.s([[]]) { |a, b| [a, b] }.should == [[], nil] + end + + it "destructures a single Array value yielded" do + @y.s([1, 2, 3]) { |a, b| [a, b] }.should == [1, 2] + end + + it "destructures a splatted Array" do + @y.r([[]]) { |a, b| [a, b] }.should == [nil, nil] + @y.r([[1]]) { |a, b| [a, b] }.should == [1, nil] + end + + it "calls #to_ary to convert a single yielded object to an Array" do + obj = mock("block yield to_ary") + obj.should_receive(:to_ary).and_return([1, 2]) + + @y.s(obj) { |a, b| [a, b] }.should == [1, 2] + end + + it "does not call #to_ary if the single yielded object is an Array" do + obj = [1, 2] + obj.should_not_receive(:to_ary) + + @y.s(obj) { |a, b| [a, b] }.should == [1, 2] + end + + it "does not call #to_ary if the object does not respond to #to_ary" do + obj = mock("block yield no to_ary") + + @y.s(obj) { |a, b| [a, b] }.should == [obj, nil] + end + + it "raises a TypeError if #to_ary does not return an Array" do + obj = mock("block yield to_ary invalid") + obj.should_receive(:to_ary).and_return(1) + + -> { @y.s(obj) { |a, b| } }.should raise_error(TypeError) + end + + it "raises the original exception if #to_ary raises an exception" do + obj = mock("block yield to_ary raising an exception") + obj.should_receive(:to_ary).and_raise(ZeroDivisionError) + + -> { @y.s(obj) { |a, b| } }.should raise_error(ZeroDivisionError) + end + end + + describe "taking |a, *b| arguments" do + it "assigns 'nil' and '[]' to the arguments when no values are yielded" do + @y.z { |a, *b| [a, b] }.should == [nil, []] + end + + it "assigns all yielded values after the first to the rest argument" do + @y.m(1, 2, 3) { |a, *b| [a, b] }.should == [1, [2, 3]] + end + + it "assigns 'nil' and '[]' to the arguments when a single, empty Array is yielded" do + @y.s([]) { |a, *b| [a, b] }.should == [nil, []] + end + + it "assigns the element of a single element Array to the first argument" do + @y.s([1]) { |a, *b| [a, b] }.should == [1, []] + @y.s([nil]) { |a, *b| [a, b] }.should == [nil, []] + @y.s([[]]) { |a, *b| [a, b] }.should == [[], []] + end + + it "destructures a splatted Array" do + @y.r([[]]) { |a, *b| [a, b] }.should == [nil, []] + @y.r([[1]]) { |a, *b| [a, b] }.should == [1, []] + end + + it "destructures a single Array value assigning the remaining values to the rest argument" do + @y.s([1, 2, 3]) { |a, *b| [a, b] }.should == [1, [2, 3]] + end + + it "calls #to_ary to convert a single yielded object to an Array" do + obj = mock("block yield to_ary") + obj.should_receive(:to_ary).and_return([1, 2]) + + @y.s(obj) { |a, *b| [a, b] }.should == [1, [2]] + end + + it "does not call #to_ary if the single yielded object is an Array" do + obj = [1, 2] + obj.should_not_receive(:to_ary) + + @y.s(obj) { |a, *b| [a, b] }.should == [1, [2]] + end + + it "does not call #to_ary if the object does not respond to #to_ary" do + obj = mock("block yield no to_ary") + + @y.s(obj) { |a, *b| [a, b] }.should == [obj, []] + end + + it "raises a TypeError if #to_ary does not return an Array" do + obj = mock("block yield to_ary invalid") + obj.should_receive(:to_ary).and_return(1) + + -> { @y.s(obj) { |a, *b| } }.should raise_error(TypeError) + end + end + + describe "taking |*| arguments" do + it "does not raise an exception when no values are yielded" do + @y.z { |*| 1 }.should == 1 + end + + it "does not raise an exception when values are yielded" do + @y.s(0) { |*| 1 }.should == 1 + end + + it "does not call #to_ary if the single yielded object is an Array" do + obj = [1, 2] + obj.should_not_receive(:to_ary) + + @y.s(obj) { |*| 1 }.should == 1 + end + + it "does not call #to_ary if the object does not respond to #to_ary" do + obj = mock("block yield no to_ary") + + @y.s(obj) { |*| 1 }.should == 1 + end + + it "does not call #to_ary to convert a single yielded object to an Array" do + obj = mock("block yield to_ary") + obj.should_not_receive(:to_ary) + + @y.s(obj) { |*| 1 }.should == 1 + end + end + + describe "taking |*a| arguments" do + it "assigns '[]' to the argument when no values are yielded" do + @y.z { |*a| a }.should == [] + end + + it "assigns a single value yielded to the argument as an Array" do + @y.s(1) { |*a| a }.should == [1] + end + + it "assigns all the values passed to the argument as an Array" do + @y.m(1, 2, 3) { |*a| a }.should == [1, 2, 3] + end + + it "assigns '[[]]' to the argument when passed an empty Array" do + @y.s([]) { |*a| a }.should == [[]] + end + + it "assigns a single Array value passed to the argument by wrapping it in an Array" do + @y.s([1, 2, 3]) { |*a| a }.should == [[1, 2, 3]] + end + + it "does not call #to_ary if the single yielded object is an Array" do + obj = [1, 2] + obj.should_not_receive(:to_ary) + + @y.s(obj) { |*a| a }.should == [[1, 2]] + end + + it "does not call #to_ary if the object does not respond to #to_ary" do + obj = mock("block yield no to_ary") + + @y.s(obj) { |*a| a }.should == [obj] + end + + it "does not call #to_ary to convert a single yielded object to an Array" do + obj = mock("block yield to_ary") + obj.should_not_receive(:to_ary) + + @y.s(obj) { |*a| a }.should == [obj] + end + end + + describe "taking |a, | arguments" do + it "assigns nil to the argument when no values are yielded" do + @y.z { |a, | a }.should be_nil + end + + it "assigns the argument a single value yielded" do + @y.s(1) { |a, | a }.should == 1 + end + + it "assigns the argument the first value yielded" do + @y.m(1, 2) { |a, | a }.should == 1 + end + + it "assigns the argument the first of several values yielded when it is an Array" do + @y.m([1, 2], 3) { |a, | a }.should == [1, 2] + end + + it "assigns nil to the argument when passed an empty Array" do + @y.s([]) { |a, | a }.should be_nil + end + + it "assigns the argument the first element of the Array when passed a single Array" do + @y.s([1, 2]) { |a, | a }.should == 1 + end + + it "calls #to_ary to convert a single yielded object to an Array" do + obj = mock("block yield to_ary") + obj.should_receive(:to_ary).and_return([1, 2]) + + @y.s(obj) { |a, | a }.should == 1 + end + + it "does not call #to_ary if the single yielded object is an Array" do + obj = [1, 2] + obj.should_not_receive(:to_ary) + + @y.s(obj) { |a, | a }.should == 1 + end + + it "does not call #to_ary if the object does not respond to #to_ary" do + obj = mock("block yield no to_ary") + + @y.s(obj) { |a, | a }.should == obj + end + + it "raises a TypeError if #to_ary does not return an Array" do + obj = mock("block yield to_ary invalid") + obj.should_receive(:to_ary).and_return(1) + + -> { @y.s(obj) { |a, | } }.should raise_error(TypeError) + end + end + + describe "taking |(a, b)| arguments" do + it "assigns nil to the arguments when yielded no values" do + @y.z { |(a, b)| [a, b] }.should == [nil, nil] + end + + it "destructures a single Array value yielded" do + @y.s([1, 2]) { |(a, b)| [a, b] }.should == [1, 2] + end + + it "destructures a single Array value yielded when shadowing an outer variable" do + a = 9 + @y.s([1, 2]) { |(a, b)| [a, b] }.should == [1, 2] + end + + it "calls #to_ary to convert a single yielded object to an Array" do + obj = mock("block yield to_ary") + obj.should_receive(:to_ary).and_return([1, 2]) + + @y.s(obj) { |(a, b)| [a, b] }.should == [1, 2] + end + + it "does not call #to_ary if the single yielded object is an Array" do + obj = [1, 2] + obj.should_not_receive(:to_ary) + + @y.s(obj) { |(a, b)| [a, b] }.should == [1, 2] + end + + it "does not call #to_ary if the object does not respond to #to_ary" do + obj = mock("block yield no to_ary") + + @y.s(obj) { |(a, b)| [a, b] }.should == [obj, nil] + end + + it "raises a TypeError if #to_ary does not return an Array" do + obj = mock("block yield to_ary invalid") + obj.should_receive(:to_ary).and_return(1) + + -> { @y.s(obj) { |(a, b)| } }.should raise_error(TypeError) + end + end + + describe "taking |(a, b), c| arguments" do + it "assigns nil to the arguments when yielded no values" do + @y.z { |(a, b), c| [a, b, c] }.should == [nil, nil, nil] + end + + it "destructures a single one-level Array value yielded" do + @y.s([1, 2]) { |(a, b), c| [a, b, c] }.should == [1, nil, 2] + end + + it "destructures a single multi-level Array value yielded" do + @y.s([[1, 2, 3], 4]) { |(a, b), c| [a, b, c] }.should == [1, 2, 4] + end + + it "calls #to_ary to convert a single yielded object to an Array" do + obj = mock("block yield to_ary") + obj.should_receive(:to_ary).and_return([1, 2]) + + @y.s(obj) { |(a, b), c| [a, b, c] }.should == [1, nil, 2] + end + + it "does not call #to_ary if the single yielded object is an Array" do + obj = [1, 2] + obj.should_not_receive(:to_ary) + + @y.s(obj) { |(a, b), c| [a, b, c] }.should == [1, nil, 2] + end + + it "does not call #to_ary if the object does not respond to #to_ary" do + obj = mock("block yield no to_ary") + + @y.s(obj) { |(a, b), c| [a, b, c] }.should == [obj, nil, nil] + end + + it "raises a TypeError if #to_ary does not return an Array" do + obj = mock("block yield to_ary invalid") + obj.should_receive(:to_ary).and_return(1) + + -> { @y.s(obj) { |(a, b), c| } }.should raise_error(TypeError) + end + end + + describe "taking nested |a, (b, (c, d))|" do + it "assigns nil to the arguments when yielded no values" do + @y.m { |a, (b, (c, d))| [a, b, c, d] }.should == [nil, nil, nil, nil] + end + + it "destructures separate yielded values" do + @y.m(1, 2) { |a, (b, (c, d))| [a, b, c, d] }.should == [1, 2, nil, nil] + end + + it "destructures a nested Array value yielded" do + @y.m(1, [2, 3]) { |a, (b, (c, d))| [a, b, c, d] }.should == [1, 2, 3, nil] + end + + it "destructures a single multi-level Array value yielded" do + @y.m(1, [2, [3, 4]]) { |a, (b, (c, d))| [a, b, c, d] }.should == [1, 2, 3, 4] + end + end + + describe "taking nested |a, ((b, c), d)|" do + it "assigns nil to the arguments when yielded no values" do + @y.m { |a, ((b, c), d)| [a, b, c, d] }.should == [nil, nil, nil, nil] + end + + it "destructures separate yielded values" do + @y.m(1, 2) { |a, ((b, c), d)| [a, b, c, d] }.should == [1, 2, nil, nil] + end + + it "destructures a nested value yielded" do + @y.m(1, [2, 3]) { |a, ((b, c), d)| [a, b, c, d] }.should == [1, 2, nil, 3] + end + + it "destructures a single multi-level Array value yielded" do + @y.m(1, [[2, 3], 4]) { |a, ((b, c), d)| [a, b, c, d] }.should == [1, 2, 3, 4] + 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 + @y.m([1, [2, 3, 4]]) { |_, (_, a, _)| a }.should == 3 + end + + it "assigns the first variable named" do + @y.m(1, 2) { |_, _| _ }.should == 1 + end + end + + describe "taking identically-named arguments" do + it "raises a SyntaxError for standard arguments" do + -> { eval "lambda { |x,x| }" }.should raise_error(SyntaxError) + -> { eval "->(x,x) {}" }.should raise_error(SyntaxError) + -> { eval "Proc.new { |x,x| }" }.should raise_error(SyntaxError) + end + + it "accepts unnamed arguments" do + 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 + +describe "Block-local variables" do + it "are introduced with a semi-colon in the parameter list" do + [1].map {|one; bl| bl }.should == [nil] + end + + it "can be specified in a comma-separated list after the semi-colon" do + [1].map {|one; bl, bl2| [bl, bl2] }.should == [[nil, nil]] + end + + it "can not have the same name as one of the standard parameters" do + -> { eval "[1].each {|foo; foo| }" }.should raise_error(SyntaxError) + -> { eval "[1].each {|foo, bar; glark, bar| }" }.should raise_error(SyntaxError) + end + + it "can not be prefixed with an asterisk" 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 + -> { 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 + -> { 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 + + it "need not be preceded by standard parameters" do + [1].map {|; foo| foo }.should == [nil] + [1].map {|; glark, bar| [glark, bar] }.should == [[nil, nil]] + end + + it "only allow a single semi-colon in the parameter list" do + -> { eval "[1].each {|foo; bar; glark| }" }.should raise_error(SyntaxError) + -> { eval "[1].each {|; bar; glark| }" }.should raise_error(SyntaxError) + end + + it "override shadowed variables from the outer scope" do + out = :out + [1].each {|; out| out = :in } + out.should == :out + + a = :a + b = :b + c = :c + d = :d + {ant: :bee}.each_pair do |a, b; c, d| + a = :A + b = :B + c = :C + d = :D + end + a.should == :a + b.should == :b + c.should == :c + d.should == :d + end + + it "are not automatically instantiated in the outer scope" do + defined?(glark).should be_nil + [1].each {|;glark| 1} + defined?(glark).should be_nil + end + + it "are automatically instantiated in the block" do + [1].each do |;glark| + glark.should be_nil + end + end + + it "are visible in deeper scopes before initialization" do + [1].each {|;glark| + [1].each { + defined?(glark).should_not be_nil + glark = 1 + } + glark.should == 1 + } + end +end + +describe "Post-args" do + it "appear after a splat" do + proc do |*a, b| + [a, b] + end.call(1, 2, 3).should == [[1, 2], 3] + + proc do |*a, b, c| + [a, b, c] + end.call(1, 2, 3).should == [[1], 2, 3] + + proc do |*a, b, c, d| + [a, b, c, d] + end.call(1, 2, 3).should == [[], 1, 2, 3] + end + + 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 + proc do |a, *b, c| + [a, b, c] + end.call(1, 2, 3).should == [1, [2], 3] + end + + it "has an empty splat when there are no remaining args" do + proc do |a, b, *c, d| + [a, b, c, d] + end.call(1, 2, 3).should == [1, 2, [], 3] + + proc do |a, *b, c, d| + [a, b, c, d] + end.call(1, 2, 3).should == [1, [], 2, 3] + end + end + + describe "with optional args" do + + it "gathers remaining args in the splat" do + proc do |a=5, *b, c| + [a, b, c] + end.call(1, 2, 3).should == [1, [2], 3] + end + + it "overrides the optional arg before gathering in the splat" do + proc do |a=5, *b, c| + [a, b, c] + end.call(2, 3).should == [2, [], 3] + + proc do |a=5, b=6, *c, d| + [a, b, c, d] + end.call(1, 2, 3).should == [1, 2, [], 3] + + proc do |a=5, *b, c, d| + [a, b, c, d] + end.call(1, 2, 3).should == [1, [], 2, 3] + end + + it "uses the required arg before the optional and the splat" do + proc do |a=5, *b, c| + [a, b, c] + end.call(3).should == [5, [], 3] + + proc do |a=5, b=6, *c, d| + [a, b, c, d] + end.call(3).should == [5, 6, [], 3] + + proc do |a=5, *b, c, d| + [a, b, c, d] + end.call(2, 3).should == [5, [], 2, 3] + end + + it "overrides the optional args from left to right before gathering the splat" do + proc do |a=5, b=6, *c, d| + [a, b, c, d] + end.call(2, 3).should == [2, 6, [], 3] + end + + describe "with a circular argument reference" do + 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 + + 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 + end + + describe "with pattern matching" do + it "extracts matched blocks with post arguments" do + proc do |(a, *b, c), d, e| + [a, b, c, d, e] + end.call([1, 2, 3, 4], 5, 6).should == [1, [2, 3], 4, 5, 6] + end + + it "allows empty splats" do + proc do |a, (*), b| + [a, b] + end.call([1, 2, 3]).should == [1, 3] + 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 new file mode 100644 index 0000000000..7e5b6fb328 --- /dev/null +++ b/spec/ruby/language/break_spec.rb @@ -0,0 +1,402 @@ +require_relative '../spec_helper' +require_relative 'fixtures/break' + +describe "The break statement in a block" do + before :each do + ScratchPad.record [] + @program = BreakSpecs::Block.new + end + + it "returns nil to method invoking the method yielding to the block when not passed an argument" do + @program.break_nil + ScratchPad.recorded.should == [:a, :aa, :b, nil, :d] + end + + it "returns a value to the method invoking the method yielding to the block" do + @program.break_value + ScratchPad.recorded.should == [:a, :aa, :b, :break, :d] + end + + describe "yielded inside a while" do + it "breaks out of the block" do + value = @program.break_in_block_in_while + ScratchPad.recorded.should == [:aa, :break] + value.should == :value + end + end + + describe "captured and delegated to another method repeatedly" do + it "breaks out of the block" do + @program.looped_break_in_captured_block + ScratchPad.recorded.should == [:begin, + :preloop, + :predele, + :preyield, + :prebreak, + :postbreak, + :postyield, + :postdele, + :predele, + :preyield, + :prebreak, + :end] + end + end +end + +describe "The break statement in a captured block" do + before :each do + ScratchPad.record [] + @program = BreakSpecs::Block.new + end + + describe "when the invocation of the scope creating the block is still active" do + it "raises a LocalJumpError when invoking the block from the scope creating the block" do + -> { @program.break_in_method }.should raise_error(LocalJumpError) + ScratchPad.recorded.should == [:a, :xa, :d, :b] + end + + it "raises a LocalJumpError when invoking the block from a method" do + -> { @program.break_in_nested_method }.should raise_error(LocalJumpError) + ScratchPad.recorded.should == [:a, :xa, :cc, :aa, :b] + end + + it "raises a LocalJumpError when yielding to the block" do + -> { @program.break_in_yielding_method }.should raise_error(LocalJumpError) + ScratchPad.recorded.should == [:a, :xa, :cc, :aa, :b] + end + end + + describe "from a scope that has returned" do + it "raises a LocalJumpError when calling the block from a method" do + -> { @program.break_in_method_captured }.should raise_error(LocalJumpError) + ScratchPad.recorded.should == [:a, :za, :xa, :zd, :zb] + end + + it "raises a LocalJumpError when yielding to the block" do + -> { @program.break_in_yield_captured }.should raise_error(LocalJumpError) + ScratchPad.recorded.should == [:a, :za, :xa, :zd, :aa, :zb] + end + end + + describe "from another thread" do + it "raises a LocalJumpError when getting the value from another thread" do + thread_with_break = Thread.new do + begin + break :break + rescue LocalJumpError => e + e + end + end + thread_with_break.value.should be_an_instance_of(LocalJumpError) + end + end +end + +describe "The break statement in a lambda" do + before :each do + ScratchPad.record [] + @program = BreakSpecs::Lambda.new + end + + it "returns from the lambda" do + l = -> { + ScratchPad << :before + break :foo + ScratchPad << :after + } + l.call.should == :foo + ScratchPad.recorded.should == [:before] + end + + it "returns from the call site if the lambda is passed as a block" do + def mid(&b) + -> { + ScratchPad << :before + b.call + ScratchPad << :unreachable1 + }.call + ScratchPad << :unreachable2 + end + + result = [1].each do |e| + mid { + break # This breaks from mid + ScratchPad << :unreachable3 + } + ScratchPad << :after + end + result.should == [1] + ScratchPad.recorded.should == [:before, :after] + end + + describe "when the invocation of the scope creating the lambda is still active" do + it "returns nil when not passed an argument" do + @program.break_in_defining_scope false + ScratchPad.recorded.should == [:a, :b, nil, :d] + end + + it "returns a value to the scope creating and calling the lambda" do + @program.break_in_defining_scope + ScratchPad.recorded.should == [:a, :b, :break, :d] + end + + it "returns a value to the method scope below invoking the lambda" do + @program.break_in_nested_scope + ScratchPad.recorded.should == [:a, :d, :aa, :b, :break, :bb, :e] + end + + it "returns a value to a block scope invoking the lambda in a method below" do + @program.break_in_nested_scope_block + ScratchPad.recorded.should == [:a, :d, :aa, :aaa, :bb, :b, :break, :cc, :bbb, :dd, :e] + end + + it "returns from the lambda" do + @program.break_in_nested_scope_yield + ScratchPad.recorded.should == [:a, :d, :aaa, :b, :bbb, :e] + end + end + + describe "created at the toplevel" do + it "returns a value when invoking from the toplevel" do + code = fixture __FILE__, "break_lambda_toplevel.rb" + ruby_exe(code).chomp.should == "a,b,break,d" + end + + it "returns a value when invoking from a method" do + code = fixture __FILE__, "break_lambda_toplevel_method.rb" + ruby_exe(code).chomp.should == "a,d,b,break,e,f" + end + + it "returns a value when invoking from a block" do + code = fixture __FILE__, "break_lambda_toplevel_block.rb" + ruby_exe(code).chomp.should == "a,d,f,b,break,g,e,h" + end + end + + describe "from a scope that has returned" do + it "returns a value to the method scope invoking the lambda" do + @program.break_in_method + ScratchPad.recorded.should == [:a, :la, :ld, :lb, :break, :b] + end + + it "returns a value to the block scope invoking the lambda in a method" do + @program.break_in_block_in_method + ScratchPad.recorded.should == [:a, :aaa, :b, :la, :ld, :lb, :break, :c, :bbb, :d] + end + + # By passing a lambda as a block argument, the user is requesting to treat + # the lambda as a block, which in this case means breaking to a scope that + # has returned. This is a subtle and confusing semantic where a block pass + # is removing the lambda-ness of a lambda. + it "raises a LocalJumpError when yielding to a lambda passed as a block argument" do + @program.break_in_method_yield + ScratchPad.recorded.should == [:a, :la, :ld, :aaa, :lb, :bbb, :b] + end + end +end + +describe "Break inside a while loop" do + describe "with a value" do + it "exits the loop and returns the value" do + a = while true; break; end; a.should == nil + a = while true; break nil; end; a.should == nil + a = while true; break 1; end; a.should == 1 + a = while true; break []; end; a.should == [] + a = while true; break [1]; end; a.should == [1] + end + + it "passes the value returned by a method with omitted parenthesis and passed block" do + obj = BreakSpecs::Block.new + -> { break obj.method :value do |x| x end }.call.should == :value + end + end + + describe "with a splat" do + it "exits the loop and makes the splat an Array" do + a = while true; break *[1,2]; end; a.should == [1,2] + end + + it "treats nil as an empty array" do + a = while true; break *nil; end; a.should == [] + end + + it "preserves an array as is" do + a = while true; break *[]; end; a.should == [] + a = while true; break *[1,2]; end; a.should == [1,2] + a = while true; break *[nil]; end; a.should == [nil] + a = while true; break *[[]]; end; a.should == [[]] + end + + it "wraps a non-Array in an Array" do + a = while true; break *1; end; a.should == [1] + end + end + + it "stops a while loop when run" do + i = 0 + while true + break if i == 2 + i+=1 + end + i.should == 2 + end + + it "causes a call with a block to return when run" do + at = 0 + 0.upto(5) do |i| + at = i + break i if i == 2 + end.should == 2 + at.should == 2 + 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. +describe "Executing break from within a block" do + + before :each do + ScratchPad.clear + end + + # Discovered in JRuby (see JRUBY-2756) + it "returns from the original invoking method even in case of chained calls" do + class BreakTest + # case #1: yield + def self.meth_with_yield(&b) + yield + fail("break returned from yield to wrong place") + end + def self.invoking_method(&b) + meth_with_yield(&b) + fail("break returned from 'meth_with_yield' method to wrong place") + end + + # case #2: block.call + def self.meth_with_block_call(&b) + b.call + fail("break returned from b.call to wrong place") + end + def self.invoking_method2(&b) + meth_with_block_call(&b) + fail("break returned from 'meth_with_block_call' method to wrong place") + end + end + + # this calls a method that calls another method that yields to the block + BreakTest.invoking_method do + break + fail("break didn't, well, break") + end + + # this calls a method that calls another method that calls the block + BreakTest.invoking_method2 do + break + fail("break didn't, well, break") + end + + res = BreakTest.invoking_method do + break :return_value + fail("break didn't, well, break") + end + res.should == :return_value + + res = BreakTest.invoking_method2 do + break :return_value + fail("break didn't, well, break") + end + res.should == :return_value + + end + + class BreakTest2 + def one + two { yield } + end + + def two + yield + ensure + ScratchPad << :two_ensure + end + + def three + begin + one { break } + ScratchPad << :three_post + ensure + ScratchPad << :three_ensure + end + end + end + + it "runs ensures when continuing upward" do + ScratchPad.record [] + + bt2 = BreakTest2.new + bt2.one { break } + ScratchPad.recorded.should == [:two_ensure] + end + + it "runs ensures when breaking from a loop" do + ScratchPad.record [] + + while true + begin + ScratchPad << :begin + break if true + ensure + ScratchPad << :ensure + end + end + + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "doesn't run ensures in the destination method" do + ScratchPad.record [] + + bt2 = BreakTest2.new + 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 new file mode 100644 index 0000000000..464d06e46a --- /dev/null +++ b/spec/ruby/language/case_spec.rb @@ -0,0 +1,509 @@ +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 + 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 + end.should == true + end + + it "evaluates the body of the when clause in left-to-right order if it's an array expression" do + @calls = [] + def foo; @calls << :foo; end + def bar; @calls << :bar; end + + case true + when foo, bar; + end + + @calls.should == [:foo, :bar] + end + + 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 + end.should == true + end + + it "returns nil when no 'then'-bodies are given" do + case "a" + 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' + 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 + end.should == nil + end + + it "returns 2 when a then body is empty" do + case Object.new + when Numeric then + 1 + when String then + # ok + else + 2 + end.should == 2 + end + + it "returns the statement following 'then'" do + case "a" + 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' + end.should == 'foo' + end + + it "tests with matching regexps" do + case "hello" + 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" + end.should == "foo" + end + + it "takes lists of values" do + case 'z' + 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" + 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" + 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" + end.should == "foo" + + case 'b' + 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" + 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" + 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" + end.should == "foo" + end + + it "expands multiple arrays from variables before additional listed values" do + a = ['a', 'b', 'c'] + b = ['d', 'e', 'f'] + + case 'f' + when *a, *b, 'g', 'h' + "foo" + when 'x' + "bar" + end.should == "foo" + end + + # MR: critical + it "concats arrays before expanding them" do + a = ['a', 'b', 'c', 'd'] + b = ['f'] + + case 'f' + 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" + 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' + end.should == 'bar' + end + + it "raises a SyntaxError when 'else' is used when no 'when' is given" do + -> { + eval <<-CODE + case 4 + else + true + end + CODE + }.should raise_error(SyntaxError) + end + + it "raises a SyntaxError when 'else' is used before a 'when' was given" do + -> { + eval <<-CODE + case 4 + else + true + when 4; false + end + CODE + }.should raise_error(SyntaxError) + end + + it "supports nested case statements" do + result = false + case :x + when Symbol + case :y + when Symbol + result = true + end + end + result.should == true + end + + it "supports nested case statements followed by a when with a splatted array" do + result = false + case :x + when Symbol + case :y + when Symbol + result = true + end + when *[Symbol] + result = false + end + result.should == true + end + + it "supports nested case statements followed by a when with a splatted non-array" do + result = false + case :x + when Symbol + case :y + when Symbol + result = true + end + when *Symbol + result = false + end + result.should == true + end + + it "works even if there's only one when statement" do + case 1 + when 1 + 100 + end.should == 100 + end + + it "evaluates true as only 'true' when true is the first clause" do + case 1 + 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" + end.should == "good" + end + + it "treats a literal array as its own when argument, rather than a list of arguments" do + case 'foo' + when ['foo', 'foo']; 'bad' + when 'foo'; 'good' + end.should == 'good' + end + + it "takes multiple expanded arrays" do + a1 = ['f', 'o', 'o'] + a2 = ['b', 'a', 'r'] + + case 'f' + 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" + end.should == "bar" + end + + it "calls === even when private" do + klass = Class.new do + def ===(o) + true + end + private :=== + end + + case 1 + when klass.new + :called + end.should == :called + end + + it "accepts complex expressions within ()" do + case 'a' + when (raise if 2+2 == 3; /a/) + :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 new file mode 100644 index 0000000000..6fb785fd56 --- /dev/null +++ b/spec/ruby/language/class_spec.rb @@ -0,0 +1,395 @@ +require_relative '../spec_helper' +require_relative '../fixtures/class' + +ClassSpecsNumber = 12 + +module ClassSpecs + Number = 12 +end + +describe "The class keyword" do + it "creates a new class with semicolon" do + class ClassSpecsKeywordWithSemicolon; end + ClassSpecsKeywordWithSemicolon.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 + +describe "A class definition" do + it "creates a new class" do + ClassSpecs::A.should be_kind_of(Class) + ClassSpecs::A.new.should be_kind_of(ClassSpecs::A) + end + + it "has no class variables" do + ClassSpecs::A.class_variables.should == [] + end + + it "raises TypeError if constant given as class name exists and is not a Module" do + -> { + class ClassSpecsNumber + end + }.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 + -> { + class nil::Foo + end + }.should raise_error(TypeError) + end + + it "raises TypeError if any constant qualifying the class is not a Module" do + -> { + class ClassSpecs::Number::MyClass + end + }.should raise_error(TypeError) + + -> { + class ClassSpecsNumber::MyClass + end + }.should raise_error(TypeError) + end + + it "inherits from Object by default" do + ClassSpecs::A.superclass.should == Object + end + + it "raises an error when trying to change the superclass" do + module ClassSpecs + class SuperclassResetToSubclass < L + end + -> { + class SuperclassResetToSubclass < M + end + }.should raise_error(TypeError, /superclass mismatch/) + end + end + + it "raises an error when reopening a class with BasicObject as superclass" do + module ClassSpecs + class SuperclassReopenedBasicObject < A + end + SuperclassReopenedBasicObject.superclass.should == A + + -> { + class SuperclassReopenedBasicObject < BasicObject + end + }.should raise_error(TypeError, /superclass mismatch/) + SuperclassReopenedBasicObject.superclass.should == A + end + end + + # [Bug #12367] [ruby-core:75446] + it "raises an error when reopening a class with Object as superclass" do + module ClassSpecs + class SuperclassReopenedObject < A + end + SuperclassReopenedObject.superclass.should == A + + -> { + class SuperclassReopenedObject < Object + end + }.should raise_error(TypeError, /superclass mismatch/) + SuperclassReopenedObject.superclass.should == A + end + end + + it "allows reopening a class without specifying the superclass" do + module ClassSpecs + class SuperclassNotGiven < A + end + SuperclassNotGiven.superclass.should == A + + class SuperclassNotGiven + end + SuperclassNotGiven.superclass.should == A + end + end + + it "does not allow to set the superclass even if it was not specified by the first declaration" do + module ClassSpecs + class NoSuperclassSet + end + + -> { + class NoSuperclassSet < String + end + }.should raise_error(TypeError, /superclass mismatch/) + end + end + + it "allows using self as the superclass if self is a class" do + ClassSpecs::I::J.superclass.should == ClassSpecs::I + + -> { + class ShouldNotWork < self; end + }.should raise_error(TypeError) + end + + it "first evaluates the superclass before checking if the class already exists" do + module ClassSpecs + class SuperclassEvaluatedFirst + end + a = SuperclassEvaluatedFirst + + class SuperclassEvaluatedFirst < remove_const(:SuperclassEvaluatedFirst) + end + b = SuperclassEvaluatedFirst + b.superclass.should == a + end + end + + it "raises a TypeError if inheriting from a metaclass" do + obj = mock("metaclass super") + meta = obj.singleton_class + -> { class ClassSpecs::MetaclassSuper < meta; end }.should raise_error(TypeError) + end + + it "allows the declaration of class variables in the body" do + ClassSpecs.string_class_variables(ClassSpecs::B).should == ["@@cvar"] + ClassSpecs::B.send(:class_variable_get, :@@cvar).should == :cvar + end + + it "stores instance variables defined in the class body in the class object" do + ClassSpecs.string_instance_variables(ClassSpecs::B).should include("@ivar") + ClassSpecs::B.instance_variable_get(:@ivar).should == :ivar + end + + it "allows the declaration of class variables in a class method" do + ClassSpecs::C.class_variables.should == [] + ClassSpecs::C.make_class_variable + ClassSpecs.string_class_variables(ClassSpecs::C).should == ["@@cvar"] + ClassSpecs::C.remove_class_variable :@@cvar + end + + it "allows the definition of class-level instance variables in a class method" do + ClassSpecs.string_instance_variables(ClassSpecs::C).should_not include("@civ") + ClassSpecs::C.make_class_instance_variable + ClassSpecs.string_instance_variables(ClassSpecs::C).should include("@civ") + ClassSpecs::C.remove_instance_variable :@civ + end + + it "allows the declaration of class variables in an instance method" do + ClassSpecs::D.class_variables.should == [] + ClassSpecs::D.new.make_class_variable + ClassSpecs.string_class_variables(ClassSpecs::D).should == ["@@cvar"] + ClassSpecs::D.remove_class_variable :@@cvar + end + + it "allows the definition of instance methods" do + ClassSpecs::E.new.meth.should == :meth + end + + it "allows the definition of class methods" do + ClassSpecs::E.cmeth.should == :cmeth + end + + it "allows the definition of class methods using class << self" do + ClassSpecs::E.smeth.should == :smeth + end + + it "allows the definition of Constants" do + Object.const_defined?('CONSTANT').should == false + ClassSpecs::E.const_defined?('CONSTANT').should == true + ClassSpecs::E::CONSTANT.should == :constant! + end + + it "returns the value of the last statement in the body" do + class ClassSpecs::Empty; end.should == nil + class ClassSpecs::Twenty; 20; end.should == 20 + class ClassSpecs::Plus; 10 + 20; end.should == 30 + class ClassSpecs::Singleton; class << self; :singleton; end; end.should == :singleton + end + + 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 CS_CONST_CLASS_SPECS + end + + def self.get_class_name + CS_CONST_CLASS_SPECS.name + end + end + + 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 + klass = ClassSpecs::ANON_CLASS_FOR_NEW.call + + ClassSpecs::NamedInModule.name.should == 'ClassSpecs::NamedInModule' + klass.get_class_name.should == 'ClassSpecs::NamedInModule' + end + + it "for anonymous classes" do + klass = Class.new do + def self.get_class + Class.new do + def self.foo + 'bar' + end + end + end + + def self.get_result + get_class.foo + end + end + + klass.get_result.should == 'bar' + end + + it "for anonymous classes assigned to a constant" do + klass = Class.new do + AnonWithConstant = Class.new + + def self.get_class_name + AnonWithConstant.name + end + end + + AnonWithConstant.name.should == 'AnonWithConstant' + klass.get_class_name.should == 'AnonWithConstant' + ensure + Object.send(:remove_const, :AnonWithConstant) + end + end +end + +describe "An outer class definition" do + it "contains the inner classes" do + ClassSpecs::Container.constants.should include(:A, :B) + end +end + +describe "A class definition extending an object (sclass)" do + it "allows adding methods" do + ClassSpecs::O.smeth.should == :smeth + end + + it "raises a TypeError when trying to extend numbers" do + -> { + eval <<-CODE + class << 1 + def xyz + self + end + end + CODE + }.should raise_error(TypeError) + end + + it "raises a TypeError when trying to extend non-Class" do + error_msg = /superclass must be a.* Class/ + -> { class TestClass < ""; end }.should raise_error(TypeError, error_msg) + -> { class TestClass < 1; end }.should raise_error(TypeError, error_msg) + -> { class TestClass < :symbol; end }.should raise_error(TypeError, error_msg) + -> { class TestClass < mock('o'); end }.should raise_error(TypeError, error_msg) + -> { class TestClass < Module.new; end }.should raise_error(TypeError, error_msg) + -> { class TestClass < BasicObject.new; end }.should raise_error(TypeError, error_msg) + end + + 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 + ClassSpecs.sclass_with_return.should == :inner + end +end + +describe "Reopening a class" do + it "extends the previous definitions" do + c = ClassSpecs::F.new + c.meth.should == :meth + c.another.should == :another + end + + it "overwrites existing methods" do + ClassSpecs::G.new.override.should == :override + end + + it "raises a TypeError when superclasses mismatch" do + -> { class ClassSpecs::A < Array; end }.should raise_error(TypeError) + end + + it "adds new methods to subclasses" do + -> { ClassSpecs::M.m }.should raise_error(NoMethodError) + class ClassSpecs::L + def self.m + 1 + end + end + 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 + it "calls inherited when a class is created" do + ClassSpecs::H.track_inherited.should == [ClassSpecs::K] + end +end diff --git a/spec/ruby/language/class_variable_spec.rb b/spec/ruby/language/class_variable_spec.rb new file mode 100644 index 0000000000..a26a3fb8de --- /dev/null +++ b/spec/ruby/language/class_variable_spec.rb @@ -0,0 +1,114 @@ +require_relative '../spec_helper' +require_relative '../fixtures/class_variables' + +describe "A class variable" do + after :each do + ClassVariablesSpec::ClassA.new.cvar_a = :cvar_a + end + + it "can be accessed from a subclass" do + ClassVariablesSpec::ClassB.new.cvar_a.should == :cvar_a + end + + it "is set in the superclass" do + a = ClassVariablesSpec::ClassA.new + b = ClassVariablesSpec::ClassB.new + b.cvar_a = :new_val + + a.cvar_a.should == :new_val + end +end + +describe "A class variable defined in a module" do + after :each do + ClassVariablesSpec::ClassC.cvar_m = :value + ClassVariablesSpec::ClassC.remove_class_variable(:@@cvar) if ClassVariablesSpec::ClassC.cvar_defined? + end + + it "can be accessed from classes that extend the module" do + ClassVariablesSpec::ClassC.cvar_m.should == :value + end + + it "is not defined in these classes" do + ClassVariablesSpec::ClassC.cvar_defined?.should be_false + end + + it "is only updated in the module a method defined in the module is used" do + ClassVariablesSpec::ClassC.cvar_m = "new value" + ClassVariablesSpec::ClassC.cvar_m.should == "new value" + + ClassVariablesSpec::ClassC.cvar_defined?.should be_false + end + + it "is updated in the class when a Method defined in the class is used" do + ClassVariablesSpec::ClassC.cvar_c = "new value" + ClassVariablesSpec::ClassC.cvar_defined?.should be_true + end + + it "can be accessed inside the class using the module methods" do + ClassVariablesSpec::ClassC.cvar_c = "new value" + ClassVariablesSpec::ClassC.cvar_m.should == :value + end + + it "can be accessed from modules that extend the module" do + ClassVariablesSpec::ModuleO.cvar_n.should == :value + end + + it "is defined in the extended module" do + ClassVariablesSpec::ModuleN.class_variable_defined?(:@@cvar_n).should be_true + end + + it "is not defined in the extending module" do + ClassVariablesSpec::ModuleO.class_variable_defined?(:@@cvar_n).should be_false + end +end + +describe 'A class variable definition' do + it "is created in a module if any of the parents do not define it" do + a = Class.new + b = Class.new(a) + c = Class.new(b) + b.class_variable_set(:@@cv, :value) + + -> { 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) + + -> { 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 new file mode 100644 index 0000000000..063c52c422 --- /dev/null +++ b/spec/ruby/language/constants_spec.rb @@ -0,0 +1,809 @@ +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. + +describe "Literal (A::X) constant resolution" do + describe "with statically assigned constants" do + it "searches the immediate class or module scope first" do + ConstantSpecs::ClassA::CS_CONST10.should == :const10_10 + ConstantSpecs::ModuleA::CS_CONST10.should == :const10_1 + ConstantSpecs::ParentA::CS_CONST10.should == :const10_5 + ConstantSpecs::ContainerA::CS_CONST10.should == :const10_2 + ConstantSpecs::ContainerA::ChildA::CS_CONST10.should == :const10_3 + end + + it "searches a module included in the immediate class before the superclass" do + ConstantSpecs::ContainerA::ChildA::CS_CONST15.should == :const15_1 + end + + it "searches the superclass before a module included in the superclass" do + ConstantSpecs::ContainerA::ChildA::CS_CONST11.should == :const11_1 + end + + it "searches a module included in the superclass" do + ConstantSpecs::ContainerA::ChildA::CS_CONST12.should == :const12_1 + end + + it "searches the superclass chain" do + ConstantSpecs::ContainerA::ChildA::CS_CONST13.should == :const13 + end + + it "searches Object if no class or module qualifier is given" do + CS_CONST1.should == :const1 + CS_CONST10.should == :const10_1 + end + + it "searches Object after searching other scopes" do + module ConstantSpecs::SpecAdded1 + CS_CONST10.should == :const10_1 + end + end + + it "searches Object if a toplevel qualifier (::X) is given" do + ::CS_CONST1.should == :const1 + ::CS_CONST10.should == :const10_1 + end + + it "does not search the singleton class of the class or module" do + -> do + ConstantSpecs::ContainerA::ChildA::CS_CONST14 + end.should raise_error(NameError) + -> { ConstantSpecs::CS_CONST14 }.should raise_error(NameError) + end + end + + describe "with dynamically assigned constants" do + it "searches the immediate class or module scope first" do + ConstantSpecs::ClassB::CS_CONST101 = :const101_1 + ConstantSpecs::ClassB::CS_CONST101.should == :const101_1 + + ConstantSpecs::ParentB::CS_CONST101 = :const101_2 + ConstantSpecs::ParentB::CS_CONST101.should == :const101_2 + + ConstantSpecs::ContainerB::CS_CONST101 = :const101_3 + ConstantSpecs::ContainerB::CS_CONST101.should == :const101_3 + + ConstantSpecs::ContainerB::ChildB::CS_CONST101 = :const101_4 + ConstantSpecs::ContainerB::ChildB::CS_CONST101.should == :const101_4 + + 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 + class << ConstantSpecs::ContainerB::ChildB + CS_CONST108 = :const108_1 + end + + -> do + ConstantSpecs::ContainerB::ChildB::CS_CONST108 + end.should raise_error(NameError) + + module ConstantSpecs + class << self + CS_CONST108 = :const108_2 + end + end + + -> { 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 + ConstantSpecs::ClassB::CS_CONST109 = :const109_1 + ConstantSpecs::ClassB::CS_CONST109.should == :const109_1 + + -> { + 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 left-to-right" do + mod = Module.new + + mod.module_eval <<-EOC + order = [] + ConstantSpecsRHS = Module.new + (order << :lhs; ConstantSpecsRHS)::B = (order << :rhs) + EOC + + mod::ConstantSpecsRHS::B.should == [:lhs, :rhs] + end + end + + it "raises a NameError if no constant is defined in the search path" do + -> { ConstantSpecs::ParentA::CS_CONSTX }.should raise_error(NameError) + 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 + ConstantSpecs::ClassA::CS_CONSTX.should == :CS_CONSTX + end + + it "evaluates the qualifier" do + ConstantSpecs.get_const::CS_CONST2.should == :const2 + end + + it "raises a TypeError if a non-class or non-module qualifier is given" do + -> { CS_CONST1::CS_CONST }.should raise_error(TypeError) + -> { 1::CS_CONST }.should raise_error(TypeError) + -> { "mod"::CS_CONST }.should raise_error(TypeError) + -> { false::CS_CONST }.should raise_error(TypeError) + end +end + +describe "Constant resolution within methods" do + describe "with statically assigned constants" do + it "searches the immediate class or module scope first" do + ConstantSpecs::ClassA.const10.should == :const10_10 + ConstantSpecs::ParentA.const10.should == :const10_5 + ConstantSpecs::ContainerA.const10.should == :const10_2 + ConstantSpecs::ContainerA::ChildA.const10.should == :const10_3 + + ConstantSpecs::ClassA.new.const10.should == :const10_10 + ConstantSpecs::ParentA.new.const10.should == :const10_5 + ConstantSpecs::ContainerA::ChildA.new.const10.should == :const10_3 + end + + it "searches a module included in the immediate class before the superclass" do + ConstantSpecs::ContainerA::ChildA.const15.should == :const15_1 + ConstantSpecs::ContainerA::ChildA.new.const15.should == :const15_1 + end + + it "searches the superclass before a module included in the superclass" do + ConstantSpecs::ContainerA::ChildA.const11.should == :const11_1 + ConstantSpecs::ContainerA::ChildA.new.const11.should == :const11_1 + end + + it "searches a module included in the superclass" do + ConstantSpecs::ContainerA::ChildA.const12.should == :const12_1 + ConstantSpecs::ContainerA::ChildA.new.const12.should == :const12_1 + end + + it "searches the superclass chain" do + ConstantSpecs::ContainerA::ChildA.const13.should == :const13 + ConstantSpecs::ContainerA::ChildA.new.const13.should == :const13 + end + + it "searches the lexical scope of the method not the receiver's immediate class" do + ConstantSpecs::ContainerA::ChildA.const19.should == :const19_1 + end + + it "searches the lexical scope of a singleton method" do + ConstantSpecs::CS_CONST18.const17.should == :const17_1 + end + + it "does not search the lexical scope of the caller" do + -> { ConstantSpecs::ClassA.const16 }.should raise_error(NameError) + end + + it "searches the lexical scope of a block" do + ConstantSpecs::ClassA.const22.should == :const22_1 + end + + it "searches Object as a lexical scope only if Object is explicitly opened" do + ConstantSpecs::ContainerA::ChildA.const20.should == :const20_1 + ConstantSpecs::ContainerA::ChildA.const21.should == :const21_1 + end + + it "does not search the lexical scope of qualifying modules" do + -> do + ConstantSpecs::ContainerA::ChildA.const23 + end.should raise_error(NameError) + end + end + + describe "with dynamically assigned constants" do + it "searches the immediate class or module scope first" do + ConstantSpecs::ModuleA::CS_CONST201 = :const201_1 + + class ConstantSpecs::ClassB; CS_CONST201 = :const201_2; end + ConstantSpecs::ParentB::CS_CONST201 = :const201_3 + ConstantSpecs::ContainerB::CS_CONST201 = :const201_4 + ConstantSpecs::ContainerB::ChildB::CS_CONST201 = :const201_5 + + ConstantSpecs::ClassB.const201.should == :const201_2 + ConstantSpecs::ParentB.const201.should == :const201_3 + ConstantSpecs::ContainerB.const201.should == :const201_4 + ConstantSpecs::ContainerB::ChildB.const201.should == :const201_5 + + 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 + ConstantSpecs::ParentB::CS_CONST202 = :const202_2 + ConstantSpecs::ContainerB::ChildB::CS_CONST202 = :const202_1 + + 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 + ConstantSpecs::ParentB::CS_CONST203 = :const203_1 + ConstantSpecs::ModuleE::CS_CONST203 = :const203_2 + + 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 + ConstantSpecs::ModuleA::CS_CONST204 = :const204_2 + ConstantSpecs::ModuleE::CS_CONST204 = :const204_1 + + 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 + ConstantSpecs::ModuleA::CS_CONST205 = :const205 + + 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 + ConstantSpecs::ContainerB::ChildB::CS_CONST206 = :const206_2 + class ConstantSpecs::ContainerB::ChildB + class << self + CS_CONST206 = :const206_1 + end + 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 + ConstantSpecs::CS_CONST207 = :const207_1 + ConstantSpecs::ClassB::CS_CONST207 = :const207_2 + + ConstantSpecs::CS_CONST208.const207.should == :const207_1 + ensure + ConstantSpecs.send(:remove_const, :CS_CONST207) + ConstantSpecs::ClassB.send(:remove_const, :CS_CONST207) + end + + it "does not search the lexical scope of the caller" do + ConstantSpecs::ClassB::CS_CONST209 = :const209 + + -> { ConstantSpecs::ClassB.const209 }.should raise_error(NameError) + ensure + ConstantSpecs::ClassB.send(:remove_const, :CS_CONST209) + end + + it "searches the lexical scope of a block" do + ConstantSpecs::ClassB::CS_CONST210 = :const210_1 + 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 + Object::CS_CONST211 = :const211_1 + ConstantSpecs::ParentB::CS_CONST211 = :const211_2 + ConstantSpecs::ContainerB::ChildB.const211.should == :const211_1 + + 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 + ConstantSpecs::ParentB::CS_CONST213 = :const213_1 + ConstantSpecs::ContainerB::ChildB.const213.should == :const213_1 + ConstantSpecs::ContainerB::ChildB.new.const213.should == :const213_1 + + -> { + ConstantSpecs::ParentB::CS_CONST213 = :const213_2 + }.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 + + -> 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 + -> { 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 +end + +describe "Constant resolution within a singleton class (class << obj)" do + it "works like normal classes or modules" do + ConstantSpecs::CS_SINGLETON1.foo.should == 1 + 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 "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 + 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) + end +end + +describe "Module#private_constant marked constants" do + + it "remain private even when updated" do + mod = Module.new + mod.const_set :Foo, true + mod.send :private_constant, :Foo + -> { + mod.const_set :Foo, false + }.should complain(/already initialized constant/) + + -> {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 + -> 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 + -> 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 + -> do + class ConstantVisibility::ModuleContainer::PrivateClass; end + end.should raise_error(NameError) + end + + it "can be reopened as a module where constant is not private" do + module ::ConstantVisibility::ModuleContainer + module PrivateModule + X = 1 + end + + 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 + module ::ConstantVisibility::ModuleContainer + class PrivateClass + X = 1 + end + + 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 + defined?(ConstantVisibility::PrivConstModule::PRIVATE_CONSTANT_MODULE).should == nil + end + + it "can be accessed from the module itself" do + ConstantVisibility::PrivConstModule.private_constant_from_self.should be_true + end + + it "is defined? from the module itself" do + ConstantVisibility::PrivConstModule.defined_from_self.should == "constant" + end + + it "can be accessed from lexical scope" do + ConstantVisibility::PrivConstModule::Nested.private_constant_from_scope.should be_true + end + + it "is defined? from lexical scope" do + ConstantVisibility::PrivConstModule::Nested.defined_from_scope.should == "constant" + end + + it "can be accessed from classes that include the module" do + 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::ClassIncludingPrivConstModule.new.defined_from_include.should == "constant" + end + end + + describe "in a class" do + it "cannot be accessed from outside the class" do + -> do + ConstantVisibility::PrivConstClass::PRIVATE_CONSTANT_CLASS + end.should raise_error(NameError) + end + + it "cannot be reopened as a module" do + -> do + module ConstantVisibility::ClassContainer::PrivateModule; end + end.should raise_error(NameError) + end + + it "cannot be reopened as a class" do + -> do + class ConstantVisibility::ClassContainer::PrivateClass; end + end.should raise_error(NameError) + end + + it "can be reopened as a module where constant is not private" do + class ::ConstantVisibility::ClassContainer + module PrivateModule + X = 1 + end + + 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 + class ::ConstantVisibility::ClassContainer + class PrivateClass + X = 1 + end + + 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 + defined?(ConstantVisibility::PrivConstClass::PRIVATE_CONSTANT_CLASS).should == nil + end + + it "can be accessed from the class itself" do + ConstantVisibility::PrivConstClass.private_constant_from_self.should be_true + end + + it "is defined? from the class itself" do + ConstantVisibility::PrivConstClass.defined_from_self.should == "constant" + end + + it "can be accessed from lexical scope" do + ConstantVisibility::PrivConstClass::Nested.private_constant_from_scope.should be_true + end + + it "is defined? from lexical scope" do + ConstantVisibility::PrivConstClass::Nested.defined_from_scope.should == "constant" + end + + it "can be accessed from subclasses" do + ConstantVisibility::PrivConstClassChild.new.private_constant_from_subclass.should be_true + end + + it "is defined? from subclasses" do + ConstantVisibility::PrivConstClassChild.new.defined_from_subclass.should == "constant" + end + end + + describe "in Object" do + it "cannot be accessed using ::Const form" do + -> do + ::PRIVATE_CONSTANT_IN_OBJECT + end.should raise_error(NameError) + end + + it "is not defined? using ::Const form" do + defined?(::PRIVATE_CONSTANT_IN_OBJECT).should == nil + end + + it "can be accessed through the normal search" do + PRIVATE_CONSTANT_IN_OBJECT.should == true + end + + it "is defined? through the normal search" 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 + before :each do + @module = ConstantVisibility::PrivConstModule.dup + end + + describe "in a module" do + it "can be accessed from outside the module" do + @module.send :public_constant, :PRIVATE_CONSTANT_MODULE + @module::PRIVATE_CONSTANT_MODULE.should == true + end + + it "is defined? with A::B form" do + @module.send :public_constant, :PRIVATE_CONSTANT_MODULE + defined?(@module::PRIVATE_CONSTANT_MODULE).should == "constant" + end + end + + describe "in a class" do + before :each do + @class = ConstantVisibility::PrivConstClass.dup + end + + it "can be accessed from outside the class" do + @class.send :public_constant, :PRIVATE_CONSTANT_CLASS + @class::PRIVATE_CONSTANT_CLASS.should == true + end + + it "is defined? with A::B form" do + @class.send :public_constant, :PRIVATE_CONSTANT_CLASS + defined?(@class::PRIVATE_CONSTANT_CLASS).should == "constant" + end + end + + describe "in Object" do + after :each do + ConstantVisibility.reset_private_constants + end + + it "can be accessed using ::Const form" do + Object.send :public_constant, :PRIVATE_CONSTANT_IN_OBJECT + ::PRIVATE_CONSTANT_IN_OBJECT.should == true + end + + it "is defined? using ::Const form" do + Object.send :public_constant, :PRIVATE_CONSTANT_IN_OBJECT + defined?(::PRIVATE_CONSTANT_IN_OBJECT).should == "constant" + 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 new file mode 100644 index 0000000000..0cf1790791 --- /dev/null +++ b/spec/ruby/language/def_spec.rb @@ -0,0 +1,815 @@ +require_relative '../spec_helper' +require_relative 'fixtures/def' + +# Language-level method behaviour +describe "Redefining a method" do + it "replaces the original method" do + def barfoo; 100; end + barfoo.should == 100 + + def barfoo; 200; end + barfoo.should == 200 + end +end + +describe "Defining a method at the top-level" do + it "defines it on Object with private visibility by default" do + Object.should have_private_instance_method(:some_toplevel_method, false) + end + + it "defines it on Object with public visibility after calling public" do + Object.should have_public_instance_method(:public_toplevel_method, false) + end +end + +describe "Defining an 'initialize' method" do + it "sets the method's visibility to private" do + class DefInitializeSpec + def initialize + end + end + DefInitializeSpec.should have_private_instance_method(:initialize, false) + end +end + +describe "Defining an 'initialize_copy' method" do + it "sets the method's visibility to private" do + class DefInitializeCopySpec + def initialize_copy + end + end + DefInitializeCopySpec.should have_private_instance_method(:initialize_copy, false) + end +end + +describe "Defining an 'initialize_dup' method" do + it "sets the method's visibility to private" do + class DefInitializeDupSpec + def initialize_dup + end + end + DefInitializeDupSpec.should have_private_instance_method(:initialize_dup, false) + end +end + +describe "Defining an 'initialize_clone' method" do + it "sets the method's visibility to private" do + class DefInitializeCloneSpec + def initialize_clone + end + end + DefInitializeCloneSpec.should have_private_instance_method(:initialize_clone, false) + end +end + +describe "Defining a 'respond_to_missing?' method" do + it "sets the method's visibility to private" do + class DefRespondToMissingPSpec + def respond_to_missing? + end + end + DefRespondToMissingPSpec.should have_private_instance_method(:respond_to_missing?, false) + end +end + +describe "Defining a method" do + it "returns a symbol of the method name" do + method_name = def some_method; end + method_name.should == :some_method + 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; + + foo.should == nil + foo(1, 2).should == nil + foo(1, 2, 3, 4, :a, :b, 'c', 'd').should == nil + end + + it "accepts a named * argument" do + def foo(*a); a; end; + foo.should == [] + foo(1, 2).should == [1, 2] + foo([:a]).should == [[:a]] + end + + it "accepts non-* arguments before the * argument" do + def foo(a, b, c, d, e, *f); [a, b, c, d, e, f]; end + foo(1, 2, 3, 4, 5, 6, 7, 8).should == [1, 2, 3, 4, 5, [6, 7, 8]] + end + + it "allows only a single * argument" do + -> { 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 + -> { foo 1 }.should raise_error(ArgumentError, 'wrong number of arguments (given 1, expected 2+)') + end +end + +describe "An instance method with a default argument" do + it "evaluates the default when no arguments are passed" do + def foo(a = 1) + a + end + foo.should == 1 + foo(2).should == 2 + end + + it "evaluates the default empty expression when no arguments are passed" do + def foo(a = ()) + a + end + foo.should == nil + foo(2).should == 2 + end + + it "assigns an empty Array to an unused splat argument" do + def foo(a = 1, *b) + [a,b] + end + foo.should == [1, []] + foo(2).should == [2, []] + end + + it "evaluates the default when required arguments precede it" do + def foo(a, b = 2) + [a,b] + end + -> { foo }.should raise_error(ArgumentError, 'wrong number of arguments (given 0, expected 1..2)') + foo(1).should == [1, 2] + end + + it "prefers to assign to a default argument before a splat argument" do + def foo(a, b = 2, *c) + [a,b,c] + end + -> { foo }.should raise_error(ArgumentError, 'wrong number of arguments (given 0, expected 1+)') + foo(1).should == [1,2,[]] + end + + it "prefers to assign to a default argument when there are no required arguments" do + def foo(a = 1, *args) + [a,args] + end + foo(2,2).should == [2,[2]] + end + + it "does not evaluate the default when passed a value and a * argument" do + def foo(a, b = 2, *args) + [a,b,args] + end + foo(2,3,3).should == [2,3,[3]] + end + + ruby_version_is ""..."3.4" do + it "raises a SyntaxError if using the argument in its default value" do + -> { + eval "def foo(bar = bar) + bar + end" + }.should 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 + end + + it "calls a method with the same name as the local when explicitly using ()" do + def bar + 1 + end + def foo(bar = bar()) + bar + end + foo.should == 1 + foo(2).should == 2 + end +end + +describe "A singleton method definition" do + it "can be declared for a local variable" do + a = Object.new + def a.foo + 5 + end + a.foo.should == 5 + end + + it "can be declared for an instance variable" do + @a = Object.new + def @a.foo + 6 + end + @a.foo.should == 6 + end + + it "can be declared for a global variable" do + $__a__ = +"hi" + def $__a__.foo + 7 + end + $__a__.foo.should == 7 + end + + it "can be declared with an empty method body" do + class DefSpec + def self.foo;end + end + DefSpec.foo.should == nil + end + + it "can be redefined" do + obj = Object.new + def obj.==(other) + 1 + end + (obj==1).should == 1 + def obj.==(other) + 2 + end + (obj==2).should == 2 + end + + 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 + 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 + +describe "Redefining a singleton method" do + it "does not inherit a previously set visibility" do + o = Object.new + + class << o; private; def foo; end; end; + + class << o; should have_private_instance_method(:foo); end + + class << o; def foo; end; end; + + class << o; should_not have_private_instance_method(:foo); end + class << o; should have_instance_method(:foo); end + + end +end + +describe "Redefining a singleton method" do + it "does not inherit a previously set visibility" do + o = Object.new + + class << o; private; def foo; end; end; + + class << o; should have_private_instance_method(:foo); end + + class << o; def foo; end; end; + + class << o; should_not have_private_instance_method(:foo); end + class << o; should have_instance_method(:foo); end + + end +end + +describe "A method defined with extreme default arguments" do + it "can redefine itself when the default is evaluated" do + class DefSpecs + def foo(x = (def foo; "hello"; end;1));x;end + end + + d = DefSpecs.new + d.foo(42).should == 42 + d.foo.should == 1 + d.foo.should == 'hello' + end + + it "may use an fcall as a default" do + def bar + 1 + end + def foo(x = bar()) + x + end + foo.should == 1 + foo(2).should == 2 + end + + it "evaluates the defaults in the method's scope" do + def foo(x = ($foo_self = self; nil)); end + foo + $foo_self.should == self + end + + it "may use preceding arguments as defaults" do + def foo(obj, width=obj.length) + width + end + foo('abcde').should == 5 + end + + it "may use a lambda as a default" do + def foo(output = 'a', prc = -> n { output * n }) + prc.call(5) + end + foo.should == 'aaaaa' + end +end + +describe "A singleton method defined with extreme default arguments" do + it "may use a method definition as a default" do + $__a = Object.new + def $__a.foo(x = (def $__a.foo; "hello"; end;1));x;end + + $__a.foo(42).should == 42 + $__a.foo.should == 1 + $__a.foo.should == 'hello' + end + + it "may use an fcall as a default" do + a = Object.new + def a.bar + 1 + end + def a.foo(x = bar()) + x + end + a.foo.should == 1 + a.foo(2).should == 2 + end + + it "evaluates the defaults in the singleton scope" do + a = Object.new + def a.foo(x = ($foo_self = self; nil)); 5 ;end + a.foo + $foo_self.should == a + end + + it "may use preceding arguments as defaults" do + a = Object.new + def a.foo(obj, width=obj.length) + width + end + a.foo('abcde').should == 5 + end + + it "may use a lambda as a default" do + a = Object.new + def a.foo(output = 'a', prc = -> n { output * n }) + prc.call(5) + end + a.foo.should == 'aaaaa' + end +end + +describe "A method definition inside a metaclass scope" do + it "can create a class method" do + class DefSpecSingleton + class << self + def a_class_method;self;end + end + end + + DefSpecSingleton.a_class_method.should == DefSpecSingleton + -> { Object.a_class_method }.should raise_error(NoMethodError) + end + + it "can create a singleton method" do + obj = Object.new + class << obj + def a_singleton_method;self;end + end + + obj.a_singleton_method.should == obj + -> { Object.new.a_singleton_method }.should raise_error(NoMethodError) + end + + it "raises FrozenError if frozen" do + obj = Object.new + obj.freeze + + class << obj + -> { def foo; end }.should raise_error(FrozenError) + end + end +end + +describe "A nested method definition" do + it "creates an instance method when evaluated in an instance method" do + class DefSpecNested + def create_instance_method + def an_instance_method;self;end + an_instance_method + end + end + + obj = DefSpecNested.new + obj.create_instance_method.should == obj + obj.an_instance_method.should == obj + + other = DefSpecNested.new + other.an_instance_method.should == other + + DefSpecNested.should have_instance_method(:an_instance_method) + end + + it "creates a class method when evaluated in a class method" do + class DefSpecNested + class << self + # cleanup + remove_method :a_class_method if method_defined? :a_class_method + def create_class_method + def a_class_method;self;end + a_class_method + end + end + end + + -> { DefSpecNested.a_class_method }.should raise_error(NoMethodError) + DefSpecNested.create_class_method.should == DefSpecNested + DefSpecNested.a_class_method.should == DefSpecNested + -> { Object.a_class_method }.should raise_error(NoMethodError) + -> { DefSpecNested.new.a_class_method }.should raise_error(NoMethodError) + end + + it "creates a singleton method when evaluated in the metaclass of an instance" do + class DefSpecNested + def create_singleton_method + class << self + def a_singleton_method;self;end + end + a_singleton_method + end + end + + obj = DefSpecNested.new + obj.create_singleton_method.should == obj + obj.a_singleton_method.should == obj + + other = DefSpecNested.new + -> { 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 + class DefSpecNested + TARGET = Object.new + def TARGET.defs_method + def inherited_method;self;end + end + end + + DefSpecNested::TARGET.defs_method + DefSpecNested.should have_instance_method :inherited_method + DefSpecNested::TARGET.should_not have_method :inherited_method + + obj = DefSpecNested.new + obj.inherited_method.should == obj + ensure + DefSpecNested.send(:remove_const, :TARGET) + end + + # See http://yugui.jp/articles/846#label-3 + it "inside an instance_eval creates a singleton method" do + class DefSpecNested + OBJ = Object.new + OBJ.instance_eval do + def create_method_in_instance_eval(a = (def arg_method; end)) + def body_method; end + end + end + end + + obj = DefSpecNested::OBJ + obj.create_method_in_instance_eval + + obj.should have_method :arg_method + obj.should have_method :body_method + + DefSpecNested.should_not have_instance_method :arg_method + DefSpecNested.should_not have_instance_method :body_method + ensure + DefSpecNested.send(:remove_const, :OBJ) + end + + it "creates an instance method inside Class.new" do + cls = Class.new do + def do_def + def new_def + 1 + end + end + end + + 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 + +describe "A method definition inside an instance_eval" do + it "creates a singleton method" do + obj = Object.new + obj.instance_eval do + def an_instance_eval_method;self;end + end + obj.an_instance_eval_method.should == obj + + other = Object.new + -> { other.an_instance_eval_method }.should raise_error(NoMethodError) + end + + it "creates a singleton method when evaluated inside a metaclass" do + obj = Object.new + obj.instance_eval do + class << self + def a_metaclass_eval_method;self;end + end + end + obj.a_metaclass_eval_method.should == obj + + other = Object.new + -> { other.a_metaclass_eval_method }.should raise_error(NoMethodError) + end + + it "creates a class method when the receiver is a class" do + DefSpecNested.instance_eval do + def an_instance_eval_class_method;self;end + end + + DefSpecNested.an_instance_eval_class_method.should == DefSpecNested + -> { Object.an_instance_eval_class_method }.should raise_error(NoMethodError) + end + + it "creates a class method when the receiver is an anonymous class" do + m = Class.new + m.instance_eval do + def klass_method + :test + end + end + + m.klass_method.should == :test + -> { Object.klass_method }.should raise_error(NoMethodError) + end + + it "creates a class method when instance_eval is within class" do + m = Class.new do + instance_eval do + def klass_method + :test + end + end + end + + m.klass_method.should == :test + -> { Object.klass_method }.should raise_error(NoMethodError) + end +end + +describe "A method definition inside an instance_exec" do + it "creates a class method when the receiver is a class" do + DefSpecNested.instance_exec(1) do |param| + @stuff = param + + def an_instance_exec_class_method; @stuff; end + end + + DefSpecNested.an_instance_exec_class_method.should == 1 + -> { Object.an_instance_exec_class_method }.should raise_error(NoMethodError) + end + + it "creates a class method when the receiver is an anonymous class" do + m = Class.new + m.instance_exec(1) do |param| + @stuff = param + + def klass_method + @stuff + end + end + + m.klass_method.should == 1 + -> { Object.klass_method }.should raise_error(NoMethodError) + end + + it "creates a class method when instance_exec is within class" do + m = Class.new do + instance_exec(2) do |param| + @stuff = param + + def klass_method + @stuff + end + end + end + + m.klass_method.should == 2 + -> { Object.klass_method }.should raise_error(NoMethodError) + end +end + +describe "A method definition in an eval" do + it "creates an instance method" do + class DefSpecNested + def eval_instance_method + eval "def an_eval_instance_method;self;end", binding + an_eval_instance_method + end + end + + obj = DefSpecNested.new + obj.eval_instance_method.should == obj + obj.an_eval_instance_method.should == obj + + other = DefSpecNested.new + other.an_eval_instance_method.should == other + + -> { Object.new.an_eval_instance_method }.should raise_error(NoMethodError) + end + + it "creates a class method" do + class DefSpecNestedB + class << self + def eval_class_method + eval "def an_eval_class_method;self;end" #, binding + an_eval_class_method + end + end + end + + DefSpecNestedB.eval_class_method.should == DefSpecNestedB + DefSpecNestedB.an_eval_class_method.should == DefSpecNestedB + + -> { Object.an_eval_class_method }.should raise_error(NoMethodError) + -> { DefSpecNestedB.new.an_eval_class_method}.should raise_error(NoMethodError) + end + + it "creates a singleton method" do + class DefSpecNested + def eval_singleton_method + class << self + eval "def an_eval_singleton_method;self;end", binding + end + an_eval_singleton_method + end + end + + obj = DefSpecNested.new + obj.eval_singleton_method.should == obj + obj.an_eval_singleton_method.should == obj + + other = DefSpecNested.new + -> { other.an_eval_singleton_method }.should raise_error(NoMethodError) + end +end + +describe "a method definition that sets more than one default parameter all to the same value" do + def foo(a=b=c={}) + [a,b,c] + end + it "assigns them all the same object by default" do + foo.should == [{},{},{}] + a, b, c = foo + a.should eql(b) + a.should eql(c) + end + + it "allows the first argument to be given, and sets the rest to null" do + foo(1).should == [1,nil,nil] + end + + it "assigns the parameters different objects across different default calls" do + a, _b, _c = foo + d, _e, _f = foo + a.should_not equal(d) + end + + it "only allows overriding the default value of the first such parameter in each set" do + -> { foo(1,2) }.should raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 0..1)') + end + + def bar(a=b=c=1,d=2) + [a,b,c,d] + end + + it "treats the argument after the multi-parameter normally" do + bar.should == [1,1,1,2] + bar(3).should == [3,nil,nil,2] + bar(3,4).should == [3,nil,nil,4] + -> { bar(3,4,5) }.should raise_error(ArgumentError, 'wrong number of arguments (given 3, expected 0..2)') + end +end + +describe "The def keyword" do + describe "within a closure" do + it "looks outside the closure for the visibility" do + module DefSpecsLambdaVisibility + private + + -> { + def some_method; end + }.call + end + + DefSpecsLambdaVisibility.should have_private_instance_method("some_method") + end + end +end diff --git a/spec/ruby/language/defined_spec.rb b/spec/ruby/language/defined_spec.rb new file mode 100644 index 0000000000..80ad1818b1 --- /dev/null +++ b/spec/ruby/language/defined_spec.rb @@ -0,0 +1,1312 @@ +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 + + 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?([NonExistentConstant, Array]) + ret.should == nil + end + + it "returns nil if all elements are not defined" do + ret = defined?([NonExistentConstant, AnotherNonExistentConstant]) + ret.should == nil + end + + end +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 + ret = defined?(puts) + ret.should == "method" + ret.frozen?.should == true + end + + it "returns nil if the method is not defined" do + defined?(defined_specs_undefined_method).should be_nil + end + + it "returns 'method' if the method is defined and private" do + obj = DefinedSpecs::Basic.new + obj.private_method_defined.should == "method" + end + + it "returns 'method' if the predicate method is defined and private" do + obj = DefinedSpecs::Basic.new + obj.private_predicate_defined.should == "method" + end + end + + describe "having a module as receiver" do + it "returns 'method' if the method is defined" do + defined?(Kernel.puts).should == "method" + end + + it "returns nil if the method is private" do + defined?(Object.print).should be_nil + end + + it "returns nil if the method is protected" do + defined?(DefinedSpecs::Basic.new.protected_method).should be_nil + end + + it "returns nil if the method is not defined" do + defined?(Kernel.defined_specs_undefined_method).should be_nil + end + + it "returns nil if the class is not defined" do + defined?(DefinedSpecsUndefined.puts).should be_nil + end + + it "returns nil if the subclass is not defined" do + defined?(DefinedSpecs::Undefined.puts).should be_nil + end + end + + describe "having a local variable as receiver" do + it "returns 'method' if the method is defined" do + obj = DefinedSpecs::Basic.new + 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 + end + + it "returns nil if the variable does not exist" do + defined?(nonexistent_local_variable.some_method).should be_nil + end + + it "calls #respond_to_missing?" do + obj = mock("respond_to_missing object") + obj.should_receive(:respond_to_missing?).and_return(true) + defined?(obj.something_undefined).should == "method" + end + end + + describe "having an instance variable as receiver" do + it "returns 'method' if the method is defined" do + @defined_specs_obj = DefinedSpecs::Basic.new + defined?(@defined_specs_obj.a_defined_method).should == "method" + end + + it "returns nil if the method is not defined" do + @defined_specs_obj = DefinedSpecs::Basic.new + defined?(@defined_specs_obj.an_undefined_method).should be_nil + end + + it "returns nil if the variable does not exist" do + defined?(@nonexistent_instance_variable.some_method).should be_nil + end + end + + describe "having a global variable as receiver" do + it "returns 'method' if the method is defined" do + $defined_specs_obj = DefinedSpecs::Basic.new + defined?($defined_specs_obj.a_defined_method).should == "method" + end + + it "returns nil if the method is not defined" do + $defined_specs_obj = DefinedSpecs::Basic.new + defined?($defined_specs_obj.an_undefined_method).should be_nil + end + + it "returns nil if the variable does not exist" do + defined?($nonexistent_global_variable.some_method).should be_nil + end + end + + describe "having a method call as a receiver" do + it "returns nil if evaluating the receiver raises an exception" do + defined?(DefinedSpecs.exception_method / 2).should be_nil + ScratchPad.recorded.should == :defined_specs_exception + end + + it "returns nil if the method is not defined on the object the receiver returns" do + defined?(DefinedSpecs.side_effects / 2).should be_nil + ScratchPad.recorded.should == :defined_specs_side_effects + end + + it "returns 'method' if the method is defined on the object the receiver returns" do + defined?(DefinedSpecs.fixnum_method / 2).should == "method" + 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 + before :each do + ScratchPad.clear + end + + it "returns 'assignment' for assigning a local variable" do + ret = defined?(x = 2) + ret.should == "assignment" + ret.frozen?.should == true + end + + it "returns 'assignment' for assigning an instance variable" do + defined?(@defined_specs_x = 2).should == "assignment" + end + + it "returns 'assignment' for assigning a global variable" do + defined?($defined_specs_x = 2).should == "assignment" + end + + it "returns 'assignment' for assigning a class variable" 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 + + it "returns 'assignment' for an expression with '%='" do + defined?(x %= 2).should == "assignment" + end + + it "returns 'assignment' for an expression with '/='" do + defined?(x /= 2).should == "assignment" + end + + it "returns 'assignment' for an expression with '-='" do + defined?(x -= 2).should == "assignment" + end + + it "returns 'assignment' for an expression with '+='" do + 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 + defined?(x *= 2).should == "assignment" + end + + it "returns 'assignment' for an expression with '|='" do + defined?(x |= 2).should == "assignment" + end + + it "returns 'assignment' for an expression with '&='" do + defined?(x &= 2).should == "assignment" + end + + it "returns 'assignment' for an expression with '^='" do + defined?(x ^= 2).should == "assignment" + end + + it "returns 'assignment' for an expression with '~='" do + defined?(x = 2).should == "assignment" + end + + it "returns 'assignment' for an expression with '<<='" do + defined?(x <<= 2).should == "assignment" + end + + it "returns 'assignment' for an expression with '>>='" do + defined?(x >>= 2).should == "assignment" + end + + 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 + + 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" + end + + it "returns nil for an expression with == and an undefined method" do + defined?(defined_specs_undefined_method == 2).should be_nil + end + + it "returns nil for an expression with != and an undefined method" do + defined?(defined_specs_undefined_method != 2).should be_nil + end + + it "returns nil for an expression with !~ and an undefined method" do + defined?(defined_specs_undefined_method !~ 2).should be_nil + end + + it "returns 'method' for an expression with '=='" do + x = 42 + defined?(x == 2).should == "method" + end + + it "returns 'method' for an expression with '!='" do + x = 42 + defined?(x != 2).should == "method" + end + + it "returns 'method' for an expression with '!~'" do + x = 42 + defined?(x !~ 2).should == "method" + end + + describe "with logical connectives" do + it "returns nil for an expression with '!' and an undefined method" do + defined?(!defined_specs_undefined_method).should be_nil + end + + it "returns nil for an expression with '!' and an unset class variable" do + @result = eval("class singleton_class::A; defined?(!@@doesnt_exist) end", binding, __FILE__, __LINE__) + @result.should be_nil + end + + it "returns nil for an expression with 'not' and an undefined method" do + defined?(not defined_specs_undefined_method).should be_nil + end + + it "returns nil for an expression with 'not' and an unset class variable" do + @result = eval("class singleton_class::A; defined?(not @@doesnt_exist) end", binding, __FILE__, __LINE__) + @result.should be_nil + end + + it "does not propagate an exception raised by a method in a 'not' expression" do + defined?(not DefinedSpecs.exception_method).should be_nil + ScratchPad.recorded.should == :defined_specs_exception + end + + it "returns 'expression' for an expression with '&&/and' and an unset global variable" do + defined?($defined_specs_undefined_global_variable && true).should == "expression" + defined?(true && $defined_specs_undefined_global_variable).should == "expression" + defined?($defined_specs_undefined_global_variable and true).should == "expression" + end + + it "returns 'expression' for an expression with '&&/and' and an unset instance variable" do + defined?(@defined_specs_undefined_instance_variable && true).should == "expression" + defined?(true && @defined_specs_undefined_instance_variable).should == "expression" + defined?(@defined_specs_undefined_instance_variable and true).should == "expression" + end + + it "returns 'expression' for an expression '&&/and' regardless of its truth value" do + defined?(true && false).should == "expression" + defined?(true and false).should == "expression" + end + + it "returns 'expression' for an expression with '||/or' and an unset global variable" do + defined?($defined_specs_undefined_global_variable || true).should == "expression" + defined?(true || $defined_specs_undefined_global_variable).should == "expression" + defined?($defined_specs_undefined_global_variable or true).should == "expression" + end + + it "returns 'expression' for an expression with '||/or' and an unset instance variable" do + defined?(@defined_specs_undefined_instance_variable || true).should == "expression" + defined?(true || @defined_specs_undefined_instance_variable).should == "expression" + defined?(@defined_specs_undefined_instance_variable or true).should == "expression" + end + + it "returns 'expression' for an expression '||/or' regardless of its truth value" do + defined?(true || false).should == "expression" + defined?(true or false).should == "expression" + end + + it "returns nil for an expression with '!' and an unset global variable" do + defined?(!$defined_specs_undefined_global_variable).should be_nil + end + + it "returns nil for an expression with '!' and an unset instance variable" do + defined?(!@defined_specs_undefined_instance_variable).should be_nil + end + + it "returns 'method' for a 'not' expression with a method" do + defined?(not DefinedSpecs.side_effects).should == "method" + end + + it "calls a method in a 'not' expression and returns 'method'" do + defined?(not DefinedSpecs.side_effects).should == "method" + ScratchPad.recorded.should == :defined_specs_side_effects + end + + it "returns nil for an expression with 'not' and an unset global variable" do + defined?(not $defined_specs_undefined_global_variable).should be_nil + end + + it "returns nil for an expression with 'not' and an unset instance variable" do + defined?(not @defined_specs_undefined_instance_variable).should be_nil + end + + it "returns 'expression' for an expression with '&&/and' and an undefined method" do + defined?(defined_specs_undefined_method && true).should == "expression" + defined?(defined_specs_undefined_method and true).should == "expression" + end + + it "returns 'expression' for an expression with '&&/and' and an unset class variable" do + defined?(@@defined_specs_undefined_class_variable && true).should == "expression" + defined?(@@defined_specs_undefined_class_variable and true).should == "expression" + end + + it "does not call a method in an '&&' expression and returns 'expression'" do + defined?(DefinedSpecs.side_effects && true).should == "expression" + ScratchPad.recorded.should be_nil + end + + it "does not call a method in an 'and' expression and returns 'expression'" do + defined?(DefinedSpecs.side_effects and true).should == "expression" + ScratchPad.recorded.should be_nil + end + + it "returns 'expression' for an expression with '||/or' and an undefined method" do + defined?(defined_specs_undefined_method || true).should == "expression" + defined?(defined_specs_undefined_method or true).should == "expression" + end + + it "returns 'expression' for an expression with '||/or' and an unset class variable" do + defined?(@@defined_specs_undefined_class_variable || true).should == "expression" + defined?(@@defined_specs_undefined_class_variable or true).should == "expression" + end + + it "does not call a method in an '||' expression and returns 'expression'" do + defined?(DefinedSpecs.side_effects || true).should == "expression" + ScratchPad.recorded.should be_nil + end + + it "does not call a method in an 'or' expression and returns 'expression'" do + defined?(DefinedSpecs.side_effects or true).should == "expression" + ScratchPad.recorded.should be_nil + end + end + + it "returns 'expression' when passed a String" do + defined?("garble gooble gable").should == "expression" + end + + describe "with a dynamic String" do + it "returns 'expression' when the String contains a literal" do + defined?("garble #{42}").should == "expression" + end + + it "returns 'expression' when the String contains a call to a defined method" do + defined?("garble #{DefinedSpecs.side_effects}").should == "expression" + end + + it "returns 'expression' when the String contains a call to an undefined method" do + defined?("garble #{DefinedSpecs.undefined_method}").should == "expression" + end + + it "does not call the method in the String" do + defined?("garble #{DefinedSpecs.dynamic_string}").should == "expression" + ScratchPad.recorded.should be_nil + end + end + + describe "with a dynamic Regexp" do + it "returns 'expression' when the Regexp contains a literal" do + defined?(/garble #{42}/).should == "expression" + end + + it "returns 'expression' when the Regexp contains a call to a defined method" do + defined?(/garble #{DefinedSpecs.side_effects}/).should == "expression" + end + + it "returns 'expression' when the Regexp contains a call to an undefined method" do + defined?(/garble #{DefinedSpecs.undefined_method}/).should == "expression" + end + + it "does not call the method in the Regexp" do + defined?(/garble #{DefinedSpecs.dynamic_string}/).should == "expression" + ScratchPad.recorded.should be_nil + end + end + + it "returns 'expression' when passed a Fixnum literal" do + defined?(42).should == "expression" + end + + it "returns 'expression' when passed a Bignum literal" do + defined?(0xdead_beef_deed_feed).should == "expression" + end + + it "returns 'expression' when passed a Float literal" do + defined?(1.5).should == "expression" + end + + it "returns 'expression' when passed a Range literal" do + defined?(0..2).should == "expression" + end + + it "returns 'expression' when passed a Regexp literal" do + defined?(/undefined/).should == "expression" + end + + it "returns 'expression' when passed an Array literal" do + defined?([1, 2]).should == "expression" + end + + it "returns 'expression' when passed a Hash literal" do + defined?({a: :b}).should == "expression" + end + + it "returns 'expression' when passed a Symbol literal" do + defined?(:defined_specs).should == "expression" + end +end + +describe "The defined? keyword for variables" do + it "returns 'local-variable' when called with the name of a local variable" do + 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 + DefinedSpecs::Basic.new.local_variable_defined_nil.should == "local-variable" + end + + it "returns nil for an instance variable that has not been read" do + DefinedSpecs::Basic.new.instance_variable_undefined.should be_nil + end + + it "returns nil for an instance variable that has been read but not assigned to" do + DefinedSpecs::Basic.new.instance_variable_read.should be_nil + end + + it "returns 'instance-variable' for an instance variable that has been assigned" do + 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 + DefinedSpecs::Basic.new.instance_variable_defined_nil.should == "instance-variable" + end + + it "returns nil for a global variable that has not been read" do + DefinedSpecs::Basic.new.global_variable_undefined.should be_nil + end + + it "returns nil for a global variable that has been read but not assigned to" do + DefinedSpecs::Basic.new.global_variable_read.should be_nil + 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 + # 'global-variable' even if no match has been done. + + it "returns 'global-variable' for $!" do + defined?($!).should == "global-variable" + end + + it "returns 'global-variable for $~" do + defined?($~).should == "global-variable" + end + + describe "when a String does not match a Regexp" do + before :each do + "mis-matched" =~ /z(z)z/ + end + + it "returns 'global-variable' for $~" do + defined?($~).should == "global-variable" + end + + it "returns nil for $&" do + defined?($&).should be_nil + end + + it "returns nil for $`" do + defined?($`).should be_nil + end + + it "returns nil for $'" do + defined?($').should be_nil + end + + it "returns nil for $+" do + defined?($+).should be_nil + end + + it "returns nil for any last match global" do + defined?($1).should be_nil + defined?($4).should be_nil + defined?($7).should be_nil + defined?($10).should be_nil + defined?($200).should be_nil + end + end + + describe "when a String matches a Regexp" do + before :each do + "mis-matched" =~ /s(-)m(.)/ + end + + it "returns 'global-variable' for $~" do + defined?($~).should == "global-variable" + end + + it "returns 'global-variable' for $&" do + defined?($&).should == "global-variable" + end + + it "returns 'global-variable' for $`" do + defined?($`).should == "global-variable" + end + + it "returns 'global-variable' for $'" do + defined?($').should == "global-variable" + end + + it "returns 'global-variable' for $+" do + defined?($+).should == "global-variable" + end + + it "returns 'global-variable' for the capture references" do + defined?($1).should == "global-variable" + defined?($2).should == "global-variable" + end + + it "returns nil for non-captures" do + defined?($4).should be_nil + defined?($7).should be_nil + defined?($10).should be_nil + defined?($200).should be_nil + end + end + + describe "when a Regexp does not match a String" do + before :each do + /z(z)z/ =~ "mis-matched" + end + + it "returns 'global-variable' for $~" do + defined?($~).should == "global-variable" + end + + it "returns nil for $&" do + defined?($&).should be_nil + end + + it "returns nil for $`" do + defined?($`).should be_nil + end + + it "returns nil for $'" do + defined?($').should be_nil + end + + it "returns nil for $+" do + defined?($+).should be_nil + end + + it "returns nil for any last match global" do + defined?($1).should be_nil + defined?($4).should be_nil + defined?($7).should be_nil + defined?($10).should be_nil + defined?($200).should be_nil + end + end + + describe "when a Regexp matches a String" do + before :each do + /s(-)m(.)/ =~ "mis-matched" + end + + it "returns 'global-variable' for $~" do + defined?($~).should == "global-variable" + end + + it "returns 'global-variable' for $&" do + defined?($&).should == "global-variable" + end + + it "returns 'global-variable' for $`" do + defined?($`).should == "global-variable" + end + + it "returns 'global-variable' for $'" do + defined?($').should == "global-variable" + end + + it "returns 'global-variable' for $+" do + defined?($+).should == "global-variable" + end + + it "returns 'global-variable' for the capture references" do + defined?($1).should == "global-variable" + defined?($2).should == "global-variable" + end + + it "returns nil for non-captures" do + defined?($4).should be_nil + defined?($7).should be_nil + defined?($10).should be_nil + defined?($200).should be_nil + end + end + it "returns 'global-variable' for a global variable that has been assigned" do + DefinedSpecs::Basic.new.global_variable_defined.should == "global-variable" + end + + it "returns nil for a class variable that has not been read" do + DefinedSpecs::Basic.new.class_variable_undefined.should be_nil + end + + # There is no spec for a class variable that is read before being assigned + # to because setting up the code for this raises a NameError before you + # 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 + 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 + block = Proc.new { |xxx| defined?(xxx) } + block.call(1).should == "local-variable" + end +end + +describe "The defined? keyword for a simple constant" do + it "returns 'constant' when the constant is defined" do + ret = defined?(DefinedSpecs) + ret.should == "constant" + ret.frozen?.should == true + end + + it "returns nil when the constant is not defined" do + defined?(DefinedSpecsUndefined).should be_nil + end + + it "does not call Object.const_missing if the constant is not defined" do + Object.should_not_receive(:const_missing) + defined?(DefinedSpecsUndefined).should be_nil + end + + it "returns 'constant' for an included module" do + DefinedSpecs::Child.module_defined.should == "constant" + end + + it "returns 'constant' for a constant defined in an included module" do + DefinedSpecs::Child.module_constant_defined.should == "constant" + end +end + +describe "The defined? keyword for a top-level constant" do + it "returns 'constant' when passed the name of a top-level constant" do + defined?(::DefinedSpecs).should == "constant" + end + + it "returns nil if the constant is not defined" do + defined?(::DefinedSpecsUndefined).should be_nil + end + + it "does not call Object.const_missing if the constant is not defined" do + Object.should_not_receive(:const_missing) + defined?(::DefinedSpecsUndefined).should be_nil + end +end + +describe "The defined? keyword for a scoped constant" do + it "returns 'constant' when the scoped constant is defined" do + defined?(DefinedSpecs::Basic).should == "constant" + end + + it "returns nil when the scoped constant is not defined" 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 + end + + it "returns nil when an undefined constant is scoped to a defined constant" do + defined?(DefinedSpecs::Child::Undefined).should be_nil + end + + it "returns nil when a constant is scoped to an undefined constant" do + Object.should_not_receive(:const_missing) + defined?(Undefined::Object).should be_nil + end + + it "returns nil when the undefined constant is scoped to an undefined constant" do + defined?(DefinedSpecs::Undefined::Undefined).should be_nil + end + + it "returns nil when a constant is defined on top-level but not on the module" do + defined?(DefinedSpecs::String).should be_nil + 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 + defined?(DefinedSpecs::Child::A).should == "constant" + end +end + +describe "The defined? keyword for a top-level scoped constant" do + it "returns 'constant' when the scoped constant is defined" do + defined?(::DefinedSpecs::Basic).should == "constant" + end + + it "returns nil when the scoped constant is not defined" do + defined?(::DefinedSpecs::Undefined).should be_nil + end + + it "returns nil when an undefined constant is scoped to a defined constant" do + defined?(::DefinedSpecs::Child::Undefined).should be_nil + end + + it "returns nil when the undefined constant is scoped to an undefined constant" do + defined?(::DefinedSpecs::Undefined::Undefined).should be_nil + end + + it "returns 'constant' if the scoped-scoped constant is defined" do + defined?(::DefinedSpecs::Child::A).should == "constant" + end +end + +describe "The defined? keyword for a self-send method call scoped constant" do + it "returns nil if the constant is not defined in the scope of the method's value" do + defined?(defined_specs_method::Undefined).should be_nil + end + + it "returns 'constant' if the constant is defined in the scope of the method's value" do + defined?(defined_specs_method::Basic).should == "constant" + end + + it "returns nil if the last constant is not defined in the scope chain" do + defined?(defined_specs_method::Basic::Undefined).should be_nil + end + + it "returns nil if the middle constant is not defined in the scope chain" do + defined?(defined_specs_method::Undefined::Undefined).should be_nil + end + + it "returns 'constant' if all the constants in the scope chain are defined" do + defined?(defined_specs_method::Basic::A).should == "constant" + end +end + +describe "The defined? keyword for a receiver method call scoped constant" do + it "returns nil if the constant is not defined in the scope of the method's value" do + defined?(defined_specs_receiver.defined_method::Undefined).should be_nil + end + + it "returns 'constant' if the constant is defined in the scope of the method's value" do + defined?(defined_specs_receiver.defined_method::Basic).should == "constant" + end + + it "returns nil if the last constant is not defined in the scope chain" do + defined?(defined_specs_receiver.defined_method::Basic::Undefined).should be_nil + end + + it "returns nil if the middle constant is not defined in the scope chain" do + defined?(defined_specs_receiver.defined_method::Undefined::Undefined).should be_nil + end + + it "returns 'constant' if all the constants in the scope chain are defined" do + defined?(defined_specs_receiver.defined_method::Basic::A).should == "constant" + end +end + +describe "The defined? keyword for a module method call scoped constant" do + it "returns nil if the constant is not defined in the scope of the method's value" do + defined?(DefinedSpecs.defined_method::Undefined).should be_nil + end + + it "returns 'constant' if the constant scoped by the method's value is defined" do + defined?(DefinedSpecs.defined_method::Basic).should == "constant" + end + + it "returns nil if the last constant in the scope chain is not defined" do + defined?(DefinedSpecs.defined_method::Basic::Undefined).should be_nil + end + + it "returns nil if the middle constant in the scope chain is not defined" do + defined?(DefinedSpecs.defined_method::Undefined::Undefined).should be_nil + end + + it "returns 'constant' if all the constants in the scope chain are defined" do + defined?(DefinedSpecs.defined_method::Basic::A).should == "constant" + end + + it "returns nil if the outer scope constant in the receiver is not defined" do + defined?(Undefined::DefinedSpecs.defined_method::Basic).should be_nil + end + + it "returns nil if the scoped constant in the receiver is not defined" do + defined?(DefinedSpecs::Undefined.defined_method::Basic).should be_nil + end + + it "returns 'constant' if all the constants in the receiver are defined" do + defined?(Object::DefinedSpecs.defined_method::Basic).should == "constant" + end + + it "returns 'constant' if all the constants in the receiver and scope chain are defined" do + defined?(Object::DefinedSpecs.defined_method::Basic::A).should == "constant" + end +end + +describe "The defined? keyword for a variable scoped constant" do + after :all do + if Object.class_variable_defined? :@@defined_specs_obj + Object.__send__(:remove_class_variable, :@@defined_specs_obj) + end + end + + it "returns nil if the instance scoped constant is not defined" do + @defined_specs_obj = DefinedSpecs::Basic + defined?(@defined_specs_obj::Undefined).should be_nil + end + + it "returns 'constant' if the constant is defined in the scope of the instance variable" do + @defined_specs_obj = DefinedSpecs::Basic + defined?(@defined_specs_obj::A).should == "constant" + end + + it "returns nil if the global scoped constant is not defined" do + $defined_specs_obj = DefinedSpecs::Basic + defined?($defined_specs_obj::Undefined).should be_nil + end + + it "returns 'constant' if the constant is defined in the scope of the global variable" do + $defined_specs_obj = DefinedSpecs::Basic + defined?($defined_specs_obj::A).should == "constant" + end + + it "returns nil if the class scoped constant is not defined" do + 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 + 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 + defined_specs_obj = DefinedSpecs::Basic + defined?(defined_specs_obj::Undefined).should be_nil + end + + it "returns 'constant' if the constant is defined in the scope of the local variable" do + defined_specs_obj = DefinedSpecs::Basic + defined?(defined_specs_obj::A).should == "constant" + end +end + +describe "The defined? keyword for a self:: scoped constant" do + it "returns 'constant' for a constant explicitly scoped to self:: when set" do + defined?(DefinedSpecs::SelfScoped).should == "constant" + end + + it "returns 'constant' for a constant explicitly scoped to self:: in subclass's metaclass" do + DefinedSpecs::Child.parent_constant_defined.should == "constant" + end +end + +describe "The defined? keyword for yield" do + it "returns nil if no block is passed to a method not taking a block parameter" do + DefinedSpecs::Basic.new.no_yield_block.should be_nil + end + + it "returns nil if no block is passed to a method taking a block parameter" do + DefinedSpecs::Basic.new.no_yield_block_parameter.should be_nil + end + + it "returns 'yield' if a block is passed to a method not taking a block parameter" do + ret = DefinedSpecs::Basic.new.yield_block + ret.should == "yield" + ret.frozen?.should == true + end + + it "returns 'yield' if a block is passed to a method taking a block parameter" do + DefinedSpecs::Basic.new.yield_block_parameter.should == "yield" + end + + it "returns 'yield' when called within a block" do + def yielder + yield + end + + def call_defined + yielder { defined?(yield) } + end + + call_defined() { }.should == "yield" + end +end + +describe "The defined? keyword for super" do + it "returns nil when a superclass undef's the method" do + DefinedSpecs::ClassWithoutMethod.new.test.should be_nil + end + + describe "for a method taking no arguments" do + it "returns nil when no superclass method exists" do + DefinedSpecs::Super.new.no_super_method_no_args.should be_nil + end + + it "returns nil from a block when no superclass method exists" do + DefinedSpecs::Super.new.no_super_method_block_no_args.should be_nil + end + + it "returns nil from a #define_method when no superclass method exists" do + DefinedSpecs::Super.new.no_super_define_method_no_args.should be_nil + end + + it "returns nil from a block in a #define_method when no superclass method exists" do + DefinedSpecs::Super.new.no_super_define_method_block_no_args.should be_nil + end + + it "returns 'super' when a superclass method exists" do + 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 + DefinedSpecs::Super.new.method_block_no_args.should == "super" + end + + it "returns 'super' from a #define_method when a superclass method exists" do + DefinedSpecs::Super.new.define_method_no_args.should == "super" + end + + it "returns 'super' from a block in a #define_method when a superclass method exists" do + DefinedSpecs::Super.new.define_method_block_no_args.should == "super" + end + + it "returns 'super' when the method exists in a supermodule" do + DefinedSpecs::SuperWithIntermediateModules.new.method_no_args.should == "super" + end + end + + describe "for a method taking arguments" do + it "returns nil when no superclass method exists" do + DefinedSpecs::Super.new.no_super_method_args.should be_nil + end + + it "returns nil from a block when no superclass method exists" do + DefinedSpecs::Super.new.no_super_method_block_args.should be_nil + end + + it "returns nil from a #define_method when no superclass method exists" do + DefinedSpecs::Super.new.no_super_define_method_args.should be_nil + end + + it "returns nil from a block in a #define_method when no superclass method exists" do + DefinedSpecs::Super.new.no_super_define_method_block_args.should be_nil + end + + it "returns 'super' when a superclass method exists" do + DefinedSpecs::Super.new.method_args.should == "super" + end + + it "returns 'super' from a block when a superclass method exists" do + DefinedSpecs::Super.new.method_block_args.should == "super" + end + + it "returns 'super' from a #define_method when a superclass method exists" do + DefinedSpecs::Super.new.define_method_args.should == "super" + end + + it "returns 'super' from a block in a #define_method when a superclass method exists" do + DefinedSpecs::Super.new.define_method_block_args.should == "super" + end + end + + describe "within an included module's method" do + it "returns 'super' when a superclass method exists in the including hierarchy" do + DefinedSpecs::Child.new.defined_super.should == "super" + end + end +end + + +describe "The defined? keyword for instance variables" do + it "returns 'instance-variable' if assigned" do + @assigned_ivar = "some value" + defined?(@assigned_ivar).should == "instance-variable" + end + + it "returns nil if not assigned" do + defined?(@unassigned_ivar).should be_nil + end +end + +describe "The defined? keyword for pseudo-variables" do + it "returns 'expression' for __FILE__" do + defined?(__FILE__).should == "expression" + end + + it "returns 'expression' for __LINE__" do + defined?(__LINE__).should == "expression" + end + + it "returns 'expression' for __ENCODING__" do + defined?(__ENCODING__).should == "expression" + end +end + +describe "The defined? keyword for conditional expressions" do + it "returns 'expression' for an 'if' conditional" do + defined?(if x then 'x' else '' end).should == "expression" + end + + it "returns 'expression' for an 'unless' conditional" do + defined?(unless x then '' else 'x' end).should == "expression" + end + + it "returns 'expression' for ternary expressions" do + defined?(x ? 'x' : '').should == "expression" + end +end + +describe "The defined? keyword for case expressions" do + it "returns 'expression'" do + defined?(case x; when 'x'; 'y' end).should == "expression" + end +end + +describe "The defined? keyword for loop expressions" do + it "returns 'expression' for a 'for' expression" do + defined?(for n in 1..3 do true end).should == "expression" + end + + it "returns 'expression' for a 'while' expression" do + defined?(while x do y end).should == "expression" + end + + it "returns 'expression' for an 'until' expression" do + defined?(until x do y end).should == "expression" + end + + it "returns 'expression' for a 'break' expression" do + defined?(break).should == "expression" + end + + it "returns 'expression' for a 'next' expression" do + defined?(next).should == "expression" + end + + it "returns 'expression' for a 'redo' expression" do + defined?(redo).should == "expression" + end + + it "returns 'expression' for a 'retry' expression" do + defined?(retry).should == "expression" + end +end + +describe "The defined? keyword for return expressions" do + it "returns 'expression'" do + defined?(return).should == "expression" + end +end + +describe "The defined? keyword for exception expressions" do + it "returns 'expression'" do + defined?(begin 1 end).should == "expression" + end +end 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 new file mode 100644 index 0000000000..e761a53cb6 --- /dev/null +++ b/spec/ruby/language/encoding_spec.rb @@ -0,0 +1,36 @@ +# -*- encoding: us-ascii -*- +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 + __ENCODING__.should be_kind_of(Encoding) + end + + it "is US-ASCII by default" do + __ENCODING__.should == Encoding::US_ASCII + end + + it "is the evaluated strings's one inside an eval" do + eval("__ENCODING__".dup.force_encoding("US-ASCII")).should == Encoding::US_ASCII + eval("__ENCODING__".dup.force_encoding("BINARY")).should == Encoding::BINARY + end + + it "is the encoding specified by a magic comment inside an eval" do + code = "# encoding: BINARY\n__ENCODING__".dup.force_encoding("US-ASCII") + eval(code).should == Encoding::BINARY + + code = "# encoding: us-ascii\n__ENCODING__".dup.force_encoding("BINARY") + eval(code).should == Encoding::US_ASCII + end + + it "is the encoding specified by a magic comment in the file" do + CodingUS_ASCII.encoding.should == Encoding::US_ASCII + CodingUTF_8.encoding.should == Encoding::UTF_8 + end + + it "raises a SyntaxError if assigned to" do + -> { 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 new file mode 100644 index 0000000000..b76292c007 --- /dev/null +++ b/spec/ruby/language/ensure_spec.rb @@ -0,0 +1,346 @@ +require_relative '../spec_helper' +require_relative 'fixtures/ensure' + +describe "An ensure block inside a begin block" do + before :each do + ScratchPad.record [] + end + + it "is executed when an exception is raised in it's corresponding begin block" do + -> { + begin + ScratchPad << :begin + raise EnsureSpec::Error + ensure + ScratchPad << :ensure + end + }.should raise_error(EnsureSpec::Error) + + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "is executed when an exception is raised and rescued in it's corresponding begin block" do + begin + ScratchPad << :begin + raise "An exception occurred!" + rescue + ScratchPad << :rescue + ensure + ScratchPad << :ensure + end + + ScratchPad.recorded.should == [:begin, :rescue, :ensure] + end + + it "is executed even when a symbol is thrown in it's corresponding begin block" do + catch(:symbol) do + begin + ScratchPad << :begin + throw(:symbol) + rescue + ScratchPad << :rescue + ensure + ScratchPad << :ensure + end + end + + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "is executed when nothing is raised or thrown in it's corresponding begin block" do + begin + ScratchPad << :begin + rescue + ScratchPad << :rescue + ensure + ScratchPad << :ensure + end + + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "has no return value" do + begin + :begin + ensure + :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 + it "in no-exception scenarios, is the value of the last statement of the protected body" do + begin + v = 1 + eval('x=1') # to prevent opts from triggering + v + ensure + v = 2 + end.should == 1 + end + + it "when an exception is rescued, is the value of the rescuing block" do + begin + raise 'foo' + rescue + v = 3 + ensure + v = 2 + end.should == 3 + end +end + +describe "An ensure block inside a method" do + before :each do + @obj = EnsureSpec::Container.new + end + + it "is executed when an exception is raised in the method" do + -> { @obj.raise_in_method_with_ensure }.should raise_error(EnsureSpec::Error) + @obj.executed.should == [:method, :ensure] + end + + it "is executed when an exception is raised and rescued in the method" do + @obj.raise_and_rescue_in_method_with_ensure + @obj.executed.should == [:method, :rescue, :ensure] + end + + it "is executed even when a symbol is thrown in the method" do + catch(:symbol) { @obj.throw_in_method_with_ensure } + @obj.executed.should == [:method, :ensure] + end + + it "has no impact on the method's implicit return value" do + @obj.implicit_return_in_method_with_ensure.should == :method + end + + 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 + before :each do + ScratchPad.record [] + end + + it "is executed when an exception is raised" do + -> { + eval <<-ruby + class EnsureInClassExample + ScratchPad << :class + raise EnsureSpec::Error + ensure + ScratchPad << :ensure + end + ruby + }.should raise_error(EnsureSpec::Error) + + ScratchPad.recorded.should == [:class, :ensure] + end + + it "is executed when an exception is raised and rescued" do + eval <<-ruby + class EnsureInClassExample + ScratchPad << :class + raise + rescue + ScratchPad << :rescue + ensure + ScratchPad << :ensure + end + ruby + + ScratchPad.recorded.should == [:class, :rescue, :ensure] + end + + it "is executed even when a symbol is thrown" do + catch(:symbol) do + eval <<-ruby + class EnsureInClassExample + ScratchPad << :class + throw(:symbol) + rescue + ScratchPad << :rescue + ensure + ScratchPad << :ensure + end + ruby + end + + ScratchPad.recorded.should == [:class, :ensure] + end + + it "is executed when nothing is raised or thrown" do + eval <<-ruby + class EnsureInClassExample + ScratchPad << :class + rescue + ScratchPad << :rescue + ensure + ScratchPad << :ensure + end + ruby + + ScratchPad.recorded.should == [:class, :ensure] + end + + it "has no return value" do + result = eval <<-ruby + class EnsureInClassExample + :class + ensure + :ensure + end + ruby + + result.should == :class + end +end + +describe "An ensure block inside {} block" do + it "is not allowed" do + -> { + eval <<-ruby + lambda { + raise + ensure + } + ruby + }.should raise_error(SyntaxError) + end +end + +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 + -> { + 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 + + 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, :rescue, :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 + end + + 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 + return caller(0, 2) # rubocop:disable Lint/EnsureReturn + end + end + line = __LINE__ + foo[0].should =~ /#{__FILE__}:#{line-3}:in 'foo'/ + foo[1].should =~ /#{__FILE__}:#{line+2}:in 'block/ + end + end +end diff --git a/spec/ruby/language/execution_spec.rb b/spec/ruby/language/execution_spec.rb new file mode 100644 index 0000000000..51bcde62e8 --- /dev/null +++ b/spec/ruby/language/execution_spec.rb @@ -0,0 +1,93 @@ +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 + it "is the same as ``" 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 new file mode 100644 index 0000000000..59563d9642 --- /dev/null +++ b/spec/ruby/language/file_spec.rb @@ -0,0 +1,29 @@ +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 + -> { eval("__FILE__ = 1") }.should raise_error(SyntaxError) + end + + ruby_version_is ""..."3.3" do + it "equals (eval) inside an eval" do + eval("__FILE__").should == "(eval)" + end + end + + 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 with require" do + it_behaves_like :language___FILE__, :require, Kernel +end + +describe "The __FILE__ pseudo-variable with load" do + it_behaves_like :language___FILE__, :load, Kernel +end diff --git a/spec/ruby/language/fixtures/argv_encoding.rb b/spec/ruby/language/fixtures/argv_encoding.rb new file mode 100644 index 0000000000..8192b2d9a0 --- /dev/null +++ b/spec/ruby/language/fixtures/argv_encoding.rb @@ -0,0 +1 @@ +p ARGV.map { |a| a.encoding.name } diff --git a/spec/ruby/language/fixtures/array.rb b/spec/ruby/language/fixtures/array.rb new file mode 100644 index 0000000000..c1036575ff --- /dev/null +++ b/spec/ruby/language/fixtures/array.rb @@ -0,0 +1,32 @@ +module ArraySpec + class Splat + def unpack_3args(a, b, c) + [a, b, c] + end + + def unpack_4args(a, b, c, d) + [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/begin_file.rb b/spec/ruby/language/fixtures/begin_file.rb new file mode 100644 index 0000000000..73cae61a08 --- /dev/null +++ b/spec/ruby/language/fixtures/begin_file.rb @@ -0,0 +1,3 @@ +BEGIN { + puts __FILE__ +} diff --git a/spec/ruby/language/fixtures/binary_symbol.rb b/spec/ruby/language/fixtures/binary_symbol.rb new file mode 100644 index 0000000000..2ddf565820 --- /dev/null +++ b/spec/ruby/language/fixtures/binary_symbol.rb @@ -0,0 +1,4 @@ +# encoding: binary + +p :il_était.to_s.bytes +puts :il_était.encoding.name diff --git a/spec/ruby/language/fixtures/block.rb b/spec/ruby/language/fixtures/block.rb new file mode 100644 index 0000000000..33baac6aeb --- /dev/null +++ b/spec/ruby/language/fixtures/block.rb @@ -0,0 +1,61 @@ +module BlockSpecs + class Yielder + def z + yield + end + + def m(*a) + yield(*a) + end + + def s(a) + yield(a) + end + + 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 + + class Yield + def splat(*args) + yield(*args) + end + + def two_args + yield 1, 2 + end + + def two_arg_array + yield [1, 2] + end + + def yield_splat_inside_block + [1, 2].send(:each_with_index) {|*args| yield(*args)} + end + + def yield_this(obj) + yield obj + end + end + + class OverwriteBlockVariable + def initialize + @y = Yielder.new + end + + def method_missing(method, *args, &block) + self.class.send :define_method, method do |*a, &b| + @y.send method, *a, &b + end + + send method, *args, &block + end + end +end diff --git a/spec/ruby/language/fixtures/break.rb b/spec/ruby/language/fixtures/break.rb new file mode 100644 index 0000000000..217c20a2c0 --- /dev/null +++ b/spec/ruby/language/fixtures/break.rb @@ -0,0 +1,291 @@ +module BreakSpecs + class Driver + def initialize(ensures=false) + @ensures = ensures + end + + def note(value) + ScratchPad << value + end + end + + class Block < Driver + def break_nil + note :a + note yielding { + note :b + break + note :c + } + note :d + end + + def break_value + note :a + note yielding { + note :b + break :break + note :c + } + note :d + end + + def yielding + note :aa + note yield + note :bb + end + + def create_block + note :za + b = capture_block do + note :zb + break :break + note :zc + end + note :zd + b + end + + def capture_block(&b) + note :xa + b + end + + def break_in_method_captured + note :a + create_block.call + note :b + end + + def break_in_yield_captured + note :a + yielding(&create_block) + note :b + end + + def break_in_method + note :a + b = capture_block { + note :b + break :break + note :c + } + note :d + note b.call + note :e + end + + def call_method(b) + note :aa + note b.call + note :bb + end + + def break_in_nested_method + note :a + b = capture_block { + note :b + break :break + note :c + } + note :cc + note call_method(b) + note :d + end + + def break_in_yielding_method + note :a + b = capture_block { + note :b + break :break + note :c + } + note :cc + note yielding(&b) + note :d + end + + def looped_break_in_captured_block + note :begin + looped_delegate_block do |i| + note :prebreak + break if i == 1 + note :postbreak + end + note :end + end + + def looped_delegate_block(&block) + note :preloop + 2.times do |i| + note :predele + yield_value(i, &block) + note :postdele + end + note :postloop + end + private :looped_delegate_block + + def yield_value(value) + note :preyield + yield value + note :postyield + end + private :yield_value + + def method(v) + yield v + end + + def invoke_yield_in_while + looping = true + while looping + note :aa + yield + note :bb + looping = false + end + note :should_not_reach_here + end + + def break_in_block_in_while + invoke_yield_in_while do + note :break + break :value + note :c + end + end + end + + class Lambda < Driver + # Cases for the invocation of the scope defining the lambda still active + # on the call stack when the lambda is invoked. + def break_in_defining_scope(value=true) + note :a + note -> { + note :b + if value + break :break + else + break + end + note :c + }.call + note :d + end + + def break_in_nested_scope + note :a + l = -> do + note :b + break :break + note :c + end + note :d + + invoke_lambda l + + note :e + end + + def invoke_lambda(l) + note :aa + note l.call + note :bb + end + + def break_in_nested_scope_yield + note :a + l = -> do + note :b + break :break + note :c + end + note :d + + invoke_yield(&l) + + note :e + end + + def note_invoke_yield + note :aa + note yield + note :bb + end + + def break_in_nested_scope_block + note :a + l = -> do + note :b + break :break + note :c + end + note :d + + invoke_lambda_block l + + note :e + end + + def invoke_yield + note :aaa + yield + note :bbb + end + + def invoke_lambda_block(b) + note :aa + invoke_yield do + note :bb + + note b.call + + note :cc + end + note :dd + end + + # Cases for the invocation of the scope defining the lambda NOT still + # active on the call stack when the lambda is invoked. + def create_lambda + note :la + l = -> do + note :lb + break :break + note :lc + end + note :ld + l + end + + def break_in_method + note :a + + note create_lambda.call + + note :b + end + + def break_in_block_in_method + note :a + invoke_yield do + note :b + + note create_lambda.call + + note :c + end + note :d + end + + def break_in_method_yield + note :a + + invoke_yield(&create_lambda) + + note :b + end + end +end diff --git a/spec/ruby/language/fixtures/break_lambda_toplevel.rb b/spec/ruby/language/fixtures/break_lambda_toplevel.rb new file mode 100644 index 0000000000..da5abbaf00 --- /dev/null +++ b/spec/ruby/language/fixtures/break_lambda_toplevel.rb @@ -0,0 +1,9 @@ +print "a," + +print -> { + print "b," + break "break," + print "c," +}.call + +puts "d" diff --git a/spec/ruby/language/fixtures/break_lambda_toplevel_block.rb b/spec/ruby/language/fixtures/break_lambda_toplevel_block.rb new file mode 100644 index 0000000000..3dcee62424 --- /dev/null +++ b/spec/ruby/language/fixtures/break_lambda_toplevel_block.rb @@ -0,0 +1,23 @@ +print "a," + +l = -> { + print "b," + break "break," + print "c," +} + +def a(l) + print "d," + b { l.call } + print "e," +end + +def b + print "f," + print yield + print "g," +end + +a(l) + +puts "h" diff --git a/spec/ruby/language/fixtures/break_lambda_toplevel_method.rb b/spec/ruby/language/fixtures/break_lambda_toplevel_method.rb new file mode 100644 index 0000000000..a5936a3d70 --- /dev/null +++ b/spec/ruby/language/fixtures/break_lambda_toplevel_method.rb @@ -0,0 +1,17 @@ +print "a," + +l = -> { + print "b," + break "break," + print "c," +} + +def a(l) + print "d," + print l.call + print "e," +end + +a(l) + +puts "f" 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/classes.rb b/spec/ruby/language/fixtures/classes.rb new file mode 100644 index 0000000000..eb239e1e29 --- /dev/null +++ b/spec/ruby/language/fixtures/classes.rb @@ -0,0 +1,31 @@ +module LanguageSpecs + # Regexp support + + def self.paired_delimiters + [%w[( )], %w[{ }], %w[< >], ["[", "]"]] + end + + def self.non_paired_delimiters + %w[~ ! # $ % ^ & * _ + ` - = " ' , . ? / | \\] + end + + def self.blanks + " \t" + end + + def self.white_spaces + return blanks + "\f\n\r\v" + end + + def self.non_alphanum_non_space + '~!@#$%^&*()+-\|{}[]:";\'<>?,./' + end + + def self.punctuations + ",.?" # TODO - Need to fill in the full list + end + + def self.get_regexp_with_substitution o + /#{o}/o + end +end diff --git a/spec/ruby/language/fixtures/coding_us_ascii.rb b/spec/ruby/language/fixtures/coding_us_ascii.rb new file mode 100644 index 0000000000..7df66109d7 --- /dev/null +++ b/spec/ruby/language/fixtures/coding_us_ascii.rb @@ -0,0 +1,11 @@ +# encoding: us-ascii + +module CodingUS_ASCII + def self.encoding + __ENCODING__ + end + + def self.string_literal + "string literal" + end +end diff --git a/spec/ruby/language/fixtures/coding_utf_8.rb b/spec/ruby/language/fixtures/coding_utf_8.rb new file mode 100644 index 0000000000..3d8e1d9a34 --- /dev/null +++ b/spec/ruby/language/fixtures/coding_utf_8.rb @@ -0,0 +1,11 @@ +# encoding: utf-8 + +module CodingUTF_8 + def self.encoding + __ENCODING__ + end + + def self.string_literal + "string literal" + end +end diff --git a/spec/ruby/language/fixtures/constant_visibility.rb b/spec/ruby/language/fixtures/constant_visibility.rb new file mode 100644 index 0000000000..af38b2d8f2 --- /dev/null +++ b/spec/ruby/language/fixtures/constant_visibility.rb @@ -0,0 +1,114 @@ +module ConstantVisibility + module ModuleContainer + module PrivateModule + end + private_constant :PrivateModule + + class PrivateClass + end + private_constant :PrivateClass + end + + class ClassContainer + module PrivateModule + end + private_constant :PrivateModule + + class PrivateClass + end + private_constant :PrivateClass + end + + module PrivConstModule + PRIVATE_CONSTANT_MODULE = true + private_constant :PRIVATE_CONSTANT_MODULE + + def self.private_constant_from_self + PRIVATE_CONSTANT_MODULE + end + + def self.defined_from_self + defined? PRIVATE_CONSTANT_MODULE + end + + module Nested + def self.private_constant_from_scope + PRIVATE_CONSTANT_MODULE + end + + def self.defined_from_scope + defined? PRIVATE_CONSTANT_MODULE + end + end + end + + class PrivConstClass + PRIVATE_CONSTANT_CLASS = true + private_constant :PRIVATE_CONSTANT_CLASS + + def self.private_constant_from_self + PRIVATE_CONSTANT_CLASS + end + + def self.defined_from_self + defined? PRIVATE_CONSTANT_CLASS + end + + module Nested + def self.private_constant_from_scope + PRIVATE_CONSTANT_CLASS + end + + def self.defined_from_scope + defined? PRIVATE_CONSTANT_CLASS + end + end + end + + class ClassIncludingPrivConstModule + include PrivConstModule + + def private_constant_from_include + PRIVATE_CONSTANT_MODULE + end + + def defined_from_include + defined? PRIVATE_CONSTANT_MODULE + 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 + end + + def defined_from_subclass + defined? PRIVATE_CONSTANT_CLASS + end + end + + def self.reset_private_constants + Object.send :private_constant, :PRIVATE_CONSTANT_IN_OBJECT + end +end + +class Object + PRIVATE_CONSTANT_IN_OBJECT = true + private_constant :PRIVATE_CONSTANT_IN_OBJECT +end diff --git a/spec/ruby/language/fixtures/constants_sclass.rb b/spec/ruby/language/fixtures/constants_sclass.rb new file mode 100644 index 0000000000..21dc4081e2 --- /dev/null +++ b/spec/ruby/language/fixtures/constants_sclass.rb @@ -0,0 +1,54 @@ +module ConstantSpecs + + CS_SINGLETON1 = Object.new + class << CS_SINGLETON1 + CONST = 1 + def foo + CONST + end + end + + CS_SINGLETON2 = [Object.new, Object.new] + 2.times do |i| + obj = CS_SINGLETON2[i] + $spec_i = i + class << obj + CONST = ($spec_i + 1) + def foo + CONST + end + end + end + + CS_SINGLETON3 = [Object.new, Object.new] + 2.times do |i| + obj = CS_SINGLETON3[i] + class << obj + class X + # creates <singleton class::X> + end + + def x + X + end + end + end + + CS_SINGLETON4 = [Object.new, Object.new] + CS_SINGLETON4_CLASSES = [] + 2.times do |i| + obj = CS_SINGLETON4[i] + $spec_i = i + class << obj + class X + CS_SINGLETON4_CLASSES << self + CONST = ($spec_i + 1) + + def foo + CONST + end + end + end + end + +end diff --git a/spec/ruby/language/fixtures/def.rb b/spec/ruby/language/fixtures/def.rb new file mode 100644 index 0000000000..e07060ed74 --- /dev/null +++ b/spec/ruby/language/fixtures/def.rb @@ -0,0 +1,14 @@ +def toplevel_define_other_method + def nested_method_in_toplevel_method + 42 + end +end + +def some_toplevel_method +end + +public +def public_toplevel_method +end + +private diff --git a/spec/ruby/language/fixtures/defined.rb b/spec/ruby/language/fixtures/defined.rb new file mode 100644 index 0000000000..3761cfa5bd --- /dev/null +++ b/spec/ruby/language/fixtures/defined.rb @@ -0,0 +1,312 @@ +module DefinedSpecs + self::SelfScoped = 42 + + def self.side_effects + ScratchPad.record :defined_specs_side_effects + end + + def self.fixnum_method + ScratchPad.record :defined_specs_fixnum_method + 42 + end + + def self.exception_method + ScratchPad.record :defined_specs_exception + raise "defined? specs exception method" + end + + def self.defined_method + DefinedSpecs + end + + def self.any_args(*) + end + + class Basic + A = 42 + + def defined_method + DefinedSpecs + end + + def a_defined_method + end + + def protected_method + end + protected :protected_method + + def private_method + end + private :private_method + + def private_method_defined + defined? private_method + end + + def private_predicate? + end + private :private_predicate? + + def private_predicate_defined + defined? private_predicate? + end + + def local_variable_defined + x = 2 + defined? x + end + + def local_variable_defined_nil + x = nil + defined? x + end + + def instance_variable_undefined + defined? @instance_variable_undefined + end + + def instance_variable_read + value = @instance_variable_read + defined? @instance_variable_read + end + + def instance_variable_defined + @instance_variable_defined = 1 + defined? @instance_variable_defined + end + + def instance_variable_defined_nil + @instance_variable_defined_nil = nil + defined? @instance_variable_defined_nil + end + + def global_variable_undefined + defined? $defined_specs_global_variable_undefined + end + + def global_variable_read + suppress_warning do + value = $defined_specs_global_variable_read + end + defined? $defined_specs_global_variable_read + end + + def global_variable_defined + $defined_specs_global_variable_defined = 1 + 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 + + def class_variable_defined + @@class_variable_defined = 1 + defined? @@class_variable_defined + end + + def yield_defined_method + defined? yield + end + + def yield_defined_parameter_method(&block) + defined? yield + end + + def no_yield_block + yield_defined_method + end + + def no_yield_block_parameter + yield_defined_parameter_method + end + + def yield_block + yield_defined_method { 42 } + end + + def yield_block_parameter + yield_defined_parameter_method { 42 } + end + end + + module Mixin + MixinConstant = 42 + + def defined_super + defined? super() + end + end + + class Parent + ParentConstant = 42 + + def defined_super; end + end + + class Child < Parent + include Mixin + + A = 42 + + def self.parent_constant_defined + defined? self::ParentConstant + end + + def self.module_defined + defined? Mixin + end + + def self.module_constant_defined + defined? MixinConstant + end + + def defined_super + super + end + end + + class Superclass + def yield_method + yield + end + + def method_no_args + end + + def method_args + end + + def method_block_no_args + end + + def method_block_args + end + + def define_method_no_args + end + + def define_method_args + end + + def define_method_block_no_args + end + + def define_method_block_args + end + end + + class Super < Superclass + def no_super_method_no_args + defined? super + end + + def no_super_method_args + defined? super() + end + + def method_no_args + defined? super + end + + def method_args + defined? super() + end + + def no_super_method_block_no_args + yield_method { defined? super } + end + + def no_super_method_block_args + yield_method { defined? super() } + end + + def method_block_no_args + yield_method { defined? super } + end + + def method_block_args + yield_method { defined? super() } + end + + define_method(:no_super_define_method_no_args) { defined? super } + define_method(:no_super_define_method_args) { defined? super() } + define_method(:define_method_no_args) { defined? super } + define_method(:define_method_args) { defined? super() } + + define_method(:no_super_define_method_block_no_args) do + yield_method { defined? super } + end + + define_method(:no_super_define_method_block_args) do + yield_method { defined? super() } + end + + define_method(:define_method_block_no_args) do + yield_method { defined? super } + end + + define_method(:define_method_block_args) do + yield_method { defined? super() } + end + end + + class ClassWithMethod + def test + end + end + + class ClassUndefiningMethod < ClassWithMethod + undef :test + end + + class ClassWithoutMethod < ClassUndefiningMethod + # If an undefined method overridden in descendants + # define?(super) should return nil + def test + defined?(super) + end + end + + module IntermediateModule1 + def method_no_args + end + end + + module IntermediateModule2 + def method_no_args + defined?(super) + end + end + + module ModuleWithConstMissing + def self.const_missing(const) + const + end + end + + class SuperWithIntermediateModules + include IntermediateModule1 + include IntermediateModule2 + + def method_no_args + super + end + end +end + +class Object + def defined_specs_method + DefinedSpecs + end + + def defined_specs_receiver + DefinedSpecs::Basic.new + end +end 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/dollar_zero.rb b/spec/ruby/language/fixtures/dollar_zero.rb new file mode 100644 index 0000000000..683bce8d4e --- /dev/null +++ b/spec/ruby/language/fixtures/dollar_zero.rb @@ -0,0 +1,6 @@ +puts $0 +puts __FILE__ + +if $0 == __FILE__ + print "OK" +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 new file mode 100644 index 0000000000..6047ac5bc0 --- /dev/null +++ b/spec/ruby/language/fixtures/ensure.rb @@ -0,0 +1,121 @@ +module EnsureSpec + class Container + attr_reader :executed + + def initialize + @executed = [] + end + + def raise_in_method_with_ensure + @executed << :method + raise EnsureSpec::Error + ensure + @executed << :ensure + end + + def raise_and_rescue_in_method_with_ensure + @executed << :method + raise "An Exception" + rescue + @executed << :rescue + ensure + @executed << :ensure + end + + def throw_in_method_with_ensure + @executed << :method + throw(:symbol) + ensure + @executed << :ensure + end + + def implicit_return_in_method_with_ensure + :method + ensure + :ensure + end + + def explicit_return_in_method_with_ensure + return :method + 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 + +module EnsureSpec + + class Test + + def initialize + @values = [] + end + + attr_reader :values + + def call_block + begin + @values << :start + yield + ensure + @values << :end + end + end + + def do_test + call_block do + @values << :in_block + return :did_test + end + end + end +end + +module EnsureSpec + class Error < RuntimeError + end +end diff --git a/spec/ruby/language/fixtures/file.rb b/spec/ruby/language/fixtures/file.rb new file mode 100644 index 0000000000..7b862cfe1a --- /dev/null +++ b/spec/ruby/language/fixtures/file.rb @@ -0,0 +1 @@ +ScratchPad.record __FILE__ 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_across_files.rb b/spec/ruby/language/fixtures/freeze_magic_comment_across_files.rb new file mode 100644 index 0000000000..3aed2f29b6 --- /dev/null +++ b/spec/ruby/language/fixtures/freeze_magic_comment_across_files.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require_relative 'freeze_magic_comment_required' + +p "abc".object_id == $second_literal_id diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_across_files_diff_enc.rb b/spec/ruby/language/fixtures/freeze_magic_comment_across_files_diff_enc.rb new file mode 100644 index 0000000000..53ef959970 --- /dev/null +++ b/spec/ruby/language/fixtures/freeze_magic_comment_across_files_diff_enc.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require_relative 'freeze_magic_comment_required_diff_enc' + +p "abc".object_id != $second_literal_id diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_across_files_no_comment.rb b/spec/ruby/language/fixtures/freeze_magic_comment_across_files_no_comment.rb new file mode 100644 index 0000000000..fc6cd5bf82 --- /dev/null +++ b/spec/ruby/language/fixtures/freeze_magic_comment_across_files_no_comment.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require_relative 'freeze_magic_comment_required_no_comment' + +p "abc".object_id != $second_literal_id diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_one_literal.rb b/spec/ruby/language/fixtures/freeze_magic_comment_one_literal.rb new file mode 100644 index 0000000000..d35905b332 --- /dev/null +++ b/spec/ruby/language/fixtures/freeze_magic_comment_one_literal.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +ids = Array.new(2) { "abc".object_id } +p ids.first == ids.last diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_required.rb b/spec/ruby/language/fixtures/freeze_magic_comment_required.rb new file mode 100644 index 0000000000..a4ff4459b1 --- /dev/null +++ b/spec/ruby/language/fixtures/freeze_magic_comment_required.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +$second_literal_id = "abc".object_id 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 new file mode 100644 index 0000000000..f72a32e879 --- /dev/null +++ b/spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb @@ -0,0 +1,4 @@ +# encoding: euc-jp # built-in for old regexp option +# frozen_string_literal: true + +$second_literal_id = "abc".object_id diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_required_no_comment.rb b/spec/ruby/language/fixtures/freeze_magic_comment_required_no_comment.rb new file mode 100644 index 0000000000..e09232a5f4 --- /dev/null +++ b/spec/ruby/language/fixtures/freeze_magic_comment_required_no_comment.rb @@ -0,0 +1 @@ +$second_literal_id = "abc".object_id diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_two_literals.rb b/spec/ruby/language/fixtures/freeze_magic_comment_two_literals.rb new file mode 100644 index 0000000000..cccc5969bd --- /dev/null +++ b/spec/ruby/language/fixtures/freeze_magic_comment_two_literals.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +p "abc".equal?("abc") diff --git a/spec/ruby/language/fixtures/hash_strings_binary.rb b/spec/ruby/language/fixtures/hash_strings_binary.rb new file mode 100644 index 0000000000..44b99cbf80 --- /dev/null +++ b/spec/ruby/language/fixtures/hash_strings_binary.rb @@ -0,0 +1,7 @@ +# encoding: binary + +module HashStringsBinary + def self.literal_hash + {"foo" => "bar"} + end +end diff --git a/spec/ruby/language/fixtures/hash_strings_usascii.rb b/spec/ruby/language/fixtures/hash_strings_usascii.rb new file mode 100644 index 0000000000..18cfef7c8c --- /dev/null +++ b/spec/ruby/language/fixtures/hash_strings_usascii.rb @@ -0,0 +1,7 @@ +# encoding: us-ascii + +module HashStringsUSASCII + def self.literal_hash + {"foo" => "bar"} + end +end diff --git a/spec/ruby/language/fixtures/hash_strings_utf8.rb b/spec/ruby/language/fixtures/hash_strings_utf8.rb new file mode 100644 index 0000000000..7928090282 --- /dev/null +++ b/spec/ruby/language/fixtures/hash_strings_utf8.rb @@ -0,0 +1,7 @@ +# encoding: utf-8 + +module HashStringsUTF8 + def self.literal_hash + {"foo" => "bar"} + end +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/match_operators.rb b/spec/ruby/language/fixtures/match_operators.rb new file mode 100644 index 0000000000..f04c54d723 --- /dev/null +++ b/spec/ruby/language/fixtures/match_operators.rb @@ -0,0 +1,9 @@ +class OperatorImplementor + def =~(val) + return val + end + + def !~(val) + return val + end +end diff --git a/spec/ruby/language/fixtures/metaclass.rb b/spec/ruby/language/fixtures/metaclass.rb new file mode 100644 index 0000000000..a8f837e701 --- /dev/null +++ b/spec/ruby/language/fixtures/metaclass.rb @@ -0,0 +1,33 @@ +module MetaClassSpecs + + def self.metaclass_of obj + class << obj + self + end + end + + class A + def self.cheese + 'edam' + end + end + + class B < A + def self.cheese + 'stilton' + end + end + + class C + class << self + class << self + def ham + 'iberico' + end + end + end + end + + class D < C; end + +end diff --git a/spec/ruby/language/fixtures/module.rb b/spec/ruby/language/fixtures/module.rb new file mode 100644 index 0000000000..75eee77791 --- /dev/null +++ b/spec/ruby/language/fixtures/module.rb @@ -0,0 +1,15 @@ +module ModuleSpecs + module Modules + class Klass + end + + A = "Module" + B = 1 + C = nil + D = true + E = false + end + + module Anonymous + end +end diff --git a/spec/ruby/language/fixtures/next.rb b/spec/ruby/language/fixtures/next.rb new file mode 100644 index 0000000000..fbca842334 --- /dev/null +++ b/spec/ruby/language/fixtures/next.rb @@ -0,0 +1,134 @@ +class NextSpecs + def self.yielding_method(expected) + yield.should == expected + :method_return_value + end + + def self.yielding + yield + end + + # The methods below are defined to spec the behavior of the next statement + # while specifically isolating whether the statement is in an Iter block or + # not. In a normal spec example, the code is always nested inside a block. + # Rather than rely on that implicit context in this case, the context is + # made explicit because of the interaction of next in a loop nested inside + # an Iter block. + def self.while_next(arg) + x = true + while x + begin + ScratchPad << :begin + x = false + if arg + next 42 + else + next + end + ensure + ScratchPad << :ensure + end + end + end + + def self.while_within_iter(arg) + yielding do + x = true + while x + begin + ScratchPad << :begin + x = false + if arg + next 42 + else + next + end + ensure + ScratchPad << :ensure + end + end + end + end + + def self.until_next(arg) + x = false + until x + begin + ScratchPad << :begin + x = true + if arg + next 42 + else + next + end + ensure + ScratchPad << :ensure + end + end + end + + def self.until_within_iter(arg) + yielding do + x = false + until x + begin + ScratchPad << :begin + x = true + if arg + next 42 + else + next + end + ensure + ScratchPad << :ensure + end + end + end + end + + def self.loop_next(arg) + x = 1 + loop do + break if x == 2 + + begin + ScratchPad << :begin + x += 1 + if arg + next 42 + else + next + end + ensure + ScratchPad << :ensure + end + end + end + + def self.loop_within_iter(arg) + yielding do + x = 1 + loop do + break if x == 2 + + begin + ScratchPad << :begin + x += 1 + if arg + next 42 + else + next + end + ensure + ScratchPad << :ensure + end + end + end + end + + class Block + def method(v) + yield v + end + end +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/precedence.rb b/spec/ruby/language/fixtures/precedence.rb new file mode 100644 index 0000000000..d2295c755b --- /dev/null +++ b/spec/ruby/language/fixtures/precedence.rb @@ -0,0 +1,16 @@ +module PrecedenceSpecs + class NonUnaryOpTest + def add_num(arg) + [1].collect { |i| arg + i +1 } + end + def sub_num(arg) + [1].collect { |i| arg + i -1 } + end + def add_str + %w[1].collect { |i| i +'1' } + end + def add_var + [1].collect { |i| i +i } + end + end +end 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 new file mode 100644 index 0000000000..da3e0a97f9 --- /dev/null +++ b/spec/ruby/language/fixtures/private.rb @@ -0,0 +1,59 @@ +module Private + class A + def foo + "foo" + end + + private + def bar + "bar" + end + end + + class B + def foo + "foo" + end + + private + + def self.public_defs_method; 0; end + + class C + def baz + "baz" + end + end + + class << self + def public_class_method1; 1; end + private + def private_class_method1; 1; end + end + + def bar + "bar" + end + end + + module D + private + def foo + "foo" + end + 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 new file mode 100644 index 0000000000..b906e17a2f --- /dev/null +++ b/spec/ruby/language/fixtures/rescue.rb @@ -0,0 +1,67 @@ +module RescueSpecs + def self.begin_else(raise_exception) + begin + ScratchPad << :one + raise "an error occurred" if raise_exception + rescue + ScratchPad << :rescue_ran + :rescue_val + else + ScratchPad << :else_ran + :val + end + end + + def self.begin_else_ensure(raise_exception) + begin + ScratchPad << :one + raise "an error occurred" if raise_exception + rescue + ScratchPad << :rescue_ran + :rescue_val + else + ScratchPad << :else_ran + :val + ensure + ScratchPad << :ensure_ran + :ensure_val + end + end + + def self.begin_else_return(raise_exception) + begin + ScratchPad << :one + raise "an error occurred" if raise_exception + rescue + ScratchPad << :rescue_ran + :rescue_val + else + ScratchPad << :else_ran + :val + end + ScratchPad << :outside_begin + :return_val + end + + def self.begin_else_return_ensure(raise_exception) + begin + ScratchPad << :one + raise "an error occurred" if raise_exception + rescue + ScratchPad << :rescue_ran + :rescue_val + else + ScratchPad << :else_ran + :val + ensure + ScratchPad << :ensure_ran + :ensure_val + end + 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 new file mode 100644 index 0000000000..f6b143f3fa --- /dev/null +++ b/spec/ruby/language/fixtures/return.rb @@ -0,0 +1,135 @@ +module ReturnSpecs + class Blocks + def yielding_method + yield + ScratchPad.record :after_yield + end + + def enclosing_method + yielding_method do + ScratchPad.record :before_return + return :return_value + ScratchPad.record :after_return + end + + ScratchPad.record :after_call + end + end + + class NestedCalls < Blocks + def invoking_method(&b) + yielding_method(&b) + ScratchPad.record :after_invoke + end + + def enclosing_method + invoking_method do + ScratchPad.record :before_return + return :return_value + ScratchPad.record :after_return + end + ScratchPad.record :after_invoke + end + end + + class NestedBlocks < Blocks + def enclosing_method + yielding_method do + yielding_method do + ScratchPad.record :before_return + return :return_value + ScratchPad.record :after_return + end + ScratchPad.record :after_invoke1 + end + ScratchPad.record :after_invoke2 + end + end + + class SavedInnerBlock + def add(&b) + @block = b + end + + def outer + yield + @block.call + end + + def inner + yield + end + + def start + outer do + inner do + add do + ScratchPad.record :before_return + return :return_value + end + end + end + + ScratchPad.record :bottom_of_start + + return false + end + end + + class ThroughDefineMethod + lamb = proc { |x| x.call } + define_method :foo, lamb + + def mp(&b); b; end + + def outer + pr = mp { return :good } + + foo(pr) + return :bad + end + end + + class DefineMethod + lamb = proc { return :good } + define_method :foo, lamb + + def outer + val = :bad + + # This is tricky, but works. If lamb properly returns, then the + # return value will go into val before we run the ensure. + # + # If lamb's return keeps unwinding incorrectly, val will still + # have its old value. + # + # We can therefore use val to figure out what happened. + begin + val = foo() + ensure + return val + end + end + end + + class MethodWithBlock + def method1 + return [2, 3].inject 0 do |a, b| + a + b + end + nil + end + + def get_ary(count, &blk) + count.times.to_a do |i| + blk.call(i) if blk + end + end + + def method2 + return get_ary 3 do |i| + end + nil + end + 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 new file mode 100644 index 0000000000..4787abee5c --- /dev/null +++ b/spec/ruby/language/fixtures/send.rb @@ -0,0 +1,151 @@ +module LangSendSpecs + module_function + + def fooM0; 100 end + def fooM1(a); [a]; end + def fooM2(a,b); [a,b]; end + def fooM3(a,b,c); [a,b,c]; end + def fooM4(a,b,c,d); [a,b,c,d]; end + def fooM5(a,b,c,d,e); [a,b,c,d,e]; end + def fooM0O1(a=1); [a]; end + def fooM1O1(a,b=1); [a,b]; end + def fooM2O1(a,b,c=1); [a,b,c]; end + def fooM3O1(a,b,c,d=1); [a,b,c,d]; end + def fooM4O1(a,b,c,d,e=1); [a,b,c,d,e]; end + def fooM0O2(a=1,b=2); [a,b]; end + def fooM0R(*r); r; end + def fooM1R(a, *r); [a, r]; end + def fooM0O1R(a=1, *r); [a, r]; end + def fooM1O1R(a, b=1, *r); [a, b, r]; end + + def one(a); a; end + def oneb(a,&b); [a,yield(b)]; end + def twob(a,b,&c); [a,b,yield(c)]; end + def makeproc(&b) b end + + def yield_now; yield; end + + def double(x); x * 2 end + def weird_parens + # means double((5).to_s) + # NOT (double(5)).to_s + double (5).to_s + end + + def rest_len(*a); a.size; end + + def self.twos(a,b,*c) + [c.size, c.last] + end + + class PrivateSetter + attr_reader :foo + attr_writer :foo + private :foo= + + def call_self_foo_equals(value) + self.foo = value + end + + def call_self_foo_equals_masgn(value) + a, self.foo = 1, value + end + end + + class PrivateGetter + attr_accessor :foo + private :foo + private :foo= + + def call_self_foo + self.foo + end + + def call_self_foo_or_equals(value) + self.foo ||= 6 + end + end + + class AttrSet + attr_reader :result + def []=(a, b, c, d); @result = [a,b,c,d]; end + end + + class ToProc + def initialize(val) + @val = val + end + + def to_proc + Proc.new { @val } + 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 + end + + def to_ary + @obj + end + end + + class MethodMissing + def initialize + @message = nil + @args = nil + end + + attr_reader :message, :args + + def method_missing(m, *a) + @message = m + @args = a + end + end + + class Attr19Set + attr_reader :result + def []=(*args); @result = args; end + end + + module_function + + def fooR(*r); r; end + def fooM0RQ1(*r, q); [r, q]; end + def fooM0RQ2(*r, s, q); [r, s, q]; end + def fooM1RQ1(a, *r, q); [a, r, q]; end + def fooM1O1RQ1(a, b=9, *r, q); [a, b, r, q]; end + def fooM1O1RQ2(a, b=9, *r, q, t); [a, b, r, q, t]; end + + def fooO1Q1(a=1, b); [a,b]; end + def fooM1O1Q1(a,b=2,c); [a,b,c]; end + def fooM2O1Q1(a,b,c=3,d); [a,b,c,d]; end + def fooM2O2Q1(a,b,c=3,d=4,e); [a,b,c,d,e]; end + def fooO4Q1(a=1,b=2,c=3,d=4,e); [a,b,c,d,e]; end + def fooO4Q2(a=1,b=2,c=3,d=4,e,f); [a,b,c,d,e,f]; end + + def destructure2((a,b)); a+b; end + def destructure2b((a,b)); [a,b]; end + def destructure4r((a,b,*c,d,e)); [a,b,c,d,e]; end + def destructure4o(a=1,(b,c),d,&e); [a,b,c,d]; end + def destructure5o(a=1, f=2, (b,c),d,&e); [a,f,b,c,d]; end + def destructure7o(a=1, f=2, (b,c),(d,e), &g); [a,f,b,c,d,e]; end + def destructure7b(a=1, f=2, (b,c),(d,e), &g); g.call([a,f,b,c,d,e]); end + def destructure4os(a=1,(b,*c)); [a,b,c]; end +end + +def lang_send_rest_len(*a) + a.size +end 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 new file mode 100644 index 0000000000..984a629e5b --- /dev/null +++ b/spec/ruby/language/fixtures/squiggly_heredoc.rb @@ -0,0 +1,71 @@ +module SquigglyHeredocSpecs + def self.message + <<~HEREDOC + character density, n.: + The number of very weird people in the office. + HEREDOC + end + + def self.blank + <<~HERE + HERE + end + + def self.unquoted + <<~HERE + unquoted #{"interpolated"} + HERE + end + + def self.doublequoted + <<~"HERE" + doublequoted #{"interpolated"} + HERE + end + + def self.singlequoted + <<~'HERE' + singlequoted #{"interpolated"} + 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 + b + 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 new file mode 100644 index 0000000000..c5bdcf0e40 --- /dev/null +++ b/spec/ruby/language/fixtures/super.rb @@ -0,0 +1,804 @@ +module SuperSpecs + module S1 + class A + def foo(a) + a << "A#foo" + bar(a) + end + def bar(a) + a << "A#bar" + end + end + class B < A + def foo(a) + a << "B#foo" + super(a) + end + def bar(a) + a << "B#bar" + super(a) + end + end + end + + module S2 + class A + def baz(a) + a << "A#baz" + end + end + class B < A + def foo(a) + a << "B#foo" + baz(a) + end + end + class C < B + def baz(a) + a << "C#baz" + super(a) + end + end + end + + module S3 + class A + def foo(a) + a << "A#foo" + end + def self.foo(a) + a << "A.foo" + end + def self.bar(a) + a << "A.bar" + foo(a) + end + end + class B < A + def self.foo(a) + a << "B.foo" + super(a) + end + def self.bar(a) + a << "B.bar" + super(a) + end + end + end + + module S4 + class A + def foo(a) + a << "A#foo" + end + end + class B < A + def foo(a, b) + a << "B#foo(a,#{b})" + super(a) + end + end + end + + class S5 + def here + :good + end + end + + class S6 < S5 + def under + yield + end + + def here + under { + super + } + end + end + + class S7 < S5 + define_method(:here) { super() } + end + + module MS1 + module ModA + def foo(a) + a << "ModA#foo" + bar(a) + end + def bar(a) + a << "ModA#bar" + end + end + class A + include ModA + end + module ModB + def bar(a) + a << "ModB#bar" + super(a) + end + end + class B < A + def foo(a) + a << "B#foo" + super(a) + end + include ModB + end + end + + module MS2 + class A + def baz(a) + a << "A#baz" + end + end + module ModB + def foo(a) + a << "ModB#foo" + baz(a) + end + end + class B < A + include ModB + end + class C < B + def baz(a) + a << "C#baz" + super(a) + end + end + end + + module MultiSuperTargets + module M + def foo + super + end + end + + class BaseA + def foo + :BaseA + end + end + + class BaseB + def foo + :BaseB + end + end + + class A < BaseA + include M + end + + class B < BaseB + include M + end + end + + module MS3 + module ModA + def foo(a) + a << "ModA#foo" + end + def bar(a) + a << "ModA#bar" + foo(a) + end + end + class A + def foo(a) + a << "A#foo" + end + class << self + include ModA + end + end + class B < A + def self.foo(a) + a << "B.foo" + super(a) + end + def self.bar(a) + a << "B.bar" + super(a) + end + end + end + + module MS4 + module Layer1 + def example + 5 + end + end + + module Layer2 + include Layer1 + def example + super + end + end + + class A + include Layer2 + public :example + end + end + + class MM_A + undef_method :is_a? + end + + class MM_B < MM_A + def is_a?(blah) + # should fire the method_missing below + super + end + + def method_missing(*) + false + end + end + + class Alias1 + def name + [:alias1] + end + end + + class Alias2 < Alias1 + def initialize + @times = 0 + end + + def name + if @times >= 10 + raise "runaway super" + end + + @times += 1 + + # Use this so that we can see collect all supers that we see. + # One bug that arises is that we call Alias2#name from Alias2#name + # as it's superclass. In that case, either we get a runaway recursion + # super OR we get the return value being [:alias2, :alias2, :alias1] + # rather than [:alias2, :alias1]. + # + # Which one depends on caches and how super is implemented. + [:alias2] + super + end + end + + class Alias3 < Alias2 + alias_method :name3, :name + # In the method table for Alias3 now should be a special alias entry + # that references Alias2 and Alias2#name (probably as an object). + # + # When name3 is called then, Alias2 (NOT Alias3) is presented as the + # current module to Alias2#name, so that when super is called, + # 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. + end + + module AliasWithSuper + module AS1 + def foo + :a + end + end + + module BS1 + def foo + [:b, super] + end + end + + class Base + extend AS1 + extend BS1 + end + + class Trigger < Base + class << self + def foo_quux + foo_baz + end + + alias_method :foo_baz, :foo + alias_method :foo, :foo_quux + end + end + end + + module RestArgsWithSuper + class A + def a(*args) + args + end + end + + class B < A + def a(*args) + args << "foo" + + super + end + end + end + + class AnonymousModuleIncludedTwiceBase + def self.whatever + mod = Module.new do + def a(array) + array << "anon" + super + end + end + + include mod + end + + def a(array) + array << "non-anon" + end + end + + class AnonymousModuleIncludedTwice < AnonymousModuleIncludedTwiceBase + whatever + whatever + end + + module ZSuperWithBlock + class A + def a + yield + end + + def b(&block) + block.call + end + + def c + yield + end + end + + class B < A + def a + super { 14 } + end + + def b + block_ref = -> { 15 } + [super { 14 }, super(&block_ref)] + end + + def c + block_ref = -> { 16 } + super(&block_ref) + end + end + end + + module ZSuperWithOptional + class A + def m(x, y, z) + z + end + end + + class B < A + def m(x, y, z = 14) + super + end + end + + class C < A + def m(x, y, z = 14) + z = 100 + super + end + end + end + + module ZSuperWithRest + class A + def m(*args) + args + end + + def m_modified(*args) + args + end + end + + class B < A + def m(*args) + super + end + + def m_modified(*args) + args[1] = 14 + super + end + end + end + + module ZSuperWithRestAndOthers + class A + def m(a, b, *args) + args + end + + def m_modified(a, b, *args) + args + end + end + + class B < A + def m(a, b, *args) + super + end + + def m_modified(a, b, *args) + args[1] = 14 + super + end + 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 + end + + class B < A + def m(_, _) + 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 + end + 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) + args + end + end + + # ---- + + class RequiredArguments < Arguments + def foo(a:) + super + end + end + + class OptionalArguments < Arguments + def foo(b: 'b') + super + end + end + + class PlaceholderArguments < Arguments + def foo(**args) + super + end + end + + # ---- + + class RequiredAndOptionalArguments < Arguments + def foo(a:, b: 'b') + super + end + end + + class RequiredAndPlaceholderArguments < Arguments + def foo(a:, **args) + super + end + end + + class OptionalAndPlaceholderArguments < Arguments + def foo(b: 'b', **args) + super + end + end + + # ---- + + class RequiredAndOptionalAndPlaceholderArguments < Arguments + def foo(a:, b: 'b', **args) + super + end + end + end + + module RegularAndKeywords + class Arguments + def foo(a, **options) + [a, options] + end + end + + # ----- + + class RequiredArguments < Arguments + def foo(a, b:) + super + end + end + + class OptionalArguments < Arguments + def foo(a, c: 'c') + super + end + end + + class PlaceholderArguments < Arguments + def foo(a, **options) + super + end + end + + # ----- + + class RequiredAndOptionalArguments < Arguments + def foo(a, b:, c: 'c') + super + end + end + + class RequiredAndPlaceholderArguments < Arguments + def foo(a, b:, **options) + super + end + end + + class OptionalAndPlaceholderArguments < Arguments + def foo(a, c: 'c', **options) + super + end + end + + # ----- + + class RequiredAndOptionalAndPlaceholderArguments < Arguments + def foo(a, b:, c: 'c', **options) + super + end + end + end + + module SplatAndKeywords + class Arguments + def foo(*args, **options) + [args, options] + end + end + + class AllArguments < Arguments + def foo(*args, **options) + super + end + end + end + + module FromBasicObject + def __send__(name, *args, &block) + super + end + end + + module IntermediateBasic + include FromBasicObject + end + + class IncludesFromBasic + include FromBasicObject + + def foobar; 43; end + end + + class IncludesIntermediate + include IntermediateBasic + + def foobar; 42; end + end + + module SingletonCase + class Base + def foobar(array) + array << :base + end + end + + class Foo < Base + def foobar(array) + array << :foo + super + end + end + end + + module SingletonAliasCase + class Base + def foobar(array) + array << :base + end + + def alias_on_singleton + object = self + singleton = (class << object; self; end) + singleton.__send__(:alias_method, :new_foobar, :foobar) + end + end + + class Foo < Base + def foobar(array) + array << :foo + super + end + end + end +end 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 new file mode 100644 index 0000000000..527caa7a78 --- /dev/null +++ b/spec/ruby/language/fixtures/variables.rb @@ -0,0 +1,157 @@ +module VariablesSpecs + class ParAsgn + attr_accessor :x + + def initialize + @x = 0 + end + + def inc + @x += 1 + end + + def to_ary + [1,2,3,4] + end + end + + class OpAsgn + attr_accessor :a, :b, :side_effect + + def do_side_effect + self.side_effect = true + return @a + end + + def do_more_side_effects + @a += 5 + self + end + + def do_bool_side_effects + @b += 1 + self + end + end + + class Hashalike + def [](k) k end + def []=(k, v) [k, v] end + end + + def self.reverse_foo(a, b) + return b, a + end + + class ArrayLike + def initialize(array) + @array = array + end + + def to_a + @array + end + end + + class ArraySubclass < Array + end + + class PrivateMethods + private + + def to_ary + [1, 2] + end + + def to_a + [3, 4] + end + end + + class ToAryNil + def to_ary + end + end + + class Chain + def self.without_parenthesis a + a + end + end + + 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 new file mode 100644 index 0000000000..9f7a2ba238 --- /dev/null +++ b/spec/ruby/language/fixtures/yield.rb @@ -0,0 +1,41 @@ +module YieldSpecs + class Yielder + def z + yield + end + + def ze(&block) + block = proc { block } + yield + end + + def s(a) + yield(a) + end + + def m(a, b, c) + yield(a, b, c) + end + + def r(a) + yield(*a) + end + + def k(a) + yield(*a, b: true) + end + + def rs(a, b, c) + yield(a, b, *c) + end + + def self.define_deep(&inned_block) + define_method 'deep' do |v| + # should yield to inner_block + yield v + end + end + + define_deep { |v| v * 2} + end +end diff --git a/spec/ruby/language/for_spec.rb b/spec/ruby/language/for_spec.rb new file mode 100644 index 0000000000..b8ddfe5f0d --- /dev/null +++ b/spec/ruby/language/for_spec.rb @@ -0,0 +1,285 @@ +require_relative '../spec_helper' +require_relative 'fixtures/for_scope' + +# for name[, name]... in expr [do] +# body +# end +describe "The for expression" do + it "iterates over an Enumerable passing each element to the block" do + j = 0 + for i in 1..3 + j += i + end + j.should == 6 + end + + it "iterates over a list of arrays and destructures with empty comma" do + for i, in [[1,2]] + i.should == 1 + 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 + + for i, j in { 1 => 10, 2 => 20 } + k += i + l += j + end + + k.should == 3 + l.should == 30 + end + + it "iterates over any object responding to 'each'" do + obj = Object.new + def obj.each + (0..10).each { |i| yield i } + end + + j = 0 + for i in obj + j += i + end + j.should == 55 + end + + it "allows an instance variable as an iterator name" do + m = [1,2,3] + n = 0 + for @var in m + n += 1 + end + @var.should == 3 + n.should == 3 + end + + it "allows a class variable as an iterator name" do + class OFor + m = [1,2,3] + n = 0 + for @@var in m + n += 1 + end + @@var.should == 3 + n.should == 3 + end + end + + it "allows a constant as an iterator name" do + class OFor + m = [1,2,3] + n = 0 + -> { + for CONST in m + n += 1 + end + }.should complain(/already initialized constant/) + CONST.should == 3 + n.should == 3 + 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 + class OFor + def each + [[1,2,3], [4,5,6]].each do |a| + yield(a[0],a[1],a[2]) + end + end + end + o = OFor.new + qs = [] + for q in o + qs << q + end + qs.should == [1, 4] + q.should == 4 + end + + it "optionally takes a 'do' after the expression" do + j = 0 + for i in 1..3 do + j += i + end + j.should == 6 + end + + it "allows body begin on the same line if do is used" do + j = 0 + for i in 1..3 do j += i + end + j.should == 6 + end + + it "executes code in containing variable scope" do + for i in 1..2 + a = 123 + end + + a.should == 123 + end + + it "executes code in containing variable scope with 'do'" do + for i in 1..2 do + a = 123 + end + + 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 } + end + + it "breaks out of a loop upon 'break', returning nil" do + j = 0 + for i in 1..3 + j += i + + break if i == 2 + end.should == nil + + j.should == 3 + end + + it "allows 'break' to have an argument which becomes the value of the for expression" do + for i in 1..3 + break 10 if i == 2 + end.should == 10 + end + + it "starts the next iteration with 'next'" do + j = 0 + for i in 1..5 + next if i == 2 + + j += i + end + + j.should == 13 + end + + it "repeats current iteration with 'redo'" do + j = 0 + for i in 1..3 + j += i + + redo if i == 2 && j < 4 + end + + j.should == 8 + end +end diff --git a/spec/ruby/language/hash_spec.rb b/spec/ruby/language/hash_spec.rb new file mode 100644 index 0000000000..668716e2e3 --- /dev/null +++ b/spec/ruby/language/hash_spec.rb @@ -0,0 +1,324 @@ +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 + {}.size.should == 0 + {}.should == {} + end + + it "{} should return a new hash populated with the given elements" do + h = {a: 'a', 'b' => 3, 44 => 2.3} + h.size.should == 3 + h.should == {a: "a", "b" => 3, 44 => 2.3} + end + + it "treats empty expressions as nils" do + h = {() => ()} + h.keys.should == [nil] + h.values.should == [nil] + h[nil].should == nil + + h = {() => :value} + h.keys.should == [nil] + h.values.should == [:value] + h[nil].should == :value + + h = {key: ()} + h.keys.should == [:key] + h.values.should == [nil] + h[:key].should == nil + end + + it "freezes string keys on initialization" do + key = +"foo" + h = {key => "bar"} + key.reverse! + h["foo"].should == "bar" + h.keys.first.should == "foo" + h.keys.first.should.frozen? + key.should == "oof" + end + + it "checks duplicated keys on initialization" do + -> { + @h = eval "{foo: :bar, foo: :foo}" + }.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 + h = {a: 1, b: 2,} + h.size.should == 2 + h.should == {a: 1, b: 2} + 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} + end + + it "with '==>' in the middle raises SyntaxError" 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 "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 + {foo: 123}.should == {foo: 123} + h = {rbx: :cool, specs: 'fail_sometimes'} + {rbx: :cool, specs: 'fail_sometimes'}.should == h + end + + it "ignores a hanging comma" do + {foo: 123,}.should == {foo: 123} + h = {rbx: :cool, specs: 'fail_sometimes'} + {rbx: :cool, specs: 'fail_sometimes',}.should == h + end + + it "accepts mixed 'key: value' and 'key => value' syntax" do + h = {:a => 1, :b => 2, "c" => 3} + {a: 1, b: 2, "c" => 3}.should == h + end + + it "accepts mixed 'key: value', 'key => value' and '\"key\"': value' syntax" do + h = {:a => 1, :b => 2, "c" => 3, :d => 4} + {a: 1, :b => 2, "c" => 3, "d": 4}.should == h + end + + it "expands an '**{}' element into the containing Hash literal initialization" do + {a: 1, **{b: 2}, c: 3}.should == {a: 1, b: 2, c: 3} + end + + it "expands an '**obj' element into the containing Hash literal initialization" do + h = {b: 2, c: 3} + {**h, a: 1}.should == {b: 2, c: 3, a: 1} + {a: 1, **h}.should == {a: 1, b: 2, c: 3} + {a: 1, **h, c: 4}.should == {a: 1, b: 2, c: 4} + end + + it "expands a BasicObject using ** into the containing Hash literal initialization" do + h = BasicObject.new + def h.to_hash; {:b => 2, :c => 3}; end + {**h, a: 1}.should == {b: 2, c: 3, a: 1} + {a: 1, **h}.should == {a: 1, b: 2, c: 3} + {a: 1, **h, c: 4}.should == {a: 1, b: 2, c: 4} + end + + 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 + -> { + @h = eval "{a: 1, **{a: 2, **{b: 3, **{c: 4}}, **{d: 5}, }, **{d: 6}}" + }.should complain(/key :a is duplicated|duplicated key/) + @h.should == {a: 2, b: 3, c: 4, d: 6} + end + + it "calls #to_hash to expand an '**obj' element" do + obj = mock("hash splat") + obj.should_receive(:to_hash).and_return({b: 2, d: 4}) + + {a: 1, **obj, c: 3}.should == {a:1, b: 2, c: 3, d: 4} + end + + it "allows splatted elements keys that are not symbols" do + h = {1 => 2, b: 3} + {a: 1, **h}.should == {a: 1, 1 => 2, b: 3} + end + + it "raises a TypeError if #to_hash does not return a Hash" do + obj = mock("hash splat") + obj.should_receive(:to_hash).and_return(obj) + + -> { {**obj} }.should raise_error(TypeError) + 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 + binary_hash = HashStringsBinary.literal_hash + utf8_hash = HashStringsUTF8.literal_hash + usascii_hash = HashStringsUSASCII.literal_hash + + 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 new file mode 100644 index 0000000000..47ee9c2c51 --- /dev/null +++ b/spec/ruby/language/heredoc_spec.rb @@ -0,0 +1,119 @@ +# -*- encoding: us-ascii -*- + +require_relative '../spec_helper' + +describe "Heredoc string" do + + before :each do + @ip = 'xxx' # used for interpolation + end + + it "allows HEREDOC with <<identifier, interpolated" do + s = <<HERE +foo bar#{@ip} +HERE + s.should == "foo barxxx\n" + s.encoding.should == Encoding::US_ASCII + end + + it 'allow HEREDOC with <<"identifier", interpolated' do + s = <<"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 + s = <<'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 + s = <<-HERE + foo bar#{@ip} + HERE + + s.should == " foo barxxx\n" + s.encoding.should == Encoding::US_ASCII + end + + it 'allows HEREDOC with <<-"identifier", allowing to indent identifier, interpolated' do + s = <<-"HERE" + foo bar#{@ip} + 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 + s = <<-'HERE' + foo bar#{@ip} + HERE + + s.should == ' foo bar#{@ip}' + "\n" + s.encoding.should == Encoding::US_ASCII + 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 new file mode 100644 index 0000000000..2d1a89f081 --- /dev/null +++ b/spec/ruby/language/if_spec.rb @@ -0,0 +1,424 @@ +require_relative '../spec_helper' + +describe "The if expression" 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 + + 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 + + it "evaluates body if expression is true" do + a = [] + if true + a << 123 + end + a.should == [123] + end + + it "does not evaluate body if expression is false" do + a = [] + if false + a << 123 + end + a.should == [] + end + + it "does not evaluate body if expression is empty" do + a = [] + if () + a << 123 + end + a.should == [] + end + + it "does not evaluate else-body if expression is true" do + a = [] + if true + a << 123 + else + a << 456 + end + a.should == [123] + end + + it "evaluates only else-body if expression is false" do + a = [] + if false + a << 123 + else + a << 456 + end + a.should == [456] + end + + it "returns result of then-body evaluation if expression is true" do + if true + 123 + end.should == 123 + end + + it "returns result of last statement in then-body if expression is true" do + if true + 'foo' + 'bar' + 'baz' + end.should == 'baz' + end + + it "returns result of then-body evaluation if expression is true and else part is present" do + if true + 123 + else + 456 + end.should == 123 + end + + it "returns result of else-body evaluation if expression is false" do + if false + 123 + else + 456 + end.should == 456 + end + + it "returns nil if then-body is empty and expression is true" do + if true + end.should == nil + end + + it "returns nil if then-body is empty, expression is true and else part is present" do + if true + else + 456 + end.should == nil + end + + it "returns nil if then-body is empty, expression is true and else part is empty" do + if true + else + end.should == nil + end + + it "returns nil if else-body is empty and expression is false" do + if false + 123 + else + end.should == nil + end + + it "returns nil if else-body is empty, expression is false and then-body is empty" do + if false + else + end.should == nil + end + + it "considers an expression with nil result as false" do + if nil + 123 + else + 456 + end.should == 456 + end + + it "considers a non-nil and non-boolean object in expression result as true" do + if mock('x') + 123 + else + 456 + end.should == 123 + end + + it "considers a zero integer in expression result as true" do + if 0 + 123 + else + 456 + end.should == 123 + end + + it "allows starting else-body on the same line" do + if false + 123 + else 456 + end.should == 456 + end + + it "evaluates subsequent elsif statements and execute body of first matching" do + if false + 123 + elsif false + 234 + elsif true + 345 + elsif true + 456 + end.should == 345 + end + + it "evaluates else-body if no if/elsif statements match" do + if false + 123 + elsif false + 234 + elsif false + 345 + else + 456 + end.should == 456 + end + + it "allows 'then' after expression when then-body is on the next line" do + if true then + 123 + end.should == 123 + + if true then ; 123; end.should == 123 + end + + it "allows then-body on the same line separated with 'then'" do + if true then 123 + end.should == 123 + + if true then 123; end.should == 123 + end + + it "returns nil when then-body on the same line separated with 'then' and expression is false" do + if false then 123 + end.should == nil + + if false then 123; end.should == nil + end + + it "returns nil when then-body separated by 'then' is empty and expression is true" do + if true then + end.should == nil + + if true then ; end.should == nil + end + + it "returns nil when then-body separated by 'then', expression is false and no else part" do + if false then + end.should == nil + + if false then ; end.should == nil + end + + it "evaluates then-body when then-body separated by 'then', expression is true and else part is present" do + if true then 123 + else 456 + end.should == 123 + + if true then 123; else 456; end.should == 123 + end + + it "evaluates else-body when then-body separated by 'then' and expression is false" do + if false then 123 + else 456 + end.should == 456 + + if false then 123; else 456; end.should == 456 + end + + describe "with a boolean range ('flip-flop' operator)" do + before :each do + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "mimics an awk conditional with a single-element inclusive-end range" do + 10.times { |i| ScratchPad << i if (i == 4)..(i == 4) } + ScratchPad.recorded.should == [4] + end + + it "mimics an awk conditional with a many-element inclusive-end range" do + 10.times { |i| ScratchPad << i if (i == 4)..(i == 7) } + ScratchPad.recorded.should == [4, 5, 6, 7] + end + + it "mimics a sed conditional with a zero-element exclusive-end range" do + eval "10.times { |i| ScratchPad << i if (i == 4)...(i == 4) }" + ScratchPad.recorded.should == [4, 5, 6, 7, 8, 9] + end + + it "mimics a sed conditional with a many-element exclusive-end range" do + 10.times { |i| ScratchPad << i if (i == 4)...(i == 5) } + ScratchPad.recorded.should == [4, 5] + end + + it "allows combining two flip-flops" do + 10.times { |i| ScratchPad << i if (i == 4)...(i == 5) or (i == 7)...(i == 8) } + ScratchPad.recorded.should == [4, 5, 7, 8] + end + + it "evaluates the first conditions lazily with inclusive-end range" do + collector = proc { |i| ScratchPad << i } + eval "10.times { |i| i if collector[i]...false }" + ScratchPad.recorded.should == [0] + end + + it "evaluates the first conditions lazily with exclusive-end range" do + collector = proc { |i| ScratchPad << i } + eval "10.times { |i| i if collector[i]..false }" + ScratchPad.recorded.should == [0] + end + + it "evaluates the second conditions lazily with inclusive-end range" do + collector = proc { |i| ScratchPad << i } + 10.times { |i| i if (i == 4)...collector[i] } + ScratchPad.recorded.should == [5] + end + + it "evaluates the second conditions lazily with exclusive-end range" do + collector = proc { |i| ScratchPad << i } + 10.times { |i| i if (i == 4)..collector[i] } + ScratchPad.recorded.should == [4] + end + + it "scopes state by flip-flop" do + store_me = proc { |i| ScratchPad << i if (i == 4)..(i == 7) } + store_me[1] + store_me[4] + proc { store_me[1] }.call + store_me[7] + store_me[5] + ScratchPad.recorded.should == [4, 1, 7] + end + + it "keeps flip-flops from interfering" do + a = eval "proc { |i| ScratchPad << i if (i == 4)..(i == 7) }" + b = eval "proc { |i| ScratchPad << i if (i == 4)..(i == 7) }" + 6.times(&a) + 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 + +describe "The postfix if form" do + it "evaluates statement if expression is true" do + a = [] + a << 123 if true + a.should == [123] + end + + it "does not evaluate statement if expression is false" do + a = [] + a << 123 if false + a.should == [] + end + + it "returns result of expression if value is true" do + (123 if true).should == 123 + end + + it "returns nil if expression is false" do + (123 if false).should == nil + end + + it "considers a nil expression as false" do + (123 if nil).should == nil + end + + it "considers a non-nil object as true" do + (123 if mock('x')).should == 123 + end + + it "evaluates then-body in containing scope" do + a = 123 + if true + b = a+1 + end + b.should == 124 + end + + it "evaluates else-body in containing scope" do + a = 123 + if false + b = a+1 + else + b = a+2 + end + b.should == 125 + end + + it "evaluates elsif-body in containing scope" do + a = 123 + if false + b = a+1 + elsif false + b = a+2 + elsif true + b = a+3 + else + b = a+4 + end + b.should == 126 + 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 new file mode 100644 index 0000000000..ed5a1c69e8 --- /dev/null +++ b/spec/ruby/language/lambda_spec.rb @@ -0,0 +1,587 @@ +require_relative '../spec_helper' +require_relative 'fixtures/classes' + +describe "A lambda literal -> () { }" do + SpecEvaluate.desc = "for definition" + + it "returns a Proc object when used in a BasicObject method" do + klass = Class.new(BasicObject) do + def create_lambda + -> { } + end + end + + klass.new.create_lambda.should be_an_instance_of(Proc) + end + + it "does not execute the block" do + -> { fail }.should be_an_instance_of(Proc) + end + + it "returns a lambda" do + -> { }.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 + l = -> arg { + var = arg + # this would override var if it was declared outside the lambda + l.call(arg-1) if arg > 0 + var + } + l.call(1).should == 1 + end + + context "assigns no local variables" do + evaluate <<-ruby do + @a = -> { } + @b = ->() { } + @c = -> () { } + @d = -> do end + ruby + + @a.().should be_nil + @b.().should be_nil + @c.().should be_nil + @d.().should be_nil + end + end + + context "assigns variables from parameters" do + evaluate <<-ruby do + @a = -> (a) { a } + ruby + + @a.(1).should == 1 + end + + evaluate <<-ruby do + @a = -> ((a)) { a } + ruby + + @a.(1).should == 1 + @a.([1, 2, 3]).should == 1 + end + + evaluate <<-ruby do + @a = -> ((*a, b)) { [a, b] } + ruby + + @a.(1).should == [[], 1] + @a.([1, 2, 3]).should == [[1, 2], 3] + end + + evaluate <<-ruby do + @a = -> (a={}) { a } + ruby + + @a.().should == {} + @a.(2).should == 2 + end + + evaluate <<-ruby do + @a = -> (*) { } + ruby + + @a.().should be_nil + @a.(1).should be_nil + @a.(1, 2, 3).should be_nil + end + + evaluate <<-ruby do + @a = -> (*a) { a } + ruby + + @a.().should == [] + @a.(1).should == [1] + @a.(1, 2, 3).should == [1, 2, 3] + end + + evaluate <<-ruby do + @a = -> (a:) { a } + ruby + + -> { @a.() }.should raise_error(ArgumentError) + @a.(a: 1).should == 1 + end + + evaluate <<-ruby do + @a = -> (a: 1) { a } + ruby + + @a.().should == 1 + @a.(a: 2).should == 2 + end + + evaluate <<-ruby do + @a = -> (**) { } + ruby + + @a.().should be_nil + @a.(a: 1, b: 2).should be_nil + -> { @a.(1) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + @a = -> (**k) { k } + ruby + + @a.().should == {} + @a.(a: 1, b: 2).should == {a: 1, b: 2} + end + + evaluate <<-ruby do + @a = -> (&b) { b } + ruby + + @a.().should be_nil + @a.() { }.should be_an_instance_of(Proc) + end + + evaluate <<-ruby do + @a = -> (a, b) { [a, b] } + ruby + + @a.(1, 2).should == [1, 2] + -> { @a.() }.should raise_error(ArgumentError) + -> { @a.(1) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + @a = -> ((a, b, *c, d), (*e, f, g), (*h)) do + [a, b, c, d, e, f, g, h] + end + ruby + + @a.(1, 2, 3).should == [1, nil, [], nil, [], 2, nil, [3]] + result = @a.([1, 2, 3], [4, 5, 6, 7, 8], [9, 10]) + result.should == [1, 2, [], 3, [4, 5, 6], 7, 8, [9, 10]] + end + + evaluate <<-ruby do + @a = -> (a, (b, (c, *d, (e, (*f)), g), (h, (i, j)))) do + [a, b, c, d, e, f, g, h, i, j] + end + ruby + + @a.(1, 2).should == [1, 2, nil, [], nil, [nil], nil, nil, nil, nil] + result = @a.(1, [2, [3, 4, 5, [6, [7, 8]], 9], [10, [11, 12]]]) + result.should == [1, 2, 3, [4, 5], 6, [7, 8], 9, 10, 11, 12] + end + + evaluate <<-ruby do + @a = -> (*, **k) { k } + ruby + + @a.().should == {} + @a.(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} + + h = mock("keyword splat") + h.should_not_receive(:to_hash) + @a.(h).should == {} + end + + evaluate <<-ruby do + @a = -> (*, &b) { b } + ruby + + @a.().should be_nil + @a.(1, 2, 3, 4).should be_nil + @a.(&(l = ->{})).should equal(l) + end + + evaluate <<-ruby do + @a = -> (a:, b:) { [a, b] } + ruby + + @a.(a: 1, b: 2).should == [1, 2] + end + + evaluate <<-ruby do + @a = -> (a:, b: 1) { [a, b] } + ruby + + @a.(a: 1).should == [1, 1] + @a.(a: 1, b: 2).should == [1, 2] + end + + evaluate <<-ruby do + @a = -> (a: 1, b:) { [a, b] } + ruby + + @a.(b: 0).should == [1, 0] + @a.(b: 2, a: 3).should == [3, 2] + end + + evaluate <<-ruby do + @a = -> (a: @a = -> (a: 1) { a }, b:) do + [a, b] + end + ruby + + @a.(a: 2, b: 3).should == [2, 3] + @a.(b: 1).should == [@a, 1] + + # Note the default value of a: in the original method. + @a.().should == 1 + end + + evaluate <<-ruby do + @a = -> (a: 1, b: 2) { [a, b] } + ruby + + @a.().should == [1, 2] + @a.(b: 3, a: 4).should == [4, 3] + end + + evaluate <<-ruby do + @a = -> (a, b=1, *c, (*d, (e)), f: 2, g:, h:, **k, &l) do + [a, b, c, d, e, f, g, h, k, l] + end + ruby + + result = @a.(9, 8, 7, 6, f: 5, g: 4, h: 3, &(l = ->{})) + result.should == [9, 8, [7], [], 6, 5, 4, 3, {}, l] + end + + evaluate <<-ruby do + @a = -> a, b=1, *c, d, e:, f: 2, g:, **k, &l do + [a, b, c, d, e, f, g, k, l] + end + ruby + + result = @a.(1, 2, e: 3, g: 4, h: 5, i: 6, &(l = ->{})) + result.should == [1, 1, [], 2, 3, 2, 4, { h: 5, i: 6 }, l] + end + + describe "with circular optional argument reference" do + 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 + + 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 + end + end + end +end + +describe "A lambda expression 'lambda { ... }'" do + SpecEvaluate.desc = "for definition" + + it "calls the #lambda method" do + obj = mock("lambda definition") + obj.should_receive(:lambda).and_return(obj) + + def obj.define + lambda { } + end + + obj.define.should equal(obj) + end + + it "does not execute the block" do + lambda { fail }.should be_an_instance_of(Proc) + end + + it "returns a lambda" do + lambda { }.lambda?.should be_true + end + + it "requires a block" do + 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 + before do + def meth; lambda; end + end + + it "raises ArgumentError" do + implicit_lambda = nil + suppress_warning do + -> { + meth { 1 } + }.should raise_error(ArgumentError, /tried to create Proc object without a block/) + end + end + end + + context "assigns no local variables" do + evaluate <<-ruby do + @a = lambda { } + @b = lambda { || } + ruby + + @a.().should be_nil + @b.().should be_nil + end + end + + context "assigns variables from parameters" do + evaluate <<-ruby do + @a = lambda { |a| a } + ruby + + @a.(1).should == 1 + end + + evaluate <<-ruby do + def m(*a) yield(*a) end + @a = lambda { |a| a } + ruby + + lambda { m(&@a) }.should raise_error(ArgumentError) + lambda { m(1, 2, &@a) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + @a = lambda { |a, | a } + ruby + + @a.(1).should == 1 + @a.([1, 2]).should == [1, 2] + + lambda { @a.() }.should raise_error(ArgumentError) + lambda { @a.(1, 2) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + def m(a) yield a end + def m2() yield end + + @a = lambda { |a, | a } + ruby + + m(1, &@a).should == 1 + m([1, 2], &@a).should == [1, 2] + + lambda { m2(&@a) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + @a = lambda { |(a)| a } + ruby + + @a.(1).should == 1 + @a.([1, 2, 3]).should == 1 + end + + evaluate <<-ruby do + @a = lambda { |(*a, b)| [a, b] } + ruby + + @a.(1).should == [[], 1] + @a.([1, 2, 3]).should == [[1, 2], 3] + end + + evaluate <<-ruby do + @a = lambda { |a={}| a } + ruby + + @a.().should == {} + @a.(2).should == 2 + end + + evaluate <<-ruby do + @a = lambda { |*| } + ruby + + @a.().should be_nil + @a.(1).should be_nil + @a.(1, 2, 3).should be_nil + end + + evaluate <<-ruby do + @a = lambda { |*a| a } + ruby + + @a.().should == [] + @a.(1).should == [1] + @a.(1, 2, 3).should == [1, 2, 3] + end + + evaluate <<-ruby do + @a = lambda { |a:| a } + ruby + + lambda { @a.() }.should raise_error(ArgumentError) + @a.(a: 1).should == 1 + end + + evaluate <<-ruby do + @a = lambda { |a: 1| a } + ruby + + @a.().should == 1 + @a.(a: 2).should == 2 + end + + evaluate <<-ruby do + @a = lambda { |**| } + ruby + + @a.().should be_nil + @a.(a: 1, b: 2).should be_nil + lambda { @a.(1) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + @a = lambda { |**k| k } + ruby + + @a.().should == {} + @a.(a: 1, b: 2).should == {a: 1, b: 2} + end + + evaluate <<-ruby do + @a = lambda { |&b| b } + ruby + + @a.().should be_nil + @a.() { }.should be_an_instance_of(Proc) + end + + evaluate <<-ruby do + @a = lambda { |a, b| [a, b] } + ruby + + @a.(1, 2).should == [1, 2] + end + + evaluate <<-ruby do + @a = lambda do |(a, b, *c, d), (*e, f, g), (*h)| + [a, b, c, d, e, f, g, h] + end + ruby + + @a.(1, 2, 3).should == [1, nil, [], nil, [], 2, nil, [3]] + result = @a.([1, 2, 3], [4, 5, 6, 7, 8], [9, 10]) + result.should == [1, 2, [], 3, [4, 5, 6], 7, 8, [9, 10]] + end + + evaluate <<-ruby do + @a = lambda do |a, (b, (c, *d, (e, (*f)), g), (h, (i, j)))| + [a, b, c, d, e, f, g, h, i, j] + end + ruby + + @a.(1, 2).should == [1, 2, nil, [], nil, [nil], nil, nil, nil, nil] + result = @a.(1, [2, [3, 4, 5, [6, [7, 8]], 9], [10, [11, 12]]]) + result.should == [1, 2, 3, [4, 5], 6, [7, 8], 9, 10, 11, 12] + end + + evaluate <<-ruby do + @a = lambda { |*, **k| k } + ruby + + @a.().should == {} + @a.(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} + + h = mock("keyword splat") + h.should_not_receive(:to_hash) + @a.(h).should == {} + end + + evaluate <<-ruby do + @a = lambda { |*, &b| b } + ruby + + @a.().should be_nil + @a.(1, 2, 3, 4).should be_nil + @a.(&(l = ->{})).should equal(l) + end + + evaluate <<-ruby do + @a = lambda { |a:, b:| [a, b] } + ruby + + @a.(a: 1, b: 2).should == [1, 2] + end + + evaluate <<-ruby do + @a = lambda { |a:, b: 1| [a, b] } + ruby + + @a.(a: 1).should == [1, 1] + @a.(a: 1, b: 2).should == [1, 2] + end + + evaluate <<-ruby do + @a = lambda { |a: 1, b:| [a, b] } + ruby + + @a.(b: 0).should == [1, 0] + @a.(b: 2, a: 3).should == [3, 2] + end + + evaluate <<-ruby do + @a = lambda do |a: (@a = -> (a: 1) { a }), b:| + [a, b] + end + ruby + + @a.(a: 2, b: 3).should == [2, 3] + @a.(b: 1).should == [@a, 1] + + # Note the default value of a: in the original method. + @a.().should == 1 + end + + evaluate <<-ruby do + @a = lambda { |a: 1, b: 2| [a, b] } + ruby + + @a.().should == [1, 2] + @a.(b: 3, a: 4).should == [4, 3] + end + + evaluate <<-ruby do + @a = lambda do |a, b=1, *c, (*d, (e)), f: 2, g:, h:, **k, &l| + [a, b, c, d, e, f, g, h, k, l] + end + ruby + + result = @a.(9, 8, 7, 6, f: 5, g: 4, h: 3, &(l = ->{})) + result.should == [9, 8, [7], [], 6, 5, 4, 3, {}, l] + end + + evaluate <<-ruby do + @a = lambda do |a, b=1, *c, d, e:, f: 2, g:, **k, &l| + [a, b, c, d, e, f, g, k, l] + end + ruby + + result = @a.(1, 2, e: 3, g: 4, h: 5, i: 6, &(l = ->{})) + result.should == [1, 1, [], 2, 3, 2, 4, { h: 5, i: 6 }, l] + end + end +end diff --git a/spec/ruby/language/line_spec.rb b/spec/ruby/language/line_spec.rb new file mode 100644 index 0000000000..fcadaa71d7 --- /dev/null +++ b/spec/ruby/language/line_spec.rb @@ -0,0 +1,45 @@ +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 + -> { eval("__LINE__ = 1") }.should raise_error(SyntaxError) + end + + before :each do + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "equals the line number of the text inside an eval" do + eval <<-EOC +ScratchPad << __LINE__ + +# line 3 + +ScratchPad << __LINE__ + EOC + + ScratchPad.recorded.should == [1, 5] + end +end + +describe "The __LINE__ pseudo-variable" do + it_behaves_like :language___LINE__, :require, CodeLoadingSpecs::Method.new +end + +describe "The __LINE__ pseudo-variable" do + it_behaves_like :language___LINE__, :require, Kernel +end + +describe "The __LINE__ pseudo-variable" do + it_behaves_like :language___LINE__, :load, CodeLoadingSpecs::Method.new +end + +describe "The __LINE__ pseudo-variable" do + it_behaves_like :language___LINE__, :load, Kernel +end diff --git a/spec/ruby/language/loop_spec.rb b/spec/ruby/language/loop_spec.rb new file mode 100644 index 0000000000..fd17b53910 --- /dev/null +++ b/spec/ruby/language/loop_spec.rb @@ -0,0 +1,67 @@ +require_relative '../spec_helper' + +describe "The loop expression" do + it "repeats the given block until a break is called" do + outer_loop = 0 + loop do + outer_loop += 1 + break if outer_loop == 10 + end + outer_loop.should == 10 + end + + it "executes code in its own scope" do + loop do + inner_loop = 123 + break + end + -> { inner_loop }.should raise_error(NameError) + end + + it "returns the value passed to break if interrupted by break" do + loop do + break 123 + end.should == 123 + end + + it "returns nil if interrupted by break with no arguments" do + loop do + break + end.should == nil + end + + it "skips to end of body with next" do + a = [] + i = 0 + loop do + break if (i+=1) >= 5 + next if i == 3 + a << i + end + a.should == [1, 2, 4] + end + + it "restarts the current iteration with redo" do + a = [] + loop do + a << 1 + redo if a.size < 2 + a << 2 + break if a.size == 3 + end + a.should == [1, 1, 2] + end + + it "uses a spaghetti nightmare of redo, next and break" do + a = [] + loop do + a << 1 + redo if a.size == 1 + a << 2 + next if a.size == 3 + a << 3 + break if a.size > 6 + end + a.should == [1, 1, 2, 1, 2, 3, 1, 2, 3] + end +end diff --git a/spec/ruby/language/magic_comment_spec.rb b/spec/ruby/language/magic_comment_spec.rb new file mode 100644 index 0000000000..af9c9dbfd0 --- /dev/null +++ b/spec/ruby/language/magic_comment_spec.rb @@ -0,0 +1,93 @@ +require_relative '../spec_helper' + +# 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 "are optional" do + @object.call('no_magic_comment.rb').should == @default.name + end + + it "are case-insensitive" do + @object.call('case_magic_comment.rb').should == Encoding::Big5.name + end + + it "must be at the first line" do + @object.call('second_line_magic_comment.rb').should == @default.name + end + + it "must be the first token of the line" do + @object.call('second_token_magic_comment.rb').should == @default.name + end + + it "can be after the shebang" do + @object.call('shebang_magic_comment.rb').should == Encoding::Big5.name + end + + it "can take Emacs style" do + @object.call('emacs_magic_comment.rb').should == Encoding::Big5.name + end + + it "can take vim style" do + @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 new file mode 100644 index 0000000000..ebf677cabc --- /dev/null +++ b/spec/ruby/language/match_spec.rb @@ -0,0 +1,81 @@ +require_relative '../spec_helper' +require_relative 'fixtures/match_operators' + +describe "The !~ operator" do + before :each do + @obj = OperatorImplementor.new + end + + it "evaluates as a call to !~" do + expected = "hello world" + + opval = (@obj !~ expected) + methodval = @obj.send(:"!~", expected) + + opval.should == expected + methodval.should == expected + end +end + +describe "The =~ operator" do + before :each do + @impl = OperatorImplementor.new + end + + it "calls the =~ method" do + expected = "hello world" + + opval = (@obj =~ expected) + methodval = @obj.send(:"=~", expected) + + opval.should == expected + methodval.should == expected + end +end + +describe "The =~ operator with named captures" do + before :each do + @regexp = /(?<matched>foo)(?<unmatched>bar)?/ + @string = "foofoo" + end + + describe "on syntax of /regexp/ =~ string_variable" do + it "sets local variables by the captured pairs" do + /(?<matched>foo)(?<unmatched>bar)?/ =~ @string + local_variables.should == [:matched, :unmatched] + matched.should == "foo" + unmatched.should == nil + 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)?/ + local_variables.should == [] + end + end + + describe "on syntax of regexp_variable =~ string_variable" do + it "does not set local variables" do + @regexp =~ @string + local_variables.should == [] + end + end + + describe "on the method calling" do + it "does not set local variables" do + @regexp.=~(@string) + local_variables.should == [] + + @regexp.send :=~, @string + local_variables.should == [] + end + end +end diff --git a/spec/ruby/language/metaclass_spec.rb b/spec/ruby/language/metaclass_spec.rb new file mode 100644 index 0000000000..fc83067977 --- /dev/null +++ b/spec/ruby/language/metaclass_spec.rb @@ -0,0 +1,143 @@ +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 + class << true; self; end.should == TrueClass + end + + it "is FalseClass for false" do + class << false; self; end.should == FalseClass + end + + it "is NilClass for nil" do + class << nil; self; end.should == NilClass + end + + it "raises a TypeError for numbers" do + -> { class << 1; self; end }.should raise_error(TypeError) + end + + it "raises a TypeError for symbols" do + -> { class << :symbol; self; end }.should raise_error(TypeError) + end + + it "is a singleton Class instance" do + cls = class << mock('x'); self; end + cls.is_a?(Class).should == true + cls.should_not equal(Object) + end +end + +describe "A constant on a metaclass" do + before :each do + @object = Object.new + class << @object + CONST = self + end + end + + it "can be accessed after the metaclass body is reopened" do + class << @object + CONST.should == self + end + end + + it "can be accessed via self::CONST" do + class << @object + self::CONST.should == self + end + end + + it "can be accessed via const_get" do + class << @object + const_get(:CONST).should == self + end + end + + it "is not defined on the object's class" do + @object.class.const_defined?(:CONST).should be_false + end + + it "is not defined in the metaclass opener's scope" do + class << @object + CONST + end + -> { CONST }.should raise_error(NameError) + end + + it "cannot be accessed via object::CONST" do + -> do + @object::CONST + end.should raise_error(TypeError) + end + + it "raises a NameError for anonymous_module::CONST" do + @object = Class.new + class << @object + CONST = 100 + end + + -> do + @object::CONST + end.should raise_error(NameError) + end + + it "appears in the metaclass constant list" do + constants = class << @object; constants; end + constants.should include(:CONST) + end + + it "does not appear in the object's class constant list" do + @object.class.constants.should_not include(:CONST) + end + + it "is not preserved when the object is duped" do + @object = @object.dup + + -> do + class << @object; CONST; end + end.should raise_error(NameError) + end + + it "is preserved when the object is cloned" do + @object = @object.clone + + class << @object + CONST.should_not be_nil + end + end +end + +describe "calling methods on the metaclass" do + + it "calls a method on the metaclass" do + MetaClassSpecs::A.cheese.should == 'edam' + MetaClassSpecs::B.cheese.should == 'stilton' + end + + it "calls a method on the instance's metaclass" do + b = MetaClassSpecs::B.new + b_meta = MetaClassSpecs.metaclass_of b + b_meta.send(:define_method, :cheese) {'cheshire'} + b.cheese.should == 'cheshire' + end + + it "calls a method in deeper chains of metaclasses" do + b = MetaClassSpecs::B.new + b_meta = MetaClassSpecs.metaclass_of b + b_meta_meta = MetaClassSpecs.metaclass_of b_meta + b_meta_meta.send(:define_method, :cheese) {'gouda'} + b_meta.cheese.should == 'gouda' + + b_meta_meta_meta = MetaClassSpecs.metaclass_of b_meta_meta + b_meta_meta_meta.send(:define_method, :cheese) {'wensleydale'} + b_meta_meta.cheese.should == 'wensleydale' + end + + it "calls a method defined on the metaclass of the metaclass" do + d_meta = MetaClassSpecs::D.singleton_class + d_meta.ham.should == 'iberico' + end +end diff --git a/spec/ruby/language/method_spec.rb b/spec/ruby/language/method_spec.rb new file mode 100644 index 0000000000..8f72bd45ed --- /dev/null +++ b/spec/ruby/language/method_spec.rb @@ -0,0 +1,1649 @@ +require_relative '../spec_helper' + +describe "A method send" do + evaluate <<-ruby do + def m(a) a end + ruby + + a = b = m 1 + a.should == 1 + b.should == 1 + end + + context "with a single splatted Object argument" do + before :all do + def m(a) a end + end + + it "does not call #to_ary" do + x = mock("splat argument") + x.should_not_receive(:to_ary) + + m(*x).should equal(x) + end + + it "calls #to_a" do + x = mock("splat argument") + x.should_receive(:to_a).and_return([1]) + + m(*x).should == 1 + end + + it "wraps the argument in an Array if #to_a returns nil" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(nil) + + m(*x).should == x + end + + it "raises a TypeError if #to_a does not return an Array" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(1) + + -> { m(*x) }.should raise_error(TypeError) + end + end + + context "with a leading splatted Object argument" do + before :all do + def m(a, b, *c, d, e) [a, b, c, d, e] end + end + + it "does not call #to_ary" do + x = mock("splat argument") + x.should_not_receive(:to_ary) + + m(*x, 1, 2, 3).should == [x, 1, [], 2, 3] + end + + it "calls #to_a" do + x = mock("splat argument") + x.should_receive(:to_a).and_return([1]) + + m(*x, 2, 3, 4).should == [1, 2, [], 3, 4] + end + + it "wraps the argument in an Array if #to_a returns nil" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(nil) + + m(*x, 2, 3, 4).should == [x, 2, [], 3, 4] + end + + it "raises a TypeError if #to_a does not return an Array" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(1) + + -> { m(*x, 2, 3) }.should raise_error(TypeError) + end + end + + context "with a middle splatted Object argument" do + before :all do + def m(a, b, *c, d, e) [a, b, c, d, e] end + end + + it "does not call #to_ary" do + x = mock("splat argument") + x.should_not_receive(:to_ary) + + m(1, 2, *x, 3, 4).should == [1, 2, [x], 3, 4] + end + + it "calls #to_a" do + x = mock("splat argument") + x.should_receive(:to_a).and_return([5, 6, 7]) + + m(1, 2, *x, 3).should == [1, 2, [5, 6], 7, 3] + end + + it "wraps the argument in an Array if #to_a returns nil" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(nil) + + m(1, 2, *x, 4).should == [1, 2, [], x, 4] + end + + it "raises a TypeError if #to_a does not return an Array" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(1) + + -> { m(1, *x, 2, 3) }.should raise_error(TypeError) + end + + it "copies the splatted array" do + args = [3, 4] + m(1, 2, *args, 4, 5).should == [1, 2, [3, 4], 4, 5] + m(1, 2, *args, 4, 5)[2].should_not equal(args) + end + + it "allows an array being splatted to be modified by another argument" do + args = [3, 4] + m(1, args.shift, *args, 4, 5).should == [1, 3, [4], 4, 5] + end + end + + context "with a trailing splatted Object argument" do + before :all do + def m(a, *b, c) [a, b, c] end + end + + it "does not call #to_ary" do + x = mock("splat argument") + x.should_not_receive(:to_ary) + + m(1, 2, *x).should == [1, [2], x] + end + + it "calls #to_a" do + x = mock("splat argument") + x.should_receive(:to_a).and_return([5, 6, 7]) + + m(1, 2, *x).should == [1, [2, 5, 6], 7] + end + + it "wraps the argument in an Array if #to_a returns nil" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(nil) + + m(1, 2, *x, 4).should == [1, [2, x], 4] + end + + it "raises a TypeError if #to_a does not return an Array" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(1) + + -> { 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 + +describe "An element assignment method send" do + before :each do + ScratchPad.clear + end + + context "with a single splatted Object argument" do + before :all do + @o = mock("element set receiver") + def @o.[]=(a, b) ScratchPad.record [a, b] end + end + + it "does not call #to_ary" do + x = mock("splat argument") + x.should_not_receive(:to_ary) + + (@o[*x] = 1).should == 1 + ScratchPad.recorded.should == [x, 1] + end + + it "calls #to_a" do + x = mock("splat argument") + x.should_receive(:to_a).and_return([1]) + + (@o[*x] = 2).should == 2 + ScratchPad.recorded.should == [1, 2] + end + + it "wraps the argument in an Array if #to_a returns nil" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(nil) + + (@o[*x] = 1).should == 1 + ScratchPad.recorded.should == [x, 1] + end + + it "raises a TypeError if #to_a does not return an Array" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(1) + + -> { @o[*x] = 1 }.should raise_error(TypeError) + end + end + + context "with a leading splatted Object argument" do + before :all do + @o = mock("element set receiver") + def @o.[]=(a, b, *c, d, e) ScratchPad.record [a, b, c, d, e] end + end + + it "does not call #to_ary" do + x = mock("splat argument") + x.should_not_receive(:to_ary) + + (@o[*x, 2, 3, 4] = 1).should == 1 + ScratchPad.recorded.should == [x, 2, [3], 4, 1] + end + + it "calls #to_a" do + x = mock("splat argument") + x.should_receive(:to_a).and_return([1, 2, 3]) + + (@o[*x, 4, 5] = 6).should == 6 + ScratchPad.recorded.should == [1, 2, [3, 4], 5, 6] + end + + it "wraps the argument in an Array if #to_a returns nil" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(nil) + + (@o[*x, 2, 3, 4] = 5).should == 5 + ScratchPad.recorded.should == [x, 2, [3], 4, 5] + end + + it "raises a TypeError if #to_a does not return an Array" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(1) + + -> { @o[*x, 2, 3] = 4 }.should raise_error(TypeError) + end + end + + context "with a middle splatted Object argument" do + before :all do + @o = mock("element set receiver") + def @o.[]=(a, b, *c, d, e) ScratchPad.record [a, b, c, d, e] end + end + + it "does not call #to_ary" do + x = mock("splat argument") + x.should_not_receive(:to_ary) + + (@o[1, *x, 2, 3] = 4).should == 4 + ScratchPad.recorded.should == [1, x, [2], 3, 4] + end + + it "calls #to_a" do + x = mock("splat argument") + x.should_receive(:to_a).and_return([2, 3]) + + (@o[1, *x, 4] = 5).should == 5 + ScratchPad.recorded.should == [1, 2, [3], 4, 5] + end + + it "wraps the argument in an Array if #to_a returns nil" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(nil) + + (@o[1, 2, *x, 3] = 4).should == 4 + ScratchPad.recorded.should == [1, 2, [x], 3, 4] + end + + it "raises a TypeError if #to_a does not return an Array" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(1) + + -> { @o[1, 2, *x, 3] = 4 }.should raise_error(TypeError) + end + end + + context "with a trailing splatted Object argument" do + before :all do + @o = mock("element set receiver") + def @o.[]=(a, b, *c, d, e) ScratchPad.record [a, b, c, d, e] end + end + + it "does not call #to_ary" do + x = mock("splat argument") + x.should_not_receive(:to_ary) + + (@o[1, 2, 3, 4, *x] = 5).should == 5 + ScratchPad.recorded.should == [1, 2, [3, 4], x, 5] + end + + it "calls #to_a" do + x = mock("splat argument") + x.should_receive(:to_a).and_return([4, 5]) + + (@o[1, 2, 3, *x] = 6).should == 6 + ScratchPad.recorded.should == [1, 2, [3, 4], 5, 6] + end + + it "wraps the argument in an Array if #to_a returns nil" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(nil) + + (@o[1, 2, 3, *x] = 4).should == 4 + ScratchPad.recorded.should == [1, 2, [3], x, 4] + end + + it "raises a TypeError if #to_a does not return an Array" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(1) + + -> { @o[1, 2, 3, *x] = 4 }.should raise_error(TypeError) + end + end +end + +describe "An attribute assignment method send" do + context "with a single splatted Object argument" do + before :all do + @o = mock("element set receiver") + def @o.m=(a, b) [a, b] end + end + + it "does not call #to_ary" do + x = mock("splat argument") + x.should_not_receive(:to_ary) + + (@o.send :m=, *x, 1).should == [x, 1] + end + + it "calls #to_a" do + x = mock("splat argument") + x.should_receive(:to_a).and_return([1]) + + (@o.send :m=, *x, 2).should == [1, 2] + end + + it "wraps the argument in an Array if #to_a returns nil" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(nil) + + (@o.send :m=, *x, 1).should == [x, 1] + end + + it "raises a TypeError if #to_a does not return an Array" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(1) + + -> { @o.send :m=, *x, 1 }.should raise_error(TypeError) + end + end + + context "with a leading splatted Object argument" do + before :all do + @o = mock("element set receiver") + def @o.m=(a, b, *c, d, e) [a, b, c, d, e] end + end + + it "does not call #to_ary" do + x = mock("splat argument") + x.should_not_receive(:to_ary) + + (@o.send :m=, *x, 2, 3, 4, 1).should == [x, 2, [3], 4, 1] + end + + it "calls #to_a" do + x = mock("splat argument") + x.should_receive(:to_a).and_return([1, 2, 3]) + + (@o.send :m=, *x, 4, 5, 6).should == [1, 2, [3, 4], 5, 6] + end + + it "wraps the argument in an Array if #to_a returns nil" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(nil) + + (@o.send :m=, *x, 2, 3, 4, 5).should == [x, 2, [3], 4, 5] + end + + it "raises a TypeError if #to_a does not return an Array" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(1) + + -> { @o.send :m=, *x, 2, 3, 4 }.should raise_error(TypeError) + end + end + + context "with a middle splatted Object argument" do + before :all do + @o = mock("element set receiver") + def @o.m=(a, b, *c, d, e) [a, b, c, d, e] end + end + + it "does not call #to_ary" do + x = mock("splat argument") + x.should_not_receive(:to_ary) + + (@o.send :m=, 1, *x, 2, 3, 4).should == [1, x, [2], 3, 4] + end + + it "calls #to_a" do + x = mock("splat argument") + x.should_receive(:to_a).and_return([2, 3]) + + (@o.send :m=, 1, *x, 4, 5).should == [1, 2, [3], 4, 5] + end + + it "wraps the argument in an Array if #to_a returns nil" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(nil) + + (@o.send :m=, 1, 2, *x, 3, 4).should == [1, 2, [x], 3, 4] + end + + it "raises a TypeError if #to_a does not return an Array" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(1) + + -> { @o.send :m=, 1, 2, *x, 3, 4 }.should raise_error(TypeError) + end + end + + context "with a trailing splatted Object argument" do + before :all do + @o = mock("element set receiver") + def @o.m=(a, b, *c, d, e) [a, b, c, d, e] end + end + + it "does not call #to_ary" do + x = mock("splat argument") + x.should_not_receive(:to_ary) + + (@o.send :m=, 1, 2, 3, 4, *x, 5).should == [1, 2, [3, 4], x, 5] + end + + it "calls #to_a" do + x = mock("splat argument") + x.should_receive(:to_a).and_return([4, 5]) + + (@o.send :m=, 1, 2, 3, *x, 6).should == [1, 2, [3, 4], 5, 6] + end + + it "wraps the argument in an Array if #to_a returns nil" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(nil) + + (@o.send :m=, 1, 2, 3, *x, 4).should == [1, 2, [3], x, 4] + end + + it "raises a TypeError if #to_a does not return an Array" do + x = mock("splat argument") + x.should_receive(:to_a).and_return(1) + + -> { @o.send :m=, 1, 2, 3, *x, 4 }.should raise_error(TypeError) + end + end +end + +describe "A method" do + SpecEvaluate.desc = "for definition" + + context "assigns no local variables" do + evaluate <<-ruby do + def m + end + ruby + + m.should be_nil + end + + evaluate <<-ruby do + def m() + end + ruby + + m.should be_nil + end + end + + context "assigns local variables from method parameters" do + evaluate <<-ruby do + def m(a) a end + ruby + + m((args = 1, 2, 3)).should equal(args) + end + + evaluate <<-ruby do + def m((a)) a end + ruby + + m(1).should == 1 + m([1, 2, 3]).should == 1 + end + + evaluate <<-ruby do + def m((*a, b)) [a, b] end + ruby + + m(1).should == [[], 1] + m([1, 2, 3]).should == [[1, 2], 3] + end + + evaluate <<-ruby do + def m(a=1) a end + ruby + + m().should == 1 + m(2).should == 2 + 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 + + m().should be_nil + m(1).should be_nil + m(1, 2, 3).should be_nil + end + + evaluate <<-ruby do + def m(*a) a end + ruby + + 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 + + -> { m() }.should raise_error(ArgumentError) + m(a: 1).should == 1 + 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 + def m(a: 1) a end + ruby + + m().should == 1 + m(a: 2).should == 2 + end + + evaluate <<-ruby do + def m(**) end + ruby + + m().should be_nil + m(a: 1, b: 2).should be_nil + -> { m(1) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + def m(**k) k end + ruby + + m().should == {} + m(a: 1, b: 2).should == { a: 1, b: 2 } + 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 + def m(&b) b end + ruby + + m { }.should be_an_instance_of(Proc) + end + + evaluate <<-ruby do + def m(a, b) [a, b] end + ruby + + m(1, 2).should == [1, 2] + end + + evaluate <<-ruby do + def m(a, (b, c)) [a, b, c] end + ruby + + m(1, 2).should == [1, 2, nil] + m(1, [2, 3, 4]).should == [1, 2, 3] + end + + evaluate <<-ruby do + def m((a), (b)) [a, b] end + ruby + + m(1, 2).should == [1, 2] + m([1, 2], [3, 4]).should == [1, 3] + end + + evaluate <<-ruby do + def m((*), (*)) end + ruby + + m(2, 3).should be_nil + m([2, 3, 4], [5, 6]).should be_nil + -> { m a: 1 }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + def m((*a), (*b)) [a, b] end + ruby + + m(1, 2).should == [[1], [2]] + m([1, 2], [3, 4]).should == [[1, 2], [3, 4]] + end + + evaluate <<-ruby do + def m((a, b), (c, d)) + [a, b, c, d] + end + ruby + + m(1, 2).should == [1, nil, 2, nil] + m([1, 2, 3], [4, 5, 6]).should == [1, 2, 4, 5] + end + + evaluate <<-ruby do + def m((a, *b), (*c, d)) + [a, b, c, d] + end + ruby + + m(1, 2).should == [1, [], [], 2] + m([1, 2, 3], [4, 5, 6]).should == [1, [2, 3], [4, 5], 6] + end + + evaluate <<-ruby do + def m((a, b, *c, d), (*e, f, g), (*h)) + [a, b, c, d, e, f, g, h] + end + ruby + + m(1, 2, 3).should == [1, nil, [], nil, [], 2, nil, [3]] + result = m([1, 2, 3], [4, 5, 6, 7, 8], [9, 10]) + result.should == [1, 2, [], 3, [4, 5, 6], 7, 8, [9, 10]] + end + + evaluate <<-ruby do + def m(a, (b, (c, *d), *e)) + [a, b, c, d, e] + end + ruby + + m(1, 2).should == [1, 2, nil, [], []] + m(1, [2, [3, 4, 5], 6, 7, 8]).should == [1, 2, 3, [4, 5], [6, 7, 8]] + end + + evaluate <<-ruby do + def m(a, (b, (c, *d, (e, (*f)), g), (h, (i, j)))) + [a, b, c, d, e, f, g, h, i, j] + end + ruby + + m(1, 2).should == [1, 2, nil, [], nil, [nil], nil, nil, nil, nil] + result = m(1, [2, [3, 4, 5, [6, [7, 8]], 9], [10, [11, 12]]]) + result.should == [1, 2, 3, [4, 5], 6, [7, 8], 9, 10, 11, 12] + end + + evaluate <<-ruby do + def m(a, b=1) [a, b] end + ruby + + m(2).should == [2, 1] + m(1, 2).should == [1, 2] + end + + evaluate <<-ruby do + def m(a, *) a end + ruby + + m(1).should == 1 + m(1, 2, 3).should == 1 + end + + evaluate <<-ruby do + def m(a, *b) [a, b] end + ruby + + m(1).should == [1, []] + m(1, 2, 3).should == [1, [2, 3]] + end + + evaluate <<-ruby do + def m(a, b:) [a, b] end + ruby + + m(1, b: 2).should == [1, 2] + suppress_keyword_warning do + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) + end + end + + evaluate <<-ruby do + def m(a, b: 1) [a, b] end + ruby + + m(2).should == [2, 1] + m(1, b: 2).should == [1, 2] + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + def m(a, **) a end + ruby + + m(1).should == 1 + m(1, a: 2, b: 3).should == 1 + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + def m(a, **k) [a, k] end + ruby + + m(1).should == [1, {}] + m(1, a: 2, b: 3).should == [1, {a: 2, b: 3}] + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + def m(a, &b) [a, b] end + ruby + + m(1).should == [1, nil] + m(1, &(l = -> {})).should == [1, l] + end + + evaluate <<-ruby do + def m(a=1, b) [a, b] end + ruby + + m(2).should == [1, 2] + m(2, 3).should == [2, 3] + end + + evaluate <<-ruby do + def m(a=1, *) a end + ruby + + m().should == 1 + m(2, 3, 4).should == 2 + end + + evaluate <<-ruby do + def m(a=1, *b) [a, b] end + ruby + + m().should == [1, []] + m(2, 3, 4).should == [2, [3, 4]] + end + + evaluate <<-ruby do + def m(a=1, (b, c)) [a, b, c] end + ruby + + m(2).should == [1, 2, nil] + m(2, 3).should == [2, 3, nil] + m(2, [3, 4, 5]).should == [2, 3, 4] + end + + evaluate <<-ruby do + def m(a=1, (b, (c, *d))) [a, b, c, d] end + ruby + + m(2).should == [1, 2, nil, []] + m(2, 3).should == [2, 3, nil, []] + m(2, [3, [4, 5, 6], 7]).should == [2, 3, 4, [5, 6]] + end + + evaluate <<-ruby do + def m(a=1, (b, (c, *d), *e)) [a, b, c, d, e] end + ruby + + m(2).should == [1, 2, nil, [], []] + m(2, [3, 4, 5, 6]).should == [2, 3, 4, [], [5, 6]] + m(2, [3, [4, 5, 6], 7]).should == [2, 3, 4, [5, 6], [7]] + end + + evaluate <<-ruby do + def m(a=1, (b), (c)) [a, b, c] end + ruby + + m(2, 3).should == [1, 2, 3] + m(2, 3, 4).should == [2, 3, 4] + m(2, [3, 4], [5, 6, 7]).should == [2, 3, 5] + end + + evaluate <<-ruby do + def m(a=1, (*b), (*c)) [a, b, c] end + ruby + + -> { 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 + + evaluate <<-ruby do + def m(a=1, (b, c), (d, e)) [a, b, c, d, e] end + ruby + + m(2, 3).should == [1, 2, nil, 3, nil] + m(2, [3, 4, 5], [6, 7, 8]).should == [2, 3, 4, 6, 7] + end + + evaluate <<-ruby do + def m(a=1, (b, *c), (*d, e)) + [a, b, c, d, e] + end + ruby + + m(1, 2).should == [1, 1, [], [], 2] + m(1, [2, 3], [4, 5, 6]).should == [1, 2, [3], [4, 5], 6] + end + + evaluate <<-ruby do + def m(a=1, (b, *c), (d, (*e, f))) + [a, b, c, d, e, f] + end + ruby + + m(1, 2).should == [1, 1, [], 2, [], nil] + m(nil, nil).should == [1, nil, [], nil, [], nil] + result = m([1, 2, 3], [4, 5, 6], [7, 8, 9]) + result.should == [[1, 2, 3], 4, [5, 6], 7, [], 8] + end + + evaluate <<-ruby do + def m(a=1, b:) [a, b] end + ruby + + m(b: 2).should == [1, 2] + m(2, b: 1).should == [2, 1] + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + def m(a=1, b: 2) [a, b] end + ruby + + m().should == [1, 2] + m(2).should == [2, 2] + m(b: 3).should == [1, 3] + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + def m(a=1, **) a end + ruby + + m().should == 1 + m(2, a: 1, b: 0).should == 2 + m("a" => 1, a: 2).should == 1 + end + + evaluate <<-ruby do + def m(a=1, **k) [a, k] end + ruby + + m().should == [1, {}] + m(2, a: 1, b: 2).should == [2, {a: 1, b: 2}] + end + + evaluate <<-ruby do + def m(a=1, &b) [a, b] end + ruby + + m().should == [1, nil] + m(&(l = -> {})).should == [1, l] + + p = -> {} + l = mock("to_proc") + l.should_receive(:to_proc).and_return(p) + m(&l).should == [1, p] + end + + evaluate <<-ruby do + def m(*, a) a end + ruby + + m(1).should == 1 + m(1, 2, 3).should == 3 + end + + evaluate <<-ruby do + def m(*a, b) [a, b] end + ruby + + m(1).should == [[], 1] + m(1, 2, 3).should == [[1, 2], 3] + end + + evaluate <<-ruby do + def m(*, &b) b end + ruby + + m().should be_nil + m(1, 2, 3, 4).should be_nil + m(&(l = ->{})).should equal(l) + end + + evaluate <<-ruby do + def m(*a, &b) [a, b] end + ruby + + m().should == [[], nil] + m(1).should == [[1], nil] + m(1, 2, 3, &(l = -> {})).should == [[1, 2, 3], l] + end + + evaluate <<-ruby do + def m(a:, b:) [a, b] end + ruby + + m(a: 1, b: 2).should == [1, 2] + suppress_keyword_warning do + -> { m("a" => 1, a: 1, b: 2) }.should raise_error(ArgumentError) + end + end + + evaluate <<-ruby do + def m(a:, b: 1) [a, b] end + ruby + + m(a: 1).should == [1, 1] + m(a: 1, b: 2).should == [1, 2] + suppress_keyword_warning do + -> { m("a" => 1, a: 1, b: 2) }.should raise_error(ArgumentError) + end + end + + evaluate <<-ruby do + def m(a:, **) a end + ruby + + m(a: 1).should == 1 + m(a: 1, b: 2).should == 1 + m("a" => 1, a: 1, b: 2).should == 1 + end + + evaluate <<-ruby do + def m(a:, **k) [a, k] end + ruby + + m(a: 1).should == [1, {}] + m(a: 1, b: 2, c: 3).should == [1, {b: 2, c: 3}] + m("a" => 1, a: 1, b: 2).should == [1, {"a" => 1, b: 2}] + end + + evaluate <<-ruby do + def m(a:, &b) [a, b] end + ruby + + m(a: 1).should == [1, nil] + m(a: 1, &(l = ->{})).should == [1, l] + end + + evaluate <<-ruby do + def m(a: 1, b:) [a, b] end + ruby + + m(b: 0).should == [1, 0] + m(b: 2, a: 3).should == [3, 2] + end + + evaluate <<-ruby do + def m(a: def m(a: 1) a end, b:) + [a, b] + end + ruby + + m(a: 2, b: 3).should == [2, 3] + m(b: 1).should == [:m, 1] + + # Note the default value of a: in the original method. + m().should == 1 + end + + evaluate <<-ruby do + def m(a: 1, b: 2) [a, b] end + ruby + + m().should == [1, 2] + m(b: 3, a: 4).should == [4, 3] + end + + evaluate <<-ruby do + def m(a: 1, **) a end + ruby + + m().should == 1 + m(a: 2, b: 1).should == 2 + end + + evaluate <<-ruby do + def m(a: 1, **k) [a, k] end + ruby + + m(b: 2, c: 3).should == [1, {b: 2, c: 3}] + end + + evaluate <<-ruby do + def m(a: 1, &b) [a, b] end + ruby + + m(&(l = ->{})).should == [1, l] + m().should == [1, nil] + end + + evaluate <<-ruby do + def m(**, &b) b end + ruby + + m(a: 1, b: 2, &(l = ->{})).should == l + end + + evaluate <<-ruby do + def m(**k, &b) [k, b] end + ruby + + m(a: 1, b: 2).should == [{ a: 1, b: 2}, nil] + end + + evaluate <<-ruby do + def m(a, b=1, *c, (*d, (e)), f: 2, g:, h:, **k, &l) + [a, b, c, d, e, f, g, h, k, l] + end + ruby + + result = m(9, 8, 7, 6, f: 5, g: 4, h: 3, &(l = ->{})) + result.should == [9, 8, [7], [], 6, 5, 4, 3, {}, l] + end + + evaluate <<-ruby do + def m(a, b=1, *c, d, e:, f: 2, g:, **k, &l) + [a, b, c, d, e, f, g, k, l] + end + ruby + + result = m(1, 2, e: 3, g: 4, h: 5, i: 6, &(l = ->{})) + result.should == [1, 1, [], 2, 3, 2, 4, { h: 5, i: 6 }, l] + end + + evaluate <<-ruby do + 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 + + result = m(1, 2) + result.should == [1, nil, nil, 2, nil, {}] + + result = m(1, 2, {foo: :bar}) + result.should == [1, 2, nil, {foo: :bar}, nil, {}] + + result = m(1, {foo: :bar}) + result.should == [1, nil, nil, {foo: :bar}, nil, {}] + end + end + + 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 + before(:each) do + def m(*args) + args + end + + def n(value, &block) + [value, block.call] + end + end + + context "when no arguments provided" do + it "assigns nil" do + args = m () + args.should == [nil] + end + end + + 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 multiple arguments are provided" do + it "assigns simple expressions" do + args = m (1), (2) + args.should == [1, 2] + end + + it "assigns expressions consisting of multiple statements" do + args = m ((0; 1)), ((2; 3)) + args.should == [1, 3] + end + end + + context "when the argument looks like an argument list" do + it "raises a syntax error" do + -> { + eval("m (1, 2)") + }.should raise_error(SyntaxError) + + -> { + eval("m (1, 2, 3)") + }.should raise_error(SyntaxError) + end + end + + it "allows to pass a block with curly braces" do + args = n () { :block_value } + args.should == [nil, :block_value] + + args = n (1) { :block_value } + args.should == [1, :block_value] + end + + it "allows to pass a block with do/end" do + args = n () do + :block_value + end + args.should == [nil, :block_value] + + args = n (1) do + :block_value + end + args.should == [1, :block_value] + end +end + +describe "An array-dereference method ([])" do + SpecEvaluate.desc = "for definition" + + context "received the passed-in block" do + evaluate <<-ruby do + def [](*, &b) + b.call + end + ruby + pr = proc {:ok} + + self[&pr].should == :ok + self['foo', &pr].should == :ok + self.[](&pr).should == :ok + self.[]('foo', &pr).should == :ok + end + + evaluate <<-ruby do + def [](*) + yield + end + ruby + pr = proc {:ok} + + self[&pr].should == :ok + self['foo', &pr].should == :ok + self.[](&pr).should == :ok + self.[]('foo', &pr).should == :ok + 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 new file mode 100644 index 0000000000..fba4aa8c6e --- /dev/null +++ b/spec/ruby/language/module_spec.rb @@ -0,0 +1,123 @@ +require_relative '../spec_helper' +require_relative 'fixtures/module' + +describe "The module keyword" do + it "creates a new module without semicolon" do + module ModuleSpecsKeywordWithoutSemicolon end + ModuleSpecsKeywordWithoutSemicolon.should be_an_instance_of(Module) + end + + it "creates a new module with a non-qualified constant name" do + module ModuleSpecsToplevel; end + ModuleSpecsToplevel.should be_an_instance_of(Module) + end + + it "creates a new module with a qualified constant name" do + module ModuleSpecs::Nested; end + ModuleSpecs::Nested.should be_an_instance_of(Module) + end + + it "creates a new module with a variable qualified constant name" do + m = Module.new + module m::N; end + m::N.should be_an_instance_of(Module) + end + + it "reopens an existing module" do + module ModuleSpecs; Reopened = true; end + ModuleSpecs::Reopened.should be_true + ensure + ModuleSpecs.send(:remove_const, :Reopened) + end + + it "does not reopen a module included in Object" do + ruby_exe(<<~RUBY).should == "false" + module IncludedInObject + module IncludedModule; end + end + class Object + include IncludedInObject + end + module IncludedModule; end + print IncludedInObject::IncludedModule == Object::IncludedModule + RUBY + end + + it "does not reopen a module included in non-Object modules" do + ruby_exe(<<~RUBY).should == "false/false" + module Included + module IncludedModule; end + end + module M + include Included + module IncludedModule; end + end + class C + include Included + module IncludedModule; end + end + print Included::IncludedModule == M::IncludedModule, "/", + Included::IncludedModule == C::IncludedModule + RUBY + end + + it "raises a TypeError if the constant is a Class" do + -> do + module ModuleSpecs::Modules::Klass; end + end.should raise_error(TypeError) + end + + it "raises a TypeError if the constant is a String" do + -> { module ModuleSpecs::Modules::A; end }.should raise_error(TypeError) + end + + 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 + -> { module ModuleSpecs::Modules::C; end }.should raise_error(TypeError) + end + + it "raises a TypeError if the constant is true" do + -> { module ModuleSpecs::Modules::D; end }.should raise_error(TypeError) + end + + it "raises a TypeError if the constant is false" do + -> { module ModuleSpecs::Modules::D; end }.should raise_error(TypeError) + end +end + +describe "Assigning an anonymous module to a constant" do + it "sets the name of the module" do + mod = Module.new + mod.name.should be_nil + + ::ModuleSpecs_CS1 = mod + mod.name.should == "ModuleSpecs_CS1" + ensure + Object.send(:remove_const, :ModuleSpecs_CS1) + end + + it "sets the name of a module scoped by an anonymous module" do + a, b = Module.new, Module.new + a::B = b + b.name.should.end_with? '::B' + end + + it "sets the name of contained modules when assigning a toplevel anonymous module" do + a, b, c, d = Module.new, Module.new, Module.new, Module.new + a::B = b + a::B::C = c + a::B::C::E = c + a::D = d + + ::ModuleSpecs_CS2 = a + a.name.should == "ModuleSpecs_CS2" + 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 new file mode 100644 index 0000000000..6fbfc4a54d --- /dev/null +++ b/spec/ruby/language/next_spec.rb @@ -0,0 +1,410 @@ +require_relative '../spec_helper' +require_relative 'fixtures/next' + +describe "The next statement from within the block" do + before :each do + ScratchPad.record [] + end + + it "ends block execution" do + a = [] + -> { + a << 1 + next + a << 2 + }.call + a.should == [1] + end + + it "causes block to return nil if invoked without arguments" do + -> { 123; next; 456 }.call.should == nil + end + + it "causes block to return nil if invoked with an empty expression" do + -> { next (); 456 }.call.should be_nil + end + + it "returns the argument passed" do + -> { 123; next 234; 345 }.call.should == 234 + end + + it "returns to the invoking method" do + NextSpecs.yielding_method(nil) { next }.should == :method_return_value + end + + it "returns to the invoking method, with the specified value" do + NextSpecs.yielding_method(nil) { + next nil; + fail("next didn't end the block execution") + }.should == :method_return_value + + NextSpecs.yielding_method(1) { + next 1 + fail("next didn't end the block execution") + }.should == :method_return_value + + NextSpecs.yielding_method([1, 2, 3]) { + next 1, 2, 3 + fail("next didn't end the block execution") + }.should == :method_return_value + end + + it "returns to the currently yielding method in case of chained calls" do + class ChainedNextTest + def self.meth_with_yield(&b) + yield.should == :next_return_value + :method_return_value + end + def self.invoking_method(&b) + meth_with_yield(&b) + end + def self.enclosing_method + invoking_method do + next :next_return_value + :wrong_return_value + end + end + end + + ChainedNextTest.enclosing_method.should == :method_return_value + end + + it "causes ensure blocks to run" do + [1].each do |i| + begin + ScratchPad << :begin + next + ensure + ScratchPad << :ensure + end + end + + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "skips following code outside an exception block" do + 3.times do |i| + begin + ScratchPad << :begin + next if i == 0 + break if i == 2 + ScratchPad << :begin_end + ensure + ScratchPad << :ensure + end + + ScratchPad << :after + end + + ScratchPad.recorded.should == [ + :begin, :ensure, :begin, :begin_end, :ensure, :after, :begin, :ensure] + end + + it "passes the value returned by a method with omitted parenthesis and passed block" do + obj = NextSpecs::Block.new + -> { 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 + -> { + eval("def m; next; end") + }.should raise_error(SyntaxError) + end + end +end + +describe "The next statement" do + before :each do + ScratchPad.record [] + end + + describe "in a while loop" do + describe "when not passed an argument" do + it "causes ensure blocks to run" do + NextSpecs.while_next(false) + + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "causes ensure blocks to run when nested in an block" do + NextSpecs.while_within_iter(false) + + ScratchPad.recorded.should == [:begin, :ensure] + end + end + + describe "when passed an argument" do + it "causes ensure blocks to run" do + NextSpecs.while_next(true) + + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "causes ensure blocks to run when nested in an block" do + NextSpecs.while_within_iter(true) + + ScratchPad.recorded.should == [:begin, :ensure] + end + end + + it "causes nested ensure blocks to run" do + x = true + while x + begin + ScratchPad << :outer_begin + x = false + begin + ScratchPad << :inner_begin + next + ensure + ScratchPad << :inner_ensure + end + ensure + ScratchPad << :outer_ensure + end + end + + ScratchPad.recorded.should == [:outer_begin, :inner_begin, :inner_ensure, :outer_ensure] + end + + it "causes ensure blocks to run when mixed with break" do + x = 1 + while true + begin + ScratchPad << :begin + break if x > 1 + x += 1 + next + ensure + ScratchPad << :ensure + end + end + + ScratchPad.recorded.should == [:begin, :ensure, :begin, :ensure] + end + end + + describe "in an until loop" do + describe "when not passed an argument" do + it "causes ensure blocks to run" do + NextSpecs.until_next(false) + + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "causes ensure blocks to run when nested in an block" do + NextSpecs.until_within_iter(false) + + ScratchPad.recorded.should == [:begin, :ensure] + end + end + + describe "when passed an argument" do + it "causes ensure blocks to run" do + NextSpecs.until_next(true) + + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "causes ensure blocks to run when nested in an block" do + NextSpecs.until_within_iter(true) + + ScratchPad.recorded.should == [:begin, :ensure] + end + end + + it "causes nested ensure blocks to run" do + x = false + until x + begin + ScratchPad << :outer_begin + x = true + begin + ScratchPad << :inner_begin + next + ensure + ScratchPad << :inner_ensure + end + ensure + ScratchPad << :outer_ensure + end + end + + ScratchPad.recorded.should == [:outer_begin, :inner_begin, :inner_ensure, :outer_ensure] + end + + it "causes ensure blocks to run when mixed with break" do + x = 1 + until false + begin + ScratchPad << :begin + break if x > 1 + x += 1 + next + ensure + ScratchPad << :ensure + end + end + + ScratchPad.recorded.should == [:begin, :ensure, :begin, :ensure] + end + end + + describe "in a loop" do + describe "when not passed an argument" do + it "causes ensure blocks to run" do + NextSpecs.loop_next(false) + + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "causes ensure blocks to run when nested in an block" do + NextSpecs.loop_within_iter(false) + + ScratchPad.recorded.should == [:begin, :ensure] + end + end + + describe "when passed an argument" do + it "causes ensure blocks to run" do + NextSpecs.loop_next(true) + + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "causes ensure blocks to run when nested in an block" do + NextSpecs.loop_within_iter(true) + + ScratchPad.recorded.should == [:begin, :ensure] + end + end + + it "causes nested ensure blocks to run" do + x = 1 + loop do + break if x == 2 + + begin + ScratchPad << :outer_begin + begin + ScratchPad << :inner_begin + x += 1 + next + ensure + ScratchPad << :inner_ensure + end + ensure + ScratchPad << :outer_ensure + end + end + + ScratchPad.recorded.should == [:outer_begin, :inner_begin, :inner_ensure, :outer_ensure] + end + + it "causes ensure blocks to run when mixed with break" do + x = 1 + loop do + begin + ScratchPad << :begin + break if x > 1 + x += 1 + next + ensure + ScratchPad << :ensure + end + end + + ScratchPad.recorded.should == [:begin, :ensure, :begin, :ensure] + end + end +end + +describe "Assignment via next" do + it "assigns objects" do + def r(val); a = yield(); val.should == a; end + r(nil){next} + r(nil){next nil} + r(1){next 1} + r([]){next []} + r([1]){next [1]} + r([nil]){next [nil]} + r([[]]){next [[]]} + r([]){next [*[]]} + r([1]){next [*[1]]} + r([1,2]){next [*[1,2]]} + end + + it "assigns splatted objects" do + def r(val); a = yield(); val.should == a; end + r([]){next *nil} + r([1]){next *1} + r([]){next *[]} + r([1]){next *[1]} + r([nil]){next *[nil]} + r([[]]){next *[[]]} + r([]){next *[*[]]} + r([1]){next *[*[1]]} + r([1,2]){next *[*[1,2]]} + end + + it "assigns objects to a splatted reference" do + def r(val); *a = yield(); val.should == a; end + r([nil]){next} + r([nil]){next nil} + r([1]){next 1} + r([]){next []} + r([1]){next [1]} + r([nil]){next [nil]} + r([[]]){next [[]]} + r([1,2]){next [1,2]} + r([]){next [*[]]} + r([1]){next [*[1]]} + r([1,2]){next [*[1,2]]} + end + + it "assigns splatted objects to a splatted reference via a splatted yield" do + def r(val); *a = *yield(); val.should == a; end + r([]){next *nil} + r([1]){next *1} + r([]){next *[]} + r([1]){next *[1]} + r([nil]){next *[nil]} + r([[]]){next *[[]]} + r([1,2]){next *[1,2]} + r([]){next *[*[]]} + r([1]){next *[*[1]]} + r([1,2]){next *[*[1,2]]} + end + + it "assigns objects to multiple variables" do + def r(val); a,b,*c = yield(); val.should == [a,b,c]; end + r([nil,nil,[]]){next} + r([nil,nil,[]]){next nil} + r([1,nil,[]]){next 1} + r([nil,nil,[]]){next []} + r([1,nil,[]]){next [1]} + r([nil,nil,[]]){next [nil]} + r([[],nil,[]]){next [[]]} + r([1,2,[]]){next [1,2]} + r([nil,nil,[]]){next [*[]]} + r([1,nil,[]]){next [*[1]]} + r([1,2,[]]){next [*[1,2]]} + end + + it "assigns splatted objects to multiple variables" do + def r(val); a,b,*c = *yield(); val.should == [a,b,c]; end + r([nil,nil,[]]){next *nil} + r([1,nil,[]]){next *1} + r([nil,nil,[]]){next *[]} + r([1,nil,[]]){next *[1]} + r([nil,nil,[]]){next *[nil]} + r([[],nil,[]]){next *[[]]} + r([1,2,[]]){next *[1,2]} + r([nil,nil,[]]){next *[*[]]} + r([1,nil,[]]){next *[*[1]]} + r([1,2,[]]){next *[*[1,2]]} + end +end diff --git a/spec/ruby/language/not_spec.rb b/spec/ruby/language/not_spec.rb new file mode 100644 index 0000000000..052af9b256 --- /dev/null +++ b/spec/ruby/language/not_spec.rb @@ -0,0 +1,51 @@ +require_relative '../spec_helper' + +describe "The not keyword" do + it "negates a `true' value" do + (not true).should be_false + (not 'true').should be_false + end + + it "negates a `false' value" do + (not false).should be_true + (not nil).should be_true + end + + it "accepts an argument" do + not(true).should be_false + end + + it "returns false if the argument is true" do + (not(true)).should be_false + end + + it "returns true if the argument is false" do + (not(false)).should be_true + end + + it "returns true if the argument is nil" do + (not(nil)).should be_true + end +end + +describe "The `!' keyword" do + it "negates a `true' value" do + (!true).should be_false + (!'true').should be_false + end + + it "negates a `false' value" do + (!false).should be_true + (!nil).should be_true + end + + it "doubled turns a truthful object into `true'" do + (!!true).should be_true + (!!'true').should be_true + end + + it "doubled turns a not truthful object into `false'" do + (!!false).should be_false + (!!nil).should be_false + end +end 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 new file mode 100644 index 0000000000..a8e023efb6 --- /dev/null +++ b/spec/ruby/language/numbers_spec.rb @@ -0,0 +1,105 @@ +require_relative '../spec_helper' + +describe "A number literal" do + + it "can be a sequence of decimal digits" do + 435.should == 435 + end + + it "can have '_' characters between digits" do + 4_3_5_7.should == 4357 + end + + it "cannot have a leading underscore" do + -> { eval("_4_2") }.should raise_error(NameError) + end + + it "can have a decimal point" do + 4.35.should == 4.35 + end + + it "must have a digit before the decimal point" do + 0.75.should == 0.75 + -> { eval(".75") }.should raise_error(SyntaxError) + -> { eval("-.75") }.should raise_error(SyntaxError) + end + + it "can have an exponent" do + 1.2e-3.should == 0.0012 + end + + it "can be a sequence of hexadecimal digits with a leading '0x'" do + 0xffff.should == 65535 + end + + it "can be a sequence of binary digits with a leading '0x'" do + 0b01011.should == 11 + end + + it "can be a sequence of octal digits with a leading '0'" do + 0377.should == 255 + end + + it "can be an integer literal with trailing 'r' to represent a Rational" do + eval('3r').should == Rational(3, 1) + eval('-3r').should == Rational(-3, 1) + end + + it "can be an float literal with trailing 'r' to represent a Rational in a canonical form" do + eval('1.0r').should == Rational(1, 1) + end + + it "can be a float literal with trailing 'r' to represent a Rational" do + eval('0.0174532925199432957r').should == Rational(174532925199432957, 10000000000000000000) + end + + it "can be a bignum literal with trailing 'r' to represent a Rational" do + eval('1111111111111111111111111111111111111111111111r').should == Rational(1111111111111111111111111111111111111111111111, 1) + eval('-1111111111111111111111111111111111111111111111r').should == Rational(-1111111111111111111111111111111111111111111111, 1) + end + + it "can be a decimal literal with trailing 'r' to represent a Rational" do + eval('0.3r').should == Rational(3, 10) + eval('-0.3r').should == Rational(-3, 10) + end + + it "can be a hexadecimal literal with trailing 'r' to represent a Rational" do + eval('0xffr').should == Rational(255, 1) + eval('-0xffr').should == Rational(-255, 1) + end + + it "can be an octal literal with trailing 'r' to represent a Rational" do + eval('042r').should == Rational(34, 1) + eval('-042r').should == Rational(-34, 1) + end + + it "can be a binary literal with trailing 'r' to represent a Rational" do + eval('0b1111r').should == Rational(15, 1) + eval('-0b1111r').should == Rational(-15, 1) + end + + it "can be an integer literal with trailing 'i' to represent a Complex" do + eval('5i').should == Complex(0, 5) + eval('-5i').should == Complex(0, -5) + end + + it "can be a decimal literal with trailing 'i' to represent a Complex" do + eval('0.6i').should == Complex(0, 0.6) + eval('-0.6i').should == Complex(0, -0.6) + end + + it "can be a hexadecimal literal with trailing 'i' to represent a Complex" do + eval('0xffi').should == Complex(0, 255) + eval('-0xffi').should == Complex(0, -255) + end + + it "can be a octal literal with trailing 'i' to represent a Complex" do + eval("042i").should == Complex(0, 34) + eval("-042i").should == Complex(0, -34) + end + + it "can be a binary literal with trailing 'i' to represent a Complex" do + eval('0b1110i').should == Complex(0, 14) + eval('-0b1110i').should == Complex(0, -14) + end +end diff --git a/spec/ruby/language/optional_assignments_spec.rb b/spec/ruby/language/optional_assignments_spec.rb new file mode 100644 index 0000000000..5fe3e3671b --- /dev/null +++ b/spec/ruby/language/optional_assignments_spec.rb @@ -0,0 +1,742 @@ +require_relative '../spec_helper' +require_relative '../fixtures/constants' + +describe 'Optional variable assignments' do + describe 'using ||=' do + describe 'using a single variable' do + it 'assigns a new variable' do + a ||= 10 + + a.should == 10 + end + + it 're-assigns an existing variable set to false' do + a = false + a ||= 10 + + a.should == 10 + end + + it 're-assigns an existing variable set to nil' do + a = nil + a ||= 10 + + a.should == 10 + end + + it 'does not re-assign a variable with a truthy value' do + a = 10 + a ||= 20 + + a.should == 10 + end + + it 'does not evaluate the right side when not needed' do + a = 10 + a ||= raise('should not be executed') + a.should == 10 + end + + it 'does not re-assign a variable with a truthy value when using an inline rescue' do + a = 10 + a ||= 20 rescue 30 + + 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 an accessor' do + before do + klass = Class.new { attr_accessor :b } + @a = klass.new + end + + it 'assigns a new variable' do + @a.b ||= 10 + + @a.b.should == 10 + end + + it 're-assigns an existing variable set to false' do + @a.b = false + @a.b ||= 10 + + @a.b.should == 10 + end + + it 're-assigns an existing variable set to nil' do + @a.b = nil + @a.b ||= 10 + + @a.b.should == 10 + end + + it 'does not re-assign a variable with a truthy value' do + @a.b = 10 + @a.b ||= 20 + + @a.b.should == 10 + end + + it 'does not evaluate the right side when not needed' do + @a.b = 10 + @a.b ||= raise('should not be executed') + @a.b.should == 10 + end + + it 'does not re-assign a variable with a truthy value when using an inline rescue' do + @a.b = 10 + @a.b ||= 20 rescue 30 + + @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 + + describe 'using &&=' do + describe 'using a single variable' do + it 'leaves new variable unassigned' do + a &&= 10 + + a.should == nil + end + + it 'leaves false' do + a = false + a &&= 10 + + a.should == false + end + + it 'leaves nil' do + a = nil + a &&= 10 + + a.should == nil + end + + it 'does not evaluate the right side when not needed' do + a = nil + a &&= raise('should not be executed') + a.should == nil + end + + it 'does re-assign a variable with a truthy value' do + a = 10 + a &&= 20 + + a.should == 20 + end + + it 'does re-assign a variable with a truthy value when using an inline rescue' do + a = 10 + a &&= 20 rescue 30 + + a.should == 20 + end + end + + describe 'using an accessor' do + before do + klass = Class.new { attr_accessor :b } + @a = klass.new + end + + it 'leaves new variable unassigned' do + @a.b &&= 10 + + @a.b.should == nil + end + + it 'leaves false' do + @a.b = false + @a.b &&= 10 + + @a.b.should == false + end + + it 'leaves nil' do + @a.b = nil + @a.b &&= 10 + + @a.b.should == nil + end + + it 'does not evaluate the right side when not needed' do + @a.b = nil + @a.b &&= raise('should not be executed') + @a.b.should == nil + end + + it 'does re-assign a variable with a truthy value' do + @a.b = 10 + @a.b &&= 20 + + @a.b.should == 20 + end + + it 'does re-assign a variable with a truthy value when using an inline rescue' do + @a.b = 10 + @a.b &&= 20 rescue 30 + + @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 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 + + it 'with ||= assignments' do + Object::A ||= 10 + Object::A.should == 10 + end + + it 'with ||= do not reassign' do + Object::A = 20 + Object::A ||= 10 + Object::A.should == 20 + end + + it 'with &&= assignments' do + Object::A = 20 + -> { + Object::A &&= 10 + }.should complain(/already initialized constant/) + Object::A.should == 10 + end + + it 'with &&= assignments will fail with non-existent constants' do + -> { Object::A &&= 10 }.should raise_error(NameError) + end + + it 'with operator assignments' do + Object::A = 20 + -> { + Object::A += 10 + }.should complain(/already initialized constant/) + Object::A.should == 30 + end + + it 'with operator assignments will fail with non-existent constants' do + -> { 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 new file mode 100644 index 0000000000..fb75e788f1 --- /dev/null +++ b/spec/ruby/language/or_spec.rb @@ -0,0 +1,90 @@ +require_relative '../spec_helper' + +describe "The || operator" do + it "evaluates to true if any of its operands are true" do + if false || true || nil + x = true + end + x.should == true + end + + it "evaluated to false if all of its operands are false" do + if false || nil + x = true + end + x.should == nil + end + + it "is evaluated before assignment operators" do + x = nil || true + x.should == true + end + + it "has a lower precedence than the && operator" do + x = 1 || false && x = 2 + x.should == 1 + end + + it "treats empty expressions as nil" do + (() || true).should be_true + (() || false).should be_false + (true || ()).should be_true + (false || ()).should be_nil + (() || ()).should be_nil + end + + it "has a higher precedence than 'break' in 'break true || false'" do + # see also 'break true or false' below + -> { break false || true }.call.should be_true + end + + it "has a higher precedence than 'next' in 'next true || false'" do + -> { next false || true }.call.should be_true + end + + it "has a higher precedence than 'return' in 'return true || false'" do + -> { return false || true }.call.should be_true + end +end + +describe "The or operator" do + it "evaluates to true if any of its operands are true" do + x = nil + if false or true + x = true + end + x.should == true + end + + it "is evaluated after variables are assigned" do + x = nil or true + x.should == nil + end + + it "has a lower precedence than the || operator" do + x,y = nil + x = true || false or y = 1 + y.should == nil + end + + it "treats empty expressions as nil" do + (() or true).should be_true + (() or false).should be_false + (true or ()).should be_true + (false or ()).should be_nil + (() or ()).should be_nil + end + + it "has a lower precedence than 'break' in 'break true or false'" do + # see also 'break true || false' above + -> { eval "break true or false" }.should raise_error(SyntaxError, /void value expression/) + end + + it "has a lower precedence than 'next' in 'next true or false'" do + -> { eval "next true or false" }.should raise_error(SyntaxError, /void value expression/) + end + + it "has a lower precedence than 'return' in 'return true or false'" do + -> { eval "return true or false" }.should raise_error(SyntaxError, /void value expression/) + end +end diff --git a/spec/ruby/language/order_spec.rb b/spec/ruby/language/order_spec.rb new file mode 100644 index 0000000000..d550f6b3f4 --- /dev/null +++ b/spec/ruby/language/order_spec.rb @@ -0,0 +1,75 @@ +require_relative '../spec_helper' + +describe "A method call" do + before :each do + @obj = Object.new + def @obj.foo0(&a) + [a ? a.call : nil] + end + def @obj.foo1(a, &b) + [a, b ? b.call : nil] + end + def @obj.foo2(a, b, &c) + [a, b, c ? c.call : nil] + end + def @obj.foo3(a, b, c, &d) + [a, b, c, d ? d.call : nil] + end + def @obj.foo4(a, b, c, d, &e) + [a, b, c, d, e ? e.call : nil] + end + end + + it "evaluates the receiver first" do + (obj = @obj).foo1(obj = nil).should == [nil, nil] + (obj = @obj).foo2(obj = nil, obj = nil).should == [nil, nil, nil] + (obj = @obj).foo3(obj = nil, obj = nil, obj = nil).should == [nil, nil, nil, nil] + (obj = @obj).foo4(obj = nil, obj = nil, obj = nil, obj = nil).should == [nil, nil, nil, nil, nil] + end + + it "evaluates arguments after receiver" do + a = 0 + (a += 1; @obj).foo1(a).should == [1, nil] + (a += 1; @obj).foo2(a, a).should == [2, 2, nil] + (a += 1; @obj).foo3(a, a, a).should == [3, 3, 3, nil] + (a += 1; @obj).foo4(a, a, a, a).should == [4, 4, 4, 4, nil] + a.should == 4 + end + + it "evaluates arguments left-to-right" do + a = 0 + @obj.foo1(a += 1).should == [1, nil] + @obj.foo2(a += 1, a += 1).should == [2, 3, nil] + @obj.foo3(a += 1, a += 1, a += 1).should == [4, 5, 6, nil] + @obj.foo4(a += 1, a += 1, a += 1, a += 1).should == [7, 8, 9, 10, nil] + a.should == 10 + end + + it "evaluates block pass after arguments" do + a = 0 + p = proc {true} + @obj.foo1(a += 1, &(a += 1; p)).should == [1, true] + @obj.foo2(a += 1, a += 1, &(a += 1; p)).should == [3, 4, true] + @obj.foo3(a += 1, a += 1, a += 1, &(a += 1; p)).should == [6, 7, 8, true] + @obj.foo4(a += 1, a += 1, a += 1, a += 1, &(a += 1; p)).should == [10, 11, 12, 13, true] + a.should == 14 + end + + it "evaluates block pass after receiver" do + p1 = proc {true} + p2 = proc {false} + p1.should_not == p2 + + p = p1 + (p = p2; @obj).foo0(&p).should == [false] + p = p1 + (p = p2; @obj).foo1(1, &p).should == [1, false] + p = p1 + (p = p2; @obj).foo2(1, 1, &p).should == [1, 1, false] + p = p1 + (p = p2; @obj).foo3(1, 1, 1, &p).should == [1, 1, 1, false] + p = p1 + (p = p2; @obj).foo4(1, 1, 1, 1, &p).should == [1, 1, 1, 1, false] + p = p1 + end +end 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 new file mode 100644 index 0000000000..5e606c16d8 --- /dev/null +++ b/spec/ruby/language/precedence_spec.rb @@ -0,0 +1,445 @@ +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 +# to use a technique from formal proofs that involve a set of +# equivalent statements. Suppose you have statements A, B, C. +# If they are claimed to be equivalent, this can be shown by +# proving that A implies B, B implies C, and C implies A. +# (Actually any closed circuit of implications.) +# +# Here, we can use a similar technique where we show starting +# at the top that each level of operator has precedence over +# the level below (as well as showing associativity within +# the precedence level). + +# 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 +# The correct table derived from MRI's parse.y is as follows: +# +# Operator Assoc Description +#--------------------------------------------------------------- +# ! ~ + > Not, complement, unary plus +# ** > Exponentiation +# - > Unary minus +# * / % < Multiply, divide, and modulo +# + - < Plus and minus +# >> << < Right and left shift +# & < “And” (bitwise for integers) +# ^ | < Exclusive “or” and regular “or” (bitwise for integers) +# <= < > >= < Comparison operators +# <=> == === != =~ !~ no Equality and pattern match operators (!= +# and !~ may not be defined as methods) +# && < Logical “and” +# || < Logical “or” +# .. ... no Range (inclusive and exclusive) +# ? : > Ternary if-then-else +# rescue < Rescue modifier +# = %= /= -= += |= &= > Assignment +# >>= <<= *= &&= ||= **= +# defined? no Check if symbol defined +# not > Logical negation +# or and < Logical composition +# if unless while until no Expression modifiers +# ----------------------------------------------------------------------- +# +# [] and []= seem to fall out of here, as well as begin/end +# + +# TODO: Resolve these two tables with actual specs. As the comment at the +# top suggests, these specs need to be reorganized into a single describe +# block for each operator. The describe block should include an example +# for associativity (if relevant), an example for any short circuit behavior +# (e.g. &&, ||, etc.) and an example block for each operator over which the +# instant operator has immediately higher precedence. + +describe "Operators" do + it "! ~ + is right-associative" do + (!!true).should == true + (~~0).should == 0 + (++2).should == 2 + end + + it "** is right-associative" do + (2**2**3).should == 256 + end + + it "** has higher precedence than unary minus" do + (-2**2).should == -4 + end + + it "unary minus is right-associative" do + (--2).should == 2 + end + + it "unary minus has higher precedence than * / %" do + class UnaryMinusTest; def -@; 50; end; end + b = UnaryMinusTest.new + + (-b * 5).should == 250 + (-b / 5).should == 10 + (-b % 7).should == 1 + end + + it "treats +/- as a regular send if the arguments are known locals or block locals" do + a = PrecedenceSpecs::NonUnaryOpTest.new + a.add_num(1).should == [3] + a.sub_num(1).should == [1] + a.add_str.should == ['11'] + a.add_var.should == [2] + end + + it "* / % are left-associative" do + (2*1/2).should == (2*1)/2 + # Guard against the Mathn library + # TODO: Make these specs not rely on specific behaviour / result values + # by using mocks. + guard -> { !defined?(Math.rsqrt) } do + (2*1/2).should_not == 2*(1/2) + end + + (10/7/5).should == (10/7)/5 + (10/7/5).should_not == 10/(7/5) + + (101 % 55 % 7).should == (101 % 55) % 7 + (101 % 55 % 7).should_not == 101 % (55 % 7) + + (50*20/7%42).should == ((50*20)/7)%42 + (50*20/7%42).should_not == 50*(20/(7%42)) + end + + it "* / % have higher precedence than + -" do + (2+2*2).should == 6 + (1+10/5).should == 3 + (2+10%5).should == 2 + + (2-2*2).should == -2 + (1-10/5).should == -1 + (10-10%4).should == 8 + end + + it "+ - are left-associative" do + (2-3-4).should == -5 + (4-3+2).should == 3 + + binary_plus = Class.new(String) do + alias_method :plus, :+ + def +(a) + plus(a) + "!" + end + end + s = binary_plus.new("a") + + (s+s+s).should == (s+s)+s + (s+s+s).should_not == s+(s+s) + end + + it "+ - have higher precedence than >> <<" do + (2<<1+2).should == 16 + (8>>1+2).should == 1 + (4<<1-3).should == 1 + (2>>1-3).should == 8 + end + + it ">> << are left-associative" do + (1 << 2 << 3).should == 32 + (10 >> 1 >> 1).should == 2 + (10 << 4 >> 1).should == 80 + end + + it ">> << have higher precedence than &" do + (4 & 2 << 1).should == 4 + (2 & 4 >> 1).should == 2 + end + + it "& is left-associative" do + class BitwiseAndTest; def &(a); a+1; end; end + c = BitwiseAndTest.new + + (c & 5 & 2).should == (c & 5) & 2 + (c & 5 & 2).should_not == c & (5 & 2) + end + + it "& has higher precedence than ^ |" do + (8 ^ 16 & 16).should == 24 + (8 | 16 & 16).should == 24 + end + + it "^ | are left-associative" do + class OrAndXorTest; def ^(a); a+10; end; def |(a); a-10; end; end + d = OrAndXorTest.new + + (d ^ 13 ^ 16).should == (d ^ 13) ^ 16 + (d ^ 13 ^ 16).should_not == d ^ (13 ^ 16) + + (d | 13 | 4).should == (d | 13) | 4 + (d | 13 | 4).should_not == d | (13 | 4) + end + + it "^ | have higher precedence than <= < > >=" do + (10 <= 7 ^ 7).should == false + (10 < 7 ^ 7).should == false + (10 > 7 ^ 7).should == true + (10 >= 7 ^ 7).should == true + (10 <= 7 | 7).should == false + (10 < 7 | 7).should == false + (10 > 7 | 7).should == true + (10 >= 7 | 7).should == true + end + + it "<= < > >= are left-associative" do + class ComparisonTest + def <=(a); 0; end; + def <(a); 0; end; + def >(a); 0; end; + def >=(a); 0; end; + end + + e = ComparisonTest.new + + (e <= 0 <= 1).should == (e <= 0) <= 1 + (e <= 0 <= 1).should_not == e <= (0 <= 1) + + (e < 0 < 1).should == (e < 0) < 1 + (e < 0 < 1).should_not == e < (0 < 1) + + (e >= 0 >= 1).should == (e >= 0) >= 1 + (e >= 0 >= 1).should_not == e >= (0 >= 1) + + (e > 0 > 1).should == (e > 0) > 1 + (e > 0 > 1).should_not == e > (0 > 1) + end + + it "<=> == === != =~ !~ are non-associative" do + -> { eval("1 <=> 2 <=> 3") }.should raise_error(SyntaxError) + -> { eval("1 == 2 == 3") }.should raise_error(SyntaxError) + -> { eval("1 === 2 === 3") }.should raise_error(SyntaxError) + -> { eval("1 != 2 != 3") }.should raise_error(SyntaxError) + -> { eval("1 =~ 2 =~ 3") }.should raise_error(SyntaxError) + -> { eval("1 !~ 2 !~ 3") }.should raise_error(SyntaxError) + end + + it "<=> == === != =~ !~ have higher precedence than &&" do + (false && 2 <=> 3).should == false + (false && 3 == false).should == false + (false && 3 === false).should == false + (false && 3 != true).should == false + + class FalseClass; def =~(o); o == false; end; end + (false && true =~ false).should == (false && (true =~ false)) + (false && true =~ false).should_not == ((false && true) =~ false) + class FalseClass; undef_method :=~; end + + (false && true !~ true).should == false + end + + # XXX: figure out how to test it + # (a && b) && c equals to a && (b && c) for all a,b,c values I can imagine so far + it "&& is left-associative" + + it "&& has higher precedence than ||" do + (true || false && false).should == true + end + + # XXX: figure out how to test it + it "|| is left-associative" + + it "|| has higher precedence than .. ..." do + (1..false||10).should == (1..10) + (1...false||10).should == (1...10) + end + + it ".. ... are non-associative" do + -> { eval("1..2..3") }.should raise_error(SyntaxError) + -> { eval("1...2...3") }.should raise_error(SyntaxError) + end + + it ".. ... have higher precedence than ? :" do + # Use variables to avoid warnings + from = 1 + to = 2 + # These are flip-flop, not Range instances + (from..to ? 3 : 4).should == 3 + (from...to ? 3 : 4).should == 3 + end + + it "? : is right-associative" do + (true ? 2 : 3 ? 4 : 5).should == 2 + end + + def oops; raise end + + it "? : has higher precedence than rescue" do + (true ? oops : 0 rescue 10).should == 10 + end + + # XXX: figure how to test it (problem similar to || associativity) + it "rescue is left-associative" + + it "rescue has higher precedence than =" do + a = oops rescue 10 + a.should == 10 + + # rescue doesn't have the same sense for %= /= and friends + end + + it "= %= /= -= += |= &= >>= <<= *= &&= ||= **= are right-associative" do + a = b = 10 + a.should == 10 + b.should == 10 + + a = b = 10 + a %= b %= 3 + a.should == 0 + b.should == 1 + + a = b = 10 + a /= b /= 2 + a.should == 2 + b.should == 5 + + a = b = 10 + a -= b -= 2 + a.should == 2 + b.should == 8 + + a = b = 10 + a += b += 2 + a.should == 22 + b.should == 12 + + a,b = 32,64 + a |= b |= 2 + a.should == 98 + b.should == 66 + + a,b = 25,13 + a &= b &= 7 + a.should == 1 + b.should == 5 + + a,b=8,2 + a >>= b >>= 1 + a.should == 4 + b.should == 1 + + a,b=8,2 + a <<= b <<= 1 + a.should == 128 + b.should == 4 + + a,b=8,2 + a *= b *= 2 + a.should == 32 + b.should == 4 + + a,b=10,20 + a &&= b &&= false + a.should == false + b.should == false + + a,b=nil,nil + a ||= b ||= 10 + a.should == 10 + b.should == 10 + + a,b=2,3 + a **= b **= 2 + a.should == 512 + b.should == 9 + end + + it "= %= /= -= += |= &= >>= <<= *= &&= ||= **= have higher precedence than defined? operator" do + (defined? a = 10).should == "assignment" + (defined? a %= 10).should == "assignment" + (defined? a /= 10).should == "assignment" + (defined? a -= 10).should == "assignment" + (defined? a += 10).should == "assignment" + (defined? a |= 10).should == "assignment" + (defined? a &= 10).should == "assignment" + (defined? a >>= 10).should == "assignment" + (defined? a <<= 10).should == "assignment" + (defined? a *= 10).should == "assignment" + (defined? a &&= 10).should == "assignment" + (defined? a ||= 10).should == "assignment" + (defined? a **= 10).should == "assignment" + end + + # XXX: figure out how to test it + it "defined? is non-associative" + + it "defined? has higher precedence than not" do + # does it have sense? + (not defined? qqq).should == true + end + + it "not is right-associative" do + (not not false).should == false + (not not 10).should == true + end + + it "not has higher precedence than or/and" do + (not false and false).should == false + (not false or true).should == true + end + + # XXX: figure out how to test it + it "or/and are left-associative" + + it "or/and have higher precedence than if unless while until modifiers" do + (1 if 2 and 3).should == 1 + (1 if 2 or 3).should == 1 + + (1 unless false and true).should == 1 + (1 unless false or false).should == 1 + + (1 while true and false).should == nil # would hang upon error + (1 while false or false).should == nil + + ((raise until true and false) rescue 10).should == 10 + (1 until false or true).should == nil # would hang upon error + end + + # XXX: it seems to me they are right-associative + it "if unless while until are non-associative" +end diff --git a/spec/ruby/language/predefined/data_spec.rb b/spec/ruby/language/predefined/data_spec.rb new file mode 100644 index 0000000000..921d236ee9 --- /dev/null +++ b/spec/ruby/language/predefined/data_spec.rb @@ -0,0 +1,48 @@ +require_relative '../../spec_helper' + +describe "The DATA constant" do + it "exists when the main script contains __END__" do + ruby_exe(fixture(__FILE__, "data1.rb")).chomp.should == "true" + end + + it "does not exist when the main script contains no __END__" do + ruby_exe("puts Object.const_defined?(:DATA)").chomp.should == 'false' + end + + it "does not exist when an included file has a __END__" do + ruby_exe(fixture(__FILE__, "data2.rb")).chomp.should == "false" + end + + it "does not change when an included files also has a __END__" do + ruby_exe(fixture(__FILE__, "data3.rb")).chomp.should == "data 3" + end + + it "is included in an otherwise empty file" do + ap = fixture(__FILE__, "print_data.rb") + str = ruby_exe(fixture(__FILE__, "data_only.rb"), options: "-r#{ap}") + 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 +end diff --git a/spec/ruby/language/predefined/fixtures/data1.rb b/spec/ruby/language/predefined/fixtures/data1.rb new file mode 100644 index 0000000000..cb9572255b --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/data1.rb @@ -0,0 +1,4 @@ +puts Object.const_defined?(:DATA) + +__END__ +data1 diff --git a/spec/ruby/language/predefined/fixtures/data2.rb b/spec/ruby/language/predefined/fixtures/data2.rb new file mode 100644 index 0000000000..a764ca56d1 --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/data2.rb @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000000..e37313c68b --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/data3.rb @@ -0,0 +1,6 @@ +require_relative 'data4' + +puts DATA.read + +__END__ +data 3 diff --git a/spec/ruby/language/predefined/fixtures/data4.rb b/spec/ruby/language/predefined/fixtures/data4.rb new file mode 100644 index 0000000000..139ef80d7b --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/data4.rb @@ -0,0 +1,4 @@ +# nothing + +__END__ +data 4 diff --git a/spec/ruby/language/predefined/fixtures/data5.rb b/spec/ruby/language/predefined/fixtures/data5.rb new file mode 100644 index 0000000000..48f060e1a9 --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/data5.rb @@ -0,0 +1,5 @@ +DATA.rewind +puts DATA.gets + +__END__ +data 5 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/data_only.rb b/spec/ruby/language/predefined/fixtures/data_only.rb new file mode 100644 index 0000000000..004ac62737 --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/data_only.rb @@ -0,0 +1,2 @@ +__END__ +data only 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/print_data.rb b/spec/ruby/language/predefined/fixtures/print_data.rb new file mode 100644 index 0000000000..4a5692e6a7 --- /dev/null +++ b/spec/ruby/language/predefined/fixtures/print_data.rb @@ -0,0 +1,3 @@ +at_exit { + puts DATA.read +} 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 new file mode 100644 index 0000000000..fc1667a38f --- /dev/null +++ b/spec/ruby/language/predefined_spec.rb @@ -0,0 +1,1574 @@ +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' +# Second Edition by Dave Thomas, Chad Fowler, and Andy Hunt, page 319-22. +# +# 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. + +# Exception Information +# --------------------------------------------------------------------------------------------------- +# +# $! Exception The exception object passed to raise. [thread] +# $@ Array The stack backtrace generated by the last exception. [thread] + +# Pattern Matching Variables +# --------------------------------------------------------------------------------------------------- +# +# These variables are set to nil after an unsuccessful pattern match. +# +# $& String The string matched (following a successful pattern match). This variable is +# local to the current scope. [r/o, thread] +# $+ String The contents of the highest-numbered group matched following a successful +# pattern match. Thus, in "cat" =~/(c|a)(t|z)/, $+ will be set to “t”. This +# variable is local to the current scope. [r/o, thread] +# $` String The string preceding the match in a successful pattern match. This variable +# is local to the current scope. [r/o, thread] +# $' String The string following the match in a successful pattern match. This variable +# is local to the current scope. [r/o, thread] +# $1 to $<N> String The contents of successive groups matched in a successful pattern match. In +# "cat" =~/(c|a)(t|z)/, $1 will be set to “a” and $2 to “t”. This variable +# is local to the current scope. [r/o, thread] +# $~ MatchData An object that encapsulates the results of a successful pattern match. The +# variables $&, $`, $', and $1 to $<N> are all derived from $~. Assigning to $~ +# changes the values of these derived variables. This variable is local to the +# current scope. [thread] + + +describe "Predefined global $~" do + it "is set to contain the MatchData object of the last match if successful" do + md = /foo/.match 'foo' + $~.should be_kind_of(MatchData) + $~.should equal md + + /bar/ =~ 'bar' + $~.should be_kind_of(MatchData) + $~.should_not equal md + end + + it "is set to nil if the last match was unsuccessful" do + /foo/ =~ 'foo' + $~.should_not.nil? + + /foo/ =~ 'bar' + $~.should.nil? + end + + it "is set at the method-scoped level rather than block-scoped" do + obj = Object.new + def obj.foo; yield; end + def obj.foo2(&proc); proc.call; end + + match2 = nil + match3 = nil + match4 = nil + + match1 = /foo/.match "foo" + + obj.foo { match2 = /bar/.match("bar") } + + match2.should_not == nil + $~.should == match2 + + match3 = /baz/.match("baz") + + match3.should_not == nil + $~.should == match3 + + obj.foo2 { match4 = /qux/.match("qux") } + + match4.should_not == nil + $~.should == match4 + end + + it "raises an error if assigned an object not nil or instanceof MatchData" do + $~ = nil + $~.should == nil + $~ = /foo/.match("foo") + $~.should be_an_instance_of(MatchData) + + -> { $~ = 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 + "foo" =~ /(f)oo/ + foo_match = $~ + "bar" =~ /(b)ar/ + $~ = foo_match + $1.should == "f" + end + + it "changes the value of the derived preceding match global" do + "foo hello" =~ /hello/ + foo_match = $~ + "bar" =~ /(bar)/ + $~ = foo_match + $`.should == "foo " + end + + it "changes the value of the derived following match global" do + "foo hello" =~ /foo/ + foo_match = $~ + "bar" =~ /(bar)/ + $~ = foo_match + $'.should == " hello" + end + + it "changes the value of the derived full match global" do + "foo hello" =~ /foo/ + foo_match = $~ + "bar" =~ /(bar)/ + $~ = foo_match + $&.should == "foo" + end +end + +describe "Predefined global $&" do + it "is equivalent to MatchData#[0] on the last match $~" do + /foo/ =~ 'barfoobaz' + $&.should == $~[0] + $&.should == 'foo' + 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 + +describe "Predefined global $`" do + it "is equivalent to MatchData#pre_match on the last match $~" do + /foo/ =~ 'barfoobaz' + $`.should == $~.pre_match + $`.should == 'bar' + 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".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 + +describe "Predefined global $'" do + it "is equivalent to MatchData#post_match on the last match $~" do + /foo/ =~ 'barfoobaz' + $'.should == $~.post_match + $'.should == 'baz' + 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".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 + +describe "Predefined global $+" do + it "is equivalent to $~.captures.last" do + /(f(o)o)/ =~ 'barfoobaz' + $+.should == $~.captures.last + $+.should == 'o' + end + + it "captures the last non nil capture" do + /(a)|(b)/ =~ 'a' + $+.should == 'a' + 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 + +describe "Predefined globals $1..N" do + it "are equivalent to $~[N]" do + /(f)(o)(o)/ =~ 'foo' + $1.should == $~[1] + $2.should == $~[2] + $3.should == $~[3] + $4.should == $~[4] + + [$1, $2, $3, $4].should == ['f', 'o', 'o', nil] + end + + it "are nil unless a match group occurs" do + def test(arg) + case arg + when /-(.)?/ + $1 + end + end + test("-").should == nil + 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 + +describe "Predefined global $stdout" do + before :each do + @old_stdout = $stdout + end + + after :each do + $stdout = @old_stdout + end + + it "raises TypeError error if assigned to nil" do + -> { $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') + -> { $stdout = obj }.should raise_error(TypeError) + + obj.stub!(:write) + $stdout = obj + $stdout.should equal(obj) + end +end + +describe "Predefined global $!" do + it "is Fiber-local" do + Fiber.new do + raise "hi" + rescue + Fiber.yield + end.resume + + $!.should == nil + end + + it "is read-only" do + -> { + $! = [] + }.should raise_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 + + obj = Class.new do + def method_missing(*args) + super + end + end.new + + [obj, 'foo'].join + + $!.should == nil + end + + it "should be set to the value of $! before the begin after a successful rescue" do + outer = StandardError.new 'outer' + inner = StandardError.new 'inner' + + begin + raise outer + rescue + $!.should == outer + + # nested rescue + begin + $!.should == outer + raise inner + rescue + $!.should == inner + ensure + $!.should == outer + end + $!.should == outer + end + $!.should == nil + end + + it "should be set to the value of $! before the begin after a rescue which returns" do + def foo + outer = StandardError.new 'outer' + inner = StandardError.new 'inner' + + begin + raise outer + rescue + $!.should == outer + + # nested rescue + begin + $!.should == outer + raise inner + rescue + $!.should == inner + return + ensure + $!.should == outer + end + $!.should == outer + end + $!.should == nil + end + foo + end + + it "should be set to the value of $! before the begin after a successful rescue within an ensure" do + outer = StandardError.new 'outer' + inner = StandardError.new 'inner' + + begin + begin + raise outer + ensure + $!.should == outer + + # nested rescue + begin + $!.should == outer + raise inner + rescue + $!.should == inner + ensure + $!.should == outer + end + $!.should == outer + end + flunk "outer should be raised after the ensure" + rescue + $!.should == outer + end + $!.should == nil + end + + it "should be set to the new exception after a throwing rescue" do + outer = StandardError.new 'outer' + inner = StandardError.new 'inner' + + begin + raise outer + rescue + $!.should == outer + + begin + # nested rescue + begin + $!.should == outer + raise inner + rescue # the throwing rescue + $!.should == inner + raise inner + ensure + $!.should == inner + end + rescue # do not make the exception fail the example + $!.should == inner + end + $!.should == outer + end + $!.should == nil + end + + describe "in bodies without ensure" do + it "should be cleared when an exception is rescued" do + e = StandardError.new 'foo' + begin + raise e + rescue + $!.should == e + end + $!.should == nil + end + + it "should be cleared when an exception is rescued even when a non-local return is present" do + def foo(e) + $!.should == e + yield + end + def bar + e = StandardError.new 'foo' + begin + raise e + rescue + $!.should == e + foo(e) { return } + end + end + + bar + $!.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 + begin + begin + raise e + rescue TypeError + flunk + end + ensure + $!.should == e + end + rescue + $!.should == e + end + $!.should == nil + end + + it "should not be cleared when an exception is rescued and rethrown" do + e = StandardError.new 'foo' + begin + begin + begin + raise e + rescue => e + $!.should == e + raise e + end + ensure + $!.should == e + end + rescue + $!.should == e + end + $!.should == nil + end + end + + describe "in ensure-protected bodies" do + it "should be cleared when an exception is rescued" do + e = StandardError.new 'foo' + begin + raise e + rescue + $!.should == e + ensure + $!.should == nil + end + $!.should == nil + end + + it "should not be cleared when an exception is not rescued" do + e = StandardError.new + begin + begin + begin + raise e + rescue TypeError + flunk + ensure + $!.should == e + end + ensure + $!.should == e + end + rescue + $!.should == e + end + end + + it "should not be cleared when an exception is rescued and rethrown" do + e = StandardError.new + begin + begin + begin + raise e + rescue => e + $!.should == e + raise e + ensure + $!.should == e + end + ensure + $!.should == e + end + rescue + $!.should == e + end + end + end +end + +describe "Predefined global $@" do + it "is Fiber-local" do + Fiber.new do + raise "hi" + rescue + Fiber.yield + end.resume + + $@.should == nil + end + + it "is 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 + + after :each do + $/ = @dollar_slash + $-0 = @dollar_dash_zero + $VERBOSE = @verbose + end + + 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 + end + + it "returns the value assigned" do + ($/ = "xyz").should == "xyz" + end + + it "changes $-0" do + $/ = "xyz" + $-0.should equal($/) + end + + it "does not call #to_str to convert the object to a String" do + obj = mock("$/ value") + obj.should_not_receive(:to_str) + + -> { $/ = obj }.should raise_error(TypeError, 'value of $/ must be String') + end + + 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 + -> { $/ = 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 + + after :each do + $/ = @dollar_slash + $-0 = @dollar_dash_zero + $VERBOSE = @verbose + end + + 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 + $-0 = nil + $-0.should be_nil + end + + it "returns the value assigned" do + ($-0 = "xyz").should == "xyz" + end + + it "changes $/" do + $-0 = "xyz" + $/.should equal($-0) + end + + it "does not call #to_str to convert the object to a String" do + obj = mock("$-0 value") + obj.should_not_receive(:to_str) + + -> { $-0 = obj }.should raise_error(TypeError, 'value of $-0 must be String') + end + + 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 + -> { $-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 + +describe "Predefined global $," do + after :each do + $, = nil + end + + it "defaults to nil" do + $,.should be_nil + end + + it "raises TypeError if assigned a non-String" do + -> { $, = 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 + +describe "Predefined global $_" do + it "is set to the last line read by e.g. StringIO#gets" do + stdin = StringIO.new("foo\nbar\n", "r") + + read = stdin.gets + read.should == "foo\n" + $_.should == read + + read = stdin.gets + read.should == "bar\n" + $_.should == read + + read = stdin.gets + read.should == nil + $_.should == read + end + + it "is set at the method-scoped level rather than block-scoped" do + obj = Object.new + def obj.foo; yield; end + def obj.foo2; yield; end + + stdin = StringIO.new("foo\nbar\nbaz\nqux\n", "r") + match = stdin.gets + + obj.foo { match = stdin.gets } + + match.should == "bar\n" + $_.should == match + + match = stdin.gets + + match.should == "baz\n" + $_.should == match + + obj.foo2 { match = stdin.gets } + + match.should == "qux\n" + $_.should == match + end + + it "is Thread-local" do + $_ = nil + running = false + + thr = Thread.new do + $_ = "last line" + running = true + end + + Thread.pass until running + $_.should be_nil + + thr.join + end + + it "can be assigned any value" do + $_ = nil + $_.should == nil + $_ = "foo" + $_.should == "foo" + o = Object.new + $_ = o + $_.should == o + $_ = 1 + $_.should == 1 + end +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 + ($:.length > 0).should == true + end + + it "does not include the current directory" do + $:.should_not include(".") + end + + it "is the same object as $LOAD_PATH and $-I" do + $:.__id__.should == $LOAD_PATH.__id__ + $:.__id__.should == $-I.__id__ + end + + it "can be changed via <<" do + $: << "foo" + $:.should include("foo") + ensure + $:.delete("foo") + end + + it "is read-only" do + -> { + $: = [] + }.should raise_error(NameError, '$: is a read-only variable') + + -> { + $LOAD_PATH = [] + }.should raise_error(NameError, '$LOAD_PATH is a read-only variable') + + -> { + $-I = [] + }.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 + $".should equal $LOADED_FEATURES + end + + it "is read-only" do + -> { + $" = [] + }.should raise_error(NameError, '$" is a read-only variable') + + -> { + $LOADED_FEATURES = [] + }.should raise_error(NameError, '$LOADED_FEATURES is a read-only variable') + end +end + +describe "Global variable $<" do + it "is read-only" do + -> { + $< = nil + }.should raise_error(NameError, '$< is a read-only variable') + end +end + +describe "Global variable $FILENAME" do + it "is read-only" do + -> { + $FILENAME = "-" + }.should raise_error(NameError, '$FILENAME is a read-only variable') + end +end + +describe "Global variable $?" do + it "is read-only" do + -> { + $? = nil + }.should raise_error(NameError, '$? is a read-only variable') + end + + it "is thread-local" do + system(ruby_cmd('exit 0')) + Thread.new { $?.should be_nil }.join + end +end + +describe "Global variable $-a" do + it "is read-only" do + -> { $-a = true }.should raise_error(NameError, '$-a is a read-only variable') + end +end + +describe "Global variable $-l" do + it "is read-only" do + -> { $-l = true }.should raise_error(NameError, '$-l is a read-only variable') + end +end + +describe "Global variable $-p" do + it "is read-only" do + -> { $-p = true }.should raise_error(NameError, '$-p is a read-only variable') + end +end + +describe "Global variable $-d" do + before :each do + @debug = $DEBUG + end + + after :each do + $DEBUG = @debug + end + + it "is an alias of $DEBUG" do + $DEBUG = true + $-d.should be_true + $-d = false + $DEBUG.should be_false + end +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 + $VERBOSE.should be_true + end + end + + it "allows false" do + $VERBOSE = false + $VERBOSE.should be_false + end + + it "allows nil without coercing to false" do + $VERBOSE = nil + $VERBOSE.should be_nil + end +end + +describe :verbose_global_alias, shared: true do + before :each do + @verbose = $VERBOSE + end + + after :each do + $VERBOSE = @verbose + end + + it "is an alias of $VERBOSE" do + $VERBOSE = true + eval(@method).should be_true + eval("#{@method} = false") + $VERBOSE.should be_false + end +end + +describe "Global variable $-v" do + it_behaves_like :verbose_global_alias, '$-v' +end + +describe "Global variable $-w" do + it_behaves_like :verbose_global_alias, '$-w' +end + +describe "Global variable $0" do + before :each do + @orig_program_name = $0 + end + + after :each do + $0 = @orig_program_name + end + + it "is the path given as the main script and the same as __FILE__" do + script = "fixtures/dollar_zero.rb" + Dir.chdir(__dir__) do + ruby_exe(script).should == "#{script}\n#{script}\nOK" + end + end + + it "returns the program name" do + $0 = "rbx" + $0.should == "rbx" + end + + platform_is :linux, :darwin do + it "actually sets the program name" do + title = "rubyspec-dollar0-test" + $0 = title + `ps -ocommand= -p#{$$}`.should include(title) + end + end + + it "returns the given value when set" do + ($0 = "rbx").should == "rbx" + end + + it "raises a TypeError when not given an object that can be coerced to a String" do + -> { $0 = nil }.should raise_error(TypeError) + end +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 + Object.const_defined?(:ARGF).should == true + end + + it "includes ARGV" do + Object.const_defined?(:ARGV).should == true + end + + it "includes a hash-like object ENV" do + Object.const_defined?(:ENV).should == true + ENV.respond_to?(:[]).should == true + end +end + +describe "The predefined standard object nil" do + it "is an instance of NilClass" do + nil.should be_kind_of(NilClass) + end + + it "raises a SyntaxError if assigned to" do + -> { eval("nil = true") }.should raise_error(SyntaxError, /Can't assign to nil/) + end +end + +describe "The predefined standard object true" do + it "is an instance of TrueClass" do + true.should be_kind_of(TrueClass) + end + + it "raises a SyntaxError if assigned to" do + -> { eval("true = false") }.should raise_error(SyntaxError, /Can't assign to true/) + end +end + +describe "The predefined standard object false" do + it "is an instance of FalseClass" do + false.should be_kind_of(FalseClass) + end + + it "raises a SyntaxError if assigned to" do + -> { 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 + -> { eval("self = 1") }.should raise_error(SyntaxError, /Can't change the value of self/) + end +end + +# Global Constants +# --------------------------------------------------------------------------------------------------- +# +# The following constants are defined by the Ruby interpreter. +# +# DATA IO If the main program file contains the directive __END__, then +# the constant DATA will be initialized so that reading from it will +# return lines following __END__ from the source file. +# FALSE FalseClass Synonym for false (deprecated, removed in Ruby 3). +# NIL NilClass Synonym for nil (deprecated, removed in Ruby 3). +# RUBY_PLATFORM String The identifier of the platform running this program. This string +# is in the same form as the platform identifier used by the GNU +# configure utility (which is not a coincidence). +# RUBY_RELEASE_DATE String The date of this release. +# RUBY_VERSION String The version number of the interpreter. +# STDERR IO The actual standard error stream for the program. The initial +# value of $stderr. +# STDIN IO The actual standard input stream for the program. The initial +# value of $stdin. +# STDOUT IO The actual standard output stream for the program. The initial +# value of $stdout. +# SCRIPT_LINES__ Hash If a constant SCRIPT_LINES__ is defined and references a Hash, +# Ruby will store an entry containing the contents of each file it +# parses, with the file’s name as the key and an array of strings as +# the value. +# TOPLEVEL_BINDING Binding A Binding object representing the binding at Ruby’s top level— +# the level where programs are initially executed. +# TRUE TrueClass Synonym for true (deprecated, removed in Ruby 3). + +describe "The predefined global constants" do + describe "TRUE" do + it "is no longer defined" do + Object.const_defined?(:TRUE).should == false + end + end + + describe "FALSE" do + it "is no longer defined" do + Object.const_defined?(:FALSE).should == false + end + end + + describe "NIL" do + it "is no longer defined" do + Object.const_defined?(:NIL).should == false + end + end + + it "includes STDIN" do + Object.const_defined?(:STDIN).should == true + end + + it "includes STDOUT" do + Object.const_defined?(:STDOUT).should == true + end + + it "includes STDERR" do + Object.const_defined?(:STDERR).should == true + end + + it "includes RUBY_VERSION" do + Object.const_defined?(:RUBY_VERSION).should == true + end + + it "includes RUBY_RELEASE_DATE" do + Object.const_defined?(:RUBY_RELEASE_DATE).should == true + end + + it "includes RUBY_PLATFORM" do + Object.const_defined?(:RUBY_PLATFORM).should == true + end + + it "includes TOPLEVEL_BINDING" do + Object.const_defined?(:TOPLEVEL_BINDING).should == true + end +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 + + 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 + + it "has the same external encoding as Encoding.default_external when that encoding is changed" do + Encoding.default_external = Encoding::ISO_8859_16 + STDIN.external_encoding.should equal(Encoding::ISO_8859_16) + end + + it "has nil for the internal encoding" do + STDIN.internal_encoding.should be_nil + end + + it "has nil for the internal encoding despite Encoding.default_internal being changed" do + Encoding.default_internal = Encoding::IBM437 + STDIN.internal_encoding.should be_nil + end + end + + it "has the encodings set by #set_encoding" do + 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 + end + + describe "STDOUT" do + it "has nil for the external encoding" do + STDOUT.external_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 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 + + it "has nil for the internal encoding" do + STDOUT.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 + STDOUT.internal_encoding.should be_nil + end + end + + describe "STDERR" do + it "has nil for the external encoding" do + STDERR.external_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 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 + + 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 new file mode 100644 index 0000000000..b04aa25c9e --- /dev/null +++ b/spec/ruby/language/private_spec.rb @@ -0,0 +1,67 @@ +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) + -> { a.bar }.should raise_error(NoMethodError) + + b = Private::B.new + b.methods.should_not include(:bar) + -> { b.bar }.should raise_error(NoMethodError) + end + + # def expr.meth() methods are always public + it "has no effect on def expr.meth() methods" do + Private::B.public_defs_method.should == 0 + end + + it "is overridden when a new class is opened" do + c = Private::B::C.new + c.methods.should include(:baz) + c.baz + Private::B.public_class_method1.should == 1 + -> { Private::B.private_class_method1 }.should raise_error(NoMethodError) + end + + it "is no longer in effect when the class is closed" do + b = Private::B.new + b.methods.should include(:foo) + b.foo + end + + it "changes visibility of previously called method" do + klass = Class.new do + def foo + "foo" + end + end + f = klass.new + f.foo + klass.class_eval do + private :foo + end + -> { f.foo }.should raise_error(NoMethodError) + end + + it "changes visibility of previously called methods with same send/call site" do + g = ::Private::G.new + -> { + 2.times do + g.foo + module ::Private + class G + private :foo + end + end + end + }.should raise_error(NoMethodError) + end + + it "changes the visibility of the existing method in the subclass" do + ::Private::A.new.foo.should == 'foo' + -> { ::Private::H.new.foo }.should raise_error(NoMethodError) + ::Private::H.new.send(:foo).should == 'foo' + end +end diff --git a/spec/ruby/language/proc_spec.rb b/spec/ruby/language/proc_spec.rb new file mode 100644 index 0000000000..ca9a13aa61 --- /dev/null +++ b/spec/ruby/language/proc_spec.rb @@ -0,0 +1,249 @@ +require_relative '../spec_helper' + +describe "A Proc" do + it "captures locals from the surrounding scope" do + var = 1 + lambda { var }.call.should == 1 + end + + it "does not capture a local when an argument has the same name" do + var = 1 + lambda { |var| var }.call(2).should == 2 + var.should == 1 + end + + describe "taking zero arguments" do + before :each do + @l = lambda { 1 } + end + + it "does not raise an exception if no values are passed" do + @l.call.should == 1 + end + + it "raises an ArgumentError if a value is passed" do + lambda { @l.call(0) }.should raise_error(ArgumentError) + end + end + + describe "taking || arguments" do + before :each do + @l = lambda { || 1 } + end + + it "does not raise an exception when passed no values" do + @l.call.should == 1 + end + + it "raises an ArgumentError if a value is passed" do + lambda { @l.call(0) }.should raise_error(ArgumentError) + end + end + + describe "taking |a| arguments" do + before :each do + @l = lambda { |a| a } + end + + it "assigns the value passed to the argument" do + @l.call(2).should == 2 + end + + it "does not destructure a single Array value" do + @l.call([1, 2]).should == [1, 2] + end + + it "does not call #to_ary to convert a single passed object to an Array" do + obj = mock("block yield to_ary") + obj.should_not_receive(:to_ary) + + @l.call(obj).should equal(obj) + end + + it "raises an ArgumentError if no value is passed" do + lambda { @l.call }.should raise_error(ArgumentError) + end + end + + describe "taking |a, b| arguments" do + before :each do + @l = lambda { |a, b| [a, b] } + end + + it "raises an ArgumentError if passed no values" do + lambda { @l.call }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if passed one value" do + lambda { @l.call(0) }.should raise_error(ArgumentError) + end + + it "assigns the values passed to the arguments" do + @l.call(1, 2).should == [1, 2] + end + + it "does not call #to_ary to convert a single passed object to an Array" do + obj = mock("proc call to_ary") + obj.should_not_receive(:to_ary) + + lambda { @l.call(obj) }.should raise_error(ArgumentError) + end + end + + describe "taking |a, *b| arguments" do + before :each do + @l = lambda { |a, *b| [a, b] } + end + + it "raises an ArgumentError if passed no values" do + lambda { @l.call }.should raise_error(ArgumentError) + end + + it "does not destructure a single Array value yielded" do + @l.call([1, 2, 3]).should == [[1, 2, 3], []] + end + + it "assigns all passed values after the first to the rest argument" do + @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 + obj = mock("block yield to_ary") + obj.should_not_receive(:to_ary) + + @l.call(obj).should == [obj, []] + end + end + + describe "taking |*| arguments" do + before :each do + @l = lambda { |*| 1 } + end + + it "does not raise an exception when passed no values" do + @l.call.should == 1 + end + + it "does not raise an exception when passed multiple values" do + @l.call(2, 3, 4).should == 1 + end + + it "does not call #to_ary to convert a single passed object to an Array" do + obj = mock("block yield to_ary") + obj.should_not_receive(:to_ary) + + @l.call(obj).should == 1 + end + end + + describe "taking |*a| arguments" do + before :each do + @l = lambda { |*a| a } + end + + it "assigns [] to the argument when passed no values" do + @l.call.should == [] + end + + it "assigns the argument an Array wrapping one passed value" do + @l.call(1).should == [1] + end + + it "assigns the argument an Array wrapping all values passed" do + @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 + obj = mock("block yield to_ary") + obj.should_not_receive(:to_ary) + + @l.call(obj).should == [obj] + end + end + + describe "taking |*a, b| arguments" do + it "assigns [] to the argument when passed no values" do + proc { |*a, b| [a, b] }.call.should == [[], nil] + end + end + + describe "taking |a, *b, c| arguments" do + it "assigns [] to the argument when passed no values" do + proc { |a, *b, c| [a, b, c] }.call.should == [nil, [], nil] + end + end + + describe "taking |a, | arguments" do + before :each do + @l = lambda { |a, | a } + end + + it "raises an ArgumentError when passed no values" do + lambda { @l.call }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed more than one value" do + lambda { @l.call(1, 2) }.should raise_error(ArgumentError) + end + + it "assigns the argument the value passed" do + @l.call(1).should == 1 + end + + it "does not destructure when passed a single Array" do + @l.call([1,2]).should == [1, 2] + end + + it "does not call #to_ary to convert a single passed object to an Array" do + obj = mock("block yield to_ary") + obj.should_not_receive(:to_ary) + + @l.call(obj).should == obj + end + end + + describe "taking |(a, b)| arguments" do + before :each do + @l = lambda { |(a, b)| [a, b] } + end + + it "raises an ArgumentError when passed no values" do + lambda { @l.call }.should raise_error(ArgumentError) + end + + it "destructures a single Array value yielded" do + @l.call([1, 2]).should == [1, 2] + end + + it "calls #to_ary to convert a single passed object to an Array" do + obj = mock("block yield to_ary") + obj.should_receive(:to_ary).and_return([1, 2]) + + @l.call(obj).should == [1, 2] + end + + it "raises a TypeError if #to_ary does not return an Array" do + obj = mock("block yield to_ary invalid") + obj.should_receive(:to_ary).and_return(1) + + 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 new file mode 100644 index 0000000000..57532553b3 --- /dev/null +++ b/spec/ruby/language/redo_spec.rb @@ -0,0 +1,66 @@ +require_relative '../spec_helper' + +describe "The redo statement" do + it "restarts block execution if used within block" do + a = [] + -> { + a << 1 + redo if a.size < 2 + a << 2 + }.call + a.should == [1, 1, 2] + end + + it "re-executes the closest loop" do + exist = [2,3] + processed = [] + order = [] + [1,2,3,4].each do |x| + order << x + begin + processed << x + if exist.include?(x) + raise StandardError, "included" + end + rescue StandardError + exist.delete(x) + redo + end + end + processed.should == [1,2,2,3,3,4] + exist.should == [] + order.should == [1,2,2,3,3,4] + end + + it "re-executes the last step in enumeration" do + list = [] + [1,2,3].each do |x| + list << x + break if list.size == 6 + redo if x == 3 + end + list.should == [1,2,3,3,3,3] + end + + it "triggers ensure block when re-executing a block" do + list = [] + [1,2,3].each do |x| + list << x + begin + list << 10*x + redo if list.count(1) == 1 + ensure + list << 100*x + end + end + list.should == [1,10,100,1,10,100,2,20,200,3,30,300] + end + + describe "in a method" do + it "is invalid and raises a SyntaxError" do + -> { + eval("def m; redo; end") + }.should raise_error(SyntaxError) + end + end +end diff --git a/spec/ruby/language/regexp/anchors_spec.rb b/spec/ruby/language/regexp/anchors_spec.rb new file mode 100644 index 0000000000..cdc06c0b4d --- /dev/null +++ b/spec/ruby/language/regexp/anchors_spec.rb @@ -0,0 +1,179 @@ +require_relative '../../spec_helper' +require_relative '../fixtures/classes' + +describe "Regexps with anchors" do + it "supports ^ (line start anchor)" do + # Basic matching + /^foo/.match("foo").to_a.should == ["foo"] + /^bar/.match("foo\nbar").to_a.should == ["bar"] + # Basic non-matching + /^foo/.match(" foo").should be_nil + /foo^/.match("foo\n\n\n").should be_nil + + # A bit advanced + /^^^foo/.match("foo").to_a.should == ["foo"] + (/^[^f]/ =~ "foo\n\n").should == "foo\n".size and $~.to_a.should == ["\n"] + (/($^)($^)/ =~ "foo\n\n").should == "foo\n".size and $~.to_a.should == ["", "", ""] + + # Different start of line chars + /^bar/.match("foo\rbar").should be_nil + /^bar/.match("foo\0bar").should be_nil + + # Trivial + /^/.match("foo").to_a.should == [""] + + # Grouping + /(^foo)/.match("foo").to_a.should == ["foo", "foo"] + /(^)/.match("foo").to_a.should == ["", ""] + /(foo\n^)(^bar)/.match("foo\nbar").to_a.should == ["foo\nbar", "foo\n", "bar"] + end + + it "does not match ^ after trailing \\n" do + /^(?!\A)/.match("foo\n").should be_nil # There is no (empty) line after a trailing \n + end + + it "supports $ (line end anchor)" do + # Basic matching + /foo$/.match("foo").to_a.should == ["foo"] + /foo$/.match("foo\nbar").to_a.should == ["foo"] + # Basic non-matching + /foo$/.match("foo ").should be_nil + /$foo/.match("\n\n\nfoo").should be_nil + + # A bit advanced + /foo$$$/.match("foo").to_a.should == ["foo"] + (/[^o]$/ =~ "foo\n\n").should == ("foo\n".size - 1) and $~.to_a.should == ["\n"] + + # Different end of line chars + /foo$/.match("foo\r\nbar").should be_nil + /foo$/.match("foo\0bar").should be_nil + + # Trivial + (/$/ =~ "foo").should == "foo".size and $~.to_a.should == [""] + + # Grouping + /(foo$)/.match("foo").to_a.should == ["foo", "foo"] + (/($)/ =~ "foo").should == "foo".size and $~.to_a.should == ["", ""] + /(foo$)($\nbar)/.match("foo\nbar").to_a.should == ["foo\nbar", "foo", "\nbar"] + end + + it "supports \\A (string start anchor)" do + # Basic matching + /\Afoo/.match("foo").to_a.should == ["foo"] + # Basic non-matching + /\Abar/.match("foo\nbar").should be_nil + /\Afoo/.match(" foo").should be_nil + + # A bit advanced + /\A\A\Afoo/.match("foo").to_a.should == ["foo"] + /(\A\Z)(\A\Z)/.match("").to_a.should == ["", "", ""] + + # Different start of line chars + /\Abar/.match("foo\0bar").should be_nil + + # Grouping + /(\Afoo)/.match("foo").to_a.should == ["foo", "foo"] + /(\A)/.match("foo").to_a.should == ["", ""] + end + + it "supports \\Z (string end anchor, including before trailing \\n)" do + # Basic matching + /foo\Z/.match("foo").to_a.should == ["foo"] + /foo\Z/.match("foo\n").to_a.should == ["foo"] + # Basic non-matching + /foo\Z/.match("foo\nbar").should be_nil + /foo\Z/.match("foo ").should be_nil + + # A bit advanced + /foo\Z\Z\Z/.match("foo\n").to_a.should == ["foo"] + (/($\Z)($\Z)/ =~ "foo\n").should == "foo".size and $~.to_a.should == ["", "", ""] + (/(\z\Z)(\z\Z)/ =~ "foo\n").should == "foo\n".size and $~.to_a.should == ["", "", ""] + + # Different end of line chars + /foo\Z/.match("foo\0bar").should be_nil + /foo\Z/.match("foo\r\n").should be_nil + + # Grouping + /(foo\Z)/.match("foo").to_a.should == ["foo", "foo"] + (/(\Z)/ =~ "foo").should == "foo".size and $~.to_a.should == ["", ""] + end + + it "supports \\z (string end anchor)" do + # Basic matching + /foo\z/.match("foo").to_a.should == ["foo"] + # Basic non-matching + /foo\z/.match("foo\nbar").should be_nil + /foo\z/.match("foo\n").should be_nil + /foo\z/.match("foo ").should be_nil + + # A bit advanced + /foo\z\z\z/.match("foo").to_a.should == ["foo"] + (/($\z)($\z)/ =~ "foo").should == "foo".size and $~.to_a.should == ["", "", ""] + + # Different end of line chars + /foo\z/.match("foo\0bar").should be_nil + /foo\z/.match("foo\r\nbar").should be_nil + + # Grouping + /(foo\z)/.match("foo").to_a.should == ["foo", "foo"] + (/(\z)/ =~ "foo").should == "foo".size and $~.to_a.should == ["", ""] + end + + it "supports \\b (word boundary)" do + # Basic matching + /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"] + end + LanguageSpecs.non_alphanum_non_space.scan(/./).each do |c| + /foo\b/.match("foo" + c).to_a.should == ["foo"] + end + /foo\b/.match("foo\0").to_a.should == ["foo"] + # Basic non-matching + /foo\b/.match("foobar").should be_nil + /foo\b/.match("foo123").should be_nil + /foo\b/.match("foo_").should be_nil + end + + it "supports \\B (non-word-boundary)" do + # Basic matching + /foo\B/.match("foobar").to_a.should == ["foo"] + /foo\B/.match("foo123").to_a.should == ["foo"] + /foo\B/.match("foo_").to_a.should == ["foo"] + # Basic non-matching + /foo\B/.match("foo").should be_nil + /foo\B/.match("foo\n").should be_nil + LanguageSpecs.white_spaces.scan(/./).each do |c| + /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 + end + /foo\B/.match("foo\0").should be_nil + end + + it "supports (?= ) (positive lookahead)" do + /foo.(?=bar)/.match("foo1 foo2bar").to_a.should == ["foo2"] + end + + it "supports (?! ) (negative lookahead)" do + /foo.(?!bar)/.match("foo1bar foo2").to_a.should == ["foo2"] + end + + it "supports (?!<) (negative lookbehind)" do + /(?<!foo)bar./.match("foobar1 bar2").to_a.should == ["bar2"] + end + + it "supports (?<=) (positive lookbehind)" do + /(?<=foo)bar./.match("bar1 foobar2").to_a.should == ["bar2"] + end + + it "supports (?<=\\b) (positive lookbehind with word boundary)" do + /(?<=\bfoo)bar./.match("1foobar1 foobar2").to_a.should == ["bar2"] + end + + it "supports (?!<\\b) (negative lookbehind with word boundary)" do + /(?<!\bfoo)bar./.match("foobar1 1foobar2").to_a.should == ["bar2"] + end +end diff --git a/spec/ruby/language/regexp/back-references_spec.rb b/spec/ruby/language/regexp/back-references_spec.rb new file mode 100644 index 0000000000..627c8daace --- /dev/null +++ b/spec/ruby/language/regexp/back-references_spec.rb @@ -0,0 +1,149 @@ +require_relative '../../spec_helper' +require_relative '../fixtures/classes' + +describe "Regexps with back-references" do + it "saves match data in the $~ pseudo-global variable" do + "hello" =~ /l+/ + $~.to_a.should == ["ll"] + end + + 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" + $2.should == "2" + $3.should == "3" + $4.should == "4" + $5.should == "5" + $6.should == "6" + $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 + cap1, cap2, cap3 = nil + "foo" =~ /(o+)/ + cap1 = [$~.to_a, $1] + Thread.new do + cap2 = [$~.to_a, $1] + "bar" =~ /(a)/ + cap3 = [$~.to_a, $1] + end.join + cap4 = [$~.to_a, $1] + cap1.should == [["oo", "oo"], "oo"] + cap2.should == [[], nil] + cap3.should == [["a", "a"], "a"] + cap4.should == [["oo", "oo"], "oo"] + end + + it "supports \<n> (backreference to previous group match)" do + /(foo.)\1/.match("foo1foo1").to_a.should == ["foo1foo1", "foo1"] + /(foo.)\1/.match("foo1foo2").should be_nil + end + + it "resets nested \<n> backreference before match of outer subexpression" do + /(a\1?){2}/.match("aaaa").to_a.should == ["aa", "a"] + end + + it "does not reset enclosed capture groups" do + /((a)|(b))+/.match("ab").captures.should == [ "b", "a", "b" ] + end + + it "can match an optional quote, followed by content, followed by a matching quote, as the whole string" do + /^("|)(.*)\1$/.match('x').to_a.should == ["x", "", "x"] + end + + it "allows forward references" do + /(?:(\2)|(.))+/.match("aa").to_a.should == [ "aa", "a", "a" ] + end + + it "disallows forward references >= 10" do + (/\10()()()()()()()()()()/ =~ "\x08").should == 0 + end + + it "fails when trying to match a backreference to an unmatched capture group" do + /\1()/.match("").should == nil + /(?:(a)|b)\1/.match("b").should == nil + end + + it "ignores backreferences > 1000" do + /\99999/.match("99999")[0].should == "99999" + end + + it "0 is not a valid backreference" do + -> { Regexp.new("\\k<0>") }.should raise_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 new file mode 100644 index 0000000000..018757db41 --- /dev/null +++ b/spec/ruby/language/regexp/character_classes_spec.rb @@ -0,0 +1,647 @@ +# coding: utf-8 +require_relative '../../spec_helper' +require_relative '../fixtures/classes' + +describe "Regexp with character classes" do + it "supports \\w (word character)" do + /\w/.match("a").to_a.should == ["a"] + /\w/.match("1").to_a.should == ["1"] + /\w/.match("_").to_a.should == ["_"] + + # Non-matches + /\w/.match(LanguageSpecs.white_spaces).should be_nil + /\w/.match(LanguageSpecs.non_alphanum_non_space).should be_nil + /\w/.match("\0").should be_nil + end + + it "supports \\W (non-word character)" do + /\W+/.match(LanguageSpecs.white_spaces).to_a.should == [LanguageSpecs.white_spaces] + /\W+/.match(LanguageSpecs.non_alphanum_non_space).to_a.should == [LanguageSpecs.non_alphanum_non_space] + /\W/.match("\0").to_a.should == ["\0"] + + # Non-matches + /\W/.match("a").should be_nil + /\W/.match("1").should be_nil + /\W/.match("_").should be_nil + end + + it "supports \\s (space character)" do + /\s+/.match(LanguageSpecs.white_spaces).to_a.should == [LanguageSpecs.white_spaces] + + # Non-matches + /\s/.match("a").should be_nil + /\s/.match("1").should be_nil + /\s/.match(LanguageSpecs.non_alphanum_non_space).should be_nil + /\s/.match("\0").should be_nil + end + + it "supports \\S (non-space character)" do + /\S/.match("a").to_a.should == ["a"] + /\S/.match("1").to_a.should == ["1"] + /\S+/.match(LanguageSpecs.non_alphanum_non_space).to_a.should == [LanguageSpecs.non_alphanum_non_space] + /\S/.match("\0").to_a.should == ["\0"] + + # Non-matches + /\S/.match(LanguageSpecs.white_spaces).should be_nil + end + + it "supports \\d (numeric digit)" do + /\d/.match("1").to_a.should == ["1"] + + # Non-matches + /\d/.match("a").should be_nil + /\d/.match(LanguageSpecs.white_spaces).should be_nil + /\d/.match(LanguageSpecs.non_alphanum_non_space).should be_nil + /\d/.match("\0").should be_nil + end + + it "supports \\D (non-digit)" do + /\D/.match("a").to_a.should == ["a"] + /\D+/.match(LanguageSpecs.white_spaces).to_a.should == [LanguageSpecs.white_spaces] + /\D+/.match(LanguageSpecs.non_alphanum_non_space).to_a.should == [LanguageSpecs.non_alphanum_non_space] + /\D/.match("\0").to_a.should == ["\0"] + + # Non-matches + /\D/.match("1").should be_nil + end + + it "supports [] (character class)" do + /[a-z]+/.match("fooBAR").to_a.should == ["foo"] + /[\b]/.match("\b").to_a.should == ["\b"] # \b inside character class is backspace + end + + it "supports [[:alpha:][:digit:][:etc:]] (predefined character classes)" do + /[[:alnum:]]+/.match("a1").to_a.should == ["a1"] + /[[:alpha:]]+/.match("Aa1").to_a.should == ["Aa"] + /[[:blank:]]+/.match(LanguageSpecs.white_spaces).to_a.should == [LanguageSpecs.blanks] + # /[[:cntrl:]]/.match("").to_a.should == [""] # TODO: what should this match? + /[[:digit:]]/.match("1").to_a.should == ["1"] + # /[[:graph:]]/.match("").to_a.should == [""] # TODO: what should this match? + /[[:lower:]]+/.match("Aa1").to_a.should == ["a"] + /[[:print:]]+/.match(LanguageSpecs.white_spaces).to_a.should == [" "] # include all of multibyte encoded characters + /[[:punct:]]+/.match(LanguageSpecs.punctuations).to_a.should == [LanguageSpecs.punctuations] + /[[:space:]]+/.match(LanguageSpecs.white_spaces).to_a.should == [LanguageSpecs.white_spaces] + /[[:upper:]]+/.match("123ABCabc").to_a.should == ["ABC"] + /[[:xdigit:]]+/.match("xyz0123456789ABCDEFabcdefXYZ").to_a.should == ["0123456789ABCDEFabcdef"] + + # Parsing + /[[:lower:][:digit:]A-C]+/.match("a1ABCDEF").to_a.should == ["a1ABC"] # can be composed with other constructs in the character class + /[^[:lower:]A-C]+/.match("abcABCDEF123def").to_a.should == ["DEF123"] # negated character class + /[:alnum:]+/.match("a:l:n:u:m").to_a.should == ["a:l:n:u:m"] # should behave like regular character class composed of the individual letters + /[\[:alnum:]+/.match("[:a:l:n:u:m").to_a.should == ["[:a:l:n:u:m"] # should behave like regular character class composed of the individual letters + -> { eval('/[[:alpha:]-[:digit:]]/') }.should raise_error(SyntaxError) # can't use character class as a start value of range + end + + it "matches ASCII characters with [[:ascii:]]" do + "\x00".match(/[[:ascii:]]/).to_a.should == ["\x00"] + "\x7F".match(/[[:ascii:]]/).to_a.should == ["\x7F"] + end + + not_supported_on :opal do + it "doesn't match non-ASCII characters with [[:ascii:]]" do + /[[:ascii:]]/.match("\u{80}").should be_nil + /[[:ascii:]]/.match("\u{9898}").should be_nil + end + end + + it "matches Unicode letter characters with [[:alnum:]]" do + "à".match(/[[:alnum:]]/).to_a.should == ["à"] + end + + it "matches Unicode digits with [[:alnum:]]" do + "\u{0660}".match(/[[:alnum:]]/).to_a.should == ["\u{0660}"] + end + + it "doesn't matches Unicode marks with [[:alnum:]]" do + "\u{3099}".match(/[[:alnum:]]/).should be_nil + end + + it "doesn't match Unicode control characters with [[:alnum:]]" do + "\u{16}".match(/[[:alnum:]]/).to_a.should == [] + end + + it "doesn't match Unicode punctuation characters with [[:alnum:]]" do + "\u{3F}".match(/[[:alnum:]]/).to_a.should == [] + end + + it "matches Unicode letter characters with [[:alpha:]]" do + "à".match(/[[:alpha:]]/).to_a.should == ["à"] + end + + it "doesn't match Unicode digits with [[:alpha:]]" do + "\u{0660}".match(/[[:alpha:]]/).to_a.should == [] + end + + it "doesn't matches Unicode marks with [[:alpha:]]" do + "\u{3099}".match(/[[:alpha:]]/).should be_nil + end + + it "doesn't match Unicode control characters with [[:alpha:]]" do + "\u{16}".match(/[[:alpha:]]/).to_a.should == [] + end + + it "doesn't match Unicode punctuation characters with [[:alpha:]]" do + "\u{3F}".match(/[[:alpha:]]/).to_a.should == [] + end + + it "matches Unicode space characters with [[:blank:]]" do + "\u{1680}".match(/[[:blank:]]/).to_a.should == ["\u{1680}"] + end + + it "doesn't match Unicode control characters with [[:blank:]]" do + "\u{16}".match(/[[:blank:]]/).should be_nil + end + + it "doesn't match Unicode punctuation characters with [[:blank:]]" do + "\u{3F}".match(/[[:blank:]]/).should be_nil + end + + it "doesn't match Unicode letter characters with [[:blank:]]" do + "à".match(/[[:blank:]]/).should be_nil + end + + it "doesn't match Unicode digits with [[:blank:]]" do + "\u{0660}".match(/[[:blank:]]/).should be_nil + end + + it "doesn't match Unicode marks with [[:blank:]]" do + "\u{36F}".match(/[[:blank:]]/).should be_nil + end + + it "doesn't Unicode letter characters with [[:cntrl:]]" do + "à".match(/[[:cntrl:]]/).should be_nil + end + + it "doesn't match Unicode digits with [[:cntrl:]]" do + "\u{0660}".match(/[[:cntrl:]]/).should be_nil + end + + it "doesn't match Unicode marks with [[:cntrl:]]" do + "\u{36F}".match(/[[:cntrl:]]/).should be_nil + end + + it "doesn't match Unicode punctuation characters with [[:cntrl:]]" do + "\u{3F}".match(/[[:cntrl:]]/).should be_nil + end + + it "matches Unicode control characters with [[:cntrl:]]" do + "\u{16}".match(/[[:cntrl:]]/).to_a.should == ["\u{16}"] + end + + it "doesn't match Unicode format characters with [[:cntrl:]]" do + "\u{2060}".match(/[[:cntrl:]]/).should be_nil + end + + it "doesn't match Unicode private-use characters with [[:cntrl:]]" do + "\u{E001}".match(/[[:cntrl:]]/).should be_nil + end + + it "doesn't match Unicode letter characters with [[:digit:]]" do + "à".match(/[[:digit:]]/).should be_nil + end + + it "matches Unicode digits with [[:digit:]]" do + "\u{0660}".match(/[[:digit:]]/).to_a.should == ["\u{0660}"] + "\u{FF12}".match(/[[:digit:]]/).to_a.should == ["\u{FF12}"] + end + + it "doesn't match Unicode marks with [[:digit:]]" do + "\u{36F}".match(/[[:digit:]]/).should be_nil + end + + it "doesn't match Unicode punctuation characters with [[:digit:]]" do + "\u{3F}".match(/[[:digit:]]/).should be_nil + end + + it "doesn't match Unicode control characters with [[:digit:]]" do + "\u{16}".match(/[[:digit:]]/).should be_nil + end + + it "doesn't match Unicode format characters with [[:digit:]]" do + "\u{2060}".match(/[[:digit:]]/).should be_nil + end + + it "doesn't match Unicode private-use characters with [[:digit:]]" do + "\u{E001}".match(/[[:digit:]]/).should be_nil + end + + it "matches Unicode letter characters with [[:graph:]]" do + "à".match(/[[:graph:]]/).to_a.should == ["à"] + end + + it "matches Unicode digits with [[:graph:]]" do + "\u{0660}".match(/[[:graph:]]/).to_a.should == ["\u{0660}"] + "\u{FF12}".match(/[[:graph:]]/).to_a.should == ["\u{FF12}"] + end + + it "matches Unicode marks with [[:graph:]]" do + "\u{36F}".match(/[[:graph:]]/).to_a.should ==["\u{36F}"] + end + + it "matches Unicode punctuation characters with [[:graph:]]" do + "\u{3F}".match(/[[:graph:]]/).to_a.should == ["\u{3F}"] + end + + it "doesn't match Unicode control characters with [[:graph:]]" do + "\u{16}".match(/[[:graph:]]/).should be_nil + end + + it "match Unicode format characters with [[:graph:]]" do + "\u{2060}".match(/[[:graph:]]/).to_a.should == ["\u2060"] + end + + it "match Unicode private-use characters with [[:graph:]]" do + "\u{E001}".match(/[[:graph:]]/).to_a.should == ["\u{E001}"] + end + + it "matches Unicode lowercase letter characters with [[:lower:]]" do + "\u{FF41}".match(/[[:lower:]]/).to_a.should == ["\u{FF41}"] + "\u{1D484}".match(/[[:lower:]]/).to_a.should == ["\u{1D484}"] + "\u{E8}".match(/[[:lower:]]/).to_a.should == ["\u{E8}"] + end + + it "doesn't match Unicode uppercase letter characters with [[:lower:]]" do + "\u{100}".match(/[[:lower:]]/).should be_nil + "\u{130}".match(/[[:lower:]]/).should be_nil + "\u{405}".match(/[[:lower:]]/).should be_nil + end + + it "doesn't match Unicode title-case characters with [[:lower:]]" do + "\u{1F88}".match(/[[:lower:]]/).should be_nil + "\u{1FAD}".match(/[[:lower:]]/).should be_nil + "\u{01C5}".match(/[[:lower:]]/).should be_nil + end + + it "doesn't match Unicode digits with [[:lower:]]" do + "\u{0660}".match(/[[:lower:]]/).should be_nil + "\u{FF12}".match(/[[:lower:]]/).should be_nil + end + + it "doesn't match Unicode marks with [[:lower:]]" do + "\u{36F}".match(/[[:lower:]]/).should be_nil + end + + it "doesn't match Unicode punctuation characters with [[:lower:]]" do + "\u{3F}".match(/[[:lower:]]/).should be_nil + end + + it "doesn't match Unicode control characters with [[:lower:]]" do + "\u{16}".match(/[[:lower:]]/).should be_nil + end + + it "doesn't match Unicode format characters with [[:lower:]]" do + "\u{2060}".match(/[[:lower:]]/).should be_nil + end + + it "doesn't match Unicode private-use characters with [[:lower:]]" do + "\u{E001}".match(/[[:lower:]]/).should be_nil + end + + it "matches Unicode lowercase letter characters with [[:print:]]" do + "\u{FF41}".match(/[[:print:]]/).to_a.should == ["\u{FF41}"] + "\u{1D484}".match(/[[:print:]]/).to_a.should == ["\u{1D484}"] + "\u{E8}".match(/[[:print:]]/).to_a.should == ["\u{E8}"] + end + + it "matches Unicode uppercase letter characters with [[:print:]]" do + "\u{100}".match(/[[:print:]]/).to_a.should == ["\u{100}"] + "\u{130}".match(/[[:print:]]/).to_a.should == ["\u{130}"] + "\u{405}".match(/[[:print:]]/).to_a.should == ["\u{405}"] + end + + it "matches Unicode title-case characters with [[:print:]]" do + "\u{1F88}".match(/[[:print:]]/).to_a.should == ["\u{1F88}"] + "\u{1FAD}".match(/[[:print:]]/).to_a.should == ["\u{1FAD}"] + "\u{01C5}".match(/[[:print:]]/).to_a.should == ["\u{01C5}"] + end + + it "matches Unicode digits with [[:print:]]" do + "\u{0660}".match(/[[:print:]]/).to_a.should == ["\u{0660}"] + "\u{FF12}".match(/[[:print:]]/).to_a.should == ["\u{FF12}"] + end + + it "matches Unicode marks with [[:print:]]" do + "\u{36F}".match(/[[:print:]]/).to_a.should == ["\u{36F}"] + end + + it "matches Unicode punctuation characters with [[:print:]]" do + "\u{3F}".match(/[[:print:]]/).to_a.should == ["\u{3F}"] + end + + it "doesn't match Unicode control characters with [[:print:]]" do + "\u{16}".match(/[[:print:]]/).should be_nil + end + + it "match Unicode format characters with [[:print:]]" do + "\u{2060}".match(/[[:print:]]/).to_a.should == ["\u{2060}"] + end + + it "match Unicode private-use characters with [[:print:]]" do + "\u{E001}".match(/[[:print:]]/).to_a.should == ["\u{E001}"] + end + + + it "doesn't match Unicode lowercase letter characters with [[:punct:]]" do + "\u{FF41}".match(/[[:punct:]]/).should be_nil + "\u{1D484}".match(/[[:punct:]]/).should be_nil + "\u{E8}".match(/[[:punct:]]/).should be_nil + end + + it "doesn't match Unicode uppercase letter characters with [[:punct:]]" do + "\u{100}".match(/[[:punct:]]/).should be_nil + "\u{130}".match(/[[:punct:]]/).should be_nil + "\u{405}".match(/[[:punct:]]/).should be_nil + end + + it "doesn't match Unicode title-case characters with [[:punct:]]" do + "\u{1F88}".match(/[[:punct:]]/).should be_nil + "\u{1FAD}".match(/[[:punct:]]/).should be_nil + "\u{01C5}".match(/[[:punct:]]/).should be_nil + end + + it "doesn't match Unicode digits with [[:punct:]]" do + "\u{0660}".match(/[[:punct:]]/).should be_nil + "\u{FF12}".match(/[[:punct:]]/).should be_nil + end + + it "doesn't match Unicode marks with [[:punct:]]" do + "\u{36F}".match(/[[:punct:]]/).should be_nil + end + + it "matches Unicode Pc characters with [[:punct:]]" do + "\u{203F}".match(/[[:punct:]]/).to_a.should == ["\u{203F}"] + end + + it "matches Unicode Pd characters with [[:punct:]]" do + "\u{2E17}".match(/[[:punct:]]/).to_a.should == ["\u{2E17}"] + end + + it "matches Unicode Ps characters with [[:punct:]]" do + "\u{0F3A}".match(/[[:punct:]]/).to_a.should == ["\u{0F3A}"] + end + + it "matches Unicode Pe characters with [[:punct:]]" do + "\u{2046}".match(/[[:punct:]]/).to_a.should == ["\u{2046}"] + end + + it "matches Unicode Pi characters with [[:punct:]]" do + "\u{00AB}".match(/[[:punct:]]/).to_a.should == ["\u{00AB}"] + end + + it "matches Unicode Pf characters with [[:punct:]]" do + "\u{201D}".match(/[[:punct:]]/).to_a.should == ["\u{201D}"] + "\u{00BB}".match(/[[:punct:]]/).to_a.should == ["\u{00BB}"] + end + + it "matches Unicode Po characters with [[:punct:]]" do + "\u{00BF}".match(/[[:punct:]]/).to_a.should == ["\u{00BF}"] + end + + it "doesn't match Unicode format characters with [[:punct:]]" do + "\u{2060}".match(/[[:punct:]]/).should be_nil + end + + it "doesn't match Unicode private-use characters with [[:punct:]]" do + "\u{E001}".match(/[[:punct:]]/).should be_nil + end + + it "doesn't match Unicode lowercase letter characters with [[:space:]]" do + "\u{FF41}".match(/[[:space:]]/).should be_nil + "\u{1D484}".match(/[[:space:]]/).should be_nil + "\u{E8}".match(/[[:space:]]/).should be_nil + end + + it "doesn't match Unicode uppercase letter characters with [[:space:]]" do + "\u{100}".match(/[[:space:]]/).should be_nil + "\u{130}".match(/[[:space:]]/).should be_nil + "\u{405}".match(/[[:space:]]/).should be_nil + end + + it "doesn't match Unicode title-case characters with [[:space:]]" do + "\u{1F88}".match(/[[:space:]]/).should be_nil + "\u{1FAD}".match(/[[:space:]]/).should be_nil + "\u{01C5}".match(/[[:space:]]/).should be_nil + end + + it "doesn't match Unicode digits with [[:space:]]" do + "\u{0660}".match(/[[:space:]]/).should be_nil + "\u{FF12}".match(/[[:space:]]/).should be_nil + end + + it "doesn't match Unicode marks with [[:space:]]" do + "\u{36F}".match(/[[:space:]]/).should be_nil + end + + it "matches Unicode Zs characters with [[:space:]]" do + "\u{205F}".match(/[[:space:]]/).to_a.should == ["\u{205F}"] + end + + it "matches Unicode Zl characters with [[:space:]]" do + "\u{2028}".match(/[[:space:]]/).to_a.should == ["\u{2028}"] + end + + it "matches Unicode Zp characters with [[:space:]]" do + "\u{2029}".match(/[[:space:]]/).to_a.should == ["\u{2029}"] + end + + it "doesn't match Unicode format characters with [[:space:]]" do + "\u{2060}".match(/[[:space:]]/).should be_nil + end + + it "doesn't match Unicode private-use characters with [[:space:]]" do + "\u{E001}".match(/[[:space:]]/).should be_nil + end + + it "doesn't match Unicode lowercase characters with [[:upper:]]" do + "\u{FF41}".match(/[[:upper:]]/).should be_nil + "\u{1D484}".match(/[[:upper:]]/).should be_nil + "\u{E8}".match(/[[:upper:]]/).should be_nil + end + + it "matches Unicode uppercase characters with [[:upper:]]" do + "\u{100}".match(/[[:upper:]]/).to_a.should == ["\u{100}"] + "\u{130}".match(/[[:upper:]]/).to_a.should == ["\u{130}"] + "\u{405}".match(/[[:upper:]]/).to_a.should == ["\u{405}"] + end + + it "doesn't match Unicode title-case characters with [[:upper:]]" do + "\u{1F88}".match(/[[:upper:]]/).should be_nil + "\u{1FAD}".match(/[[:upper:]]/).should be_nil + "\u{01C5}".match(/[[:upper:]]/).should be_nil + end + + it "doesn't match Unicode digits with [[:upper:]]" do + "\u{0660}".match(/[[:upper:]]/).should be_nil + "\u{FF12}".match(/[[:upper:]]/).should be_nil + end + + it "doesn't match Unicode marks with [[:upper:]]" do + "\u{36F}".match(/[[:upper:]]/).should be_nil + end + + it "doesn't match Unicode punctuation characters with [[:upper:]]" do + "\u{3F}".match(/[[:upper:]]/).should be_nil + end + + it "doesn't match Unicode control characters with [[:upper:]]" do + "\u{16}".match(/[[:upper:]]/).should be_nil + end + + it "doesn't match Unicode format characters with [[:upper:]]" do + "\u{2060}".match(/[[:upper:]]/).should be_nil + end + + it "doesn't match Unicode private-use characters with [[:upper:]]" do + "\u{E001}".match(/[[:upper:]]/).should be_nil + end + + it "doesn't match Unicode letter characters [^a-fA-F] with [[:xdigit:]]" do + "à".match(/[[:xdigit:]]/).should be_nil + "g".match(/[[:xdigit:]]/).should be_nil + "X".match(/[[:xdigit:]]/).should be_nil + end + + it "matches Unicode letter characters [a-fA-F] with [[:xdigit:]]" do + "a".match(/[[:xdigit:]]/).to_a.should == ["a"] + "F".match(/[[:xdigit:]]/).to_a.should == ["F"] + end + + it "doesn't match Unicode digits [^0-9] with [[:xdigit:]]" do + "\u{0660}".match(/[[:xdigit:]]/).should be_nil + "\u{FF12}".match(/[[:xdigit:]]/).should be_nil + end + + it "doesn't match Unicode marks with [[:xdigit:]]" do + "\u{36F}".match(/[[:xdigit:]]/).should be_nil + end + + it "doesn't match Unicode punctuation characters with [[:xdigit:]]" do + "\u{3F}".match(/[[:xdigit:]]/).should be_nil + end + + it "doesn't match Unicode control characters with [[:xdigit:]]" do + "\u{16}".match(/[[:xdigit:]]/).should be_nil + end + + it "doesn't match Unicode format characters with [[:xdigit:]]" do + "\u{2060}".match(/[[:xdigit:]]/).should be_nil + end + + it "doesn't match Unicode private-use characters with [[:xdigit:]]" do + "\u{E001}".match(/[[:xdigit:]]/).should be_nil + end + + it "matches Unicode lowercase characters with [[:word:]]" do + "\u{FF41}".match(/[[:word:]]/).to_a.should == ["\u{FF41}"] + "\u{1D484}".match(/[[:word:]]/).to_a.should == ["\u{1D484}"] + "\u{E8}".match(/[[:word:]]/).to_a.should == ["\u{E8}"] + end + + it "matches Unicode uppercase characters with [[:word:]]" do + "\u{100}".match(/[[:word:]]/).to_a.should == ["\u{100}"] + "\u{130}".match(/[[:word:]]/).to_a.should == ["\u{130}"] + "\u{405}".match(/[[:word:]]/).to_a.should == ["\u{405}"] + end + + it "matches Unicode title-case characters with [[:word:]]" do + "\u{1F88}".match(/[[:word:]]/).to_a.should == ["\u{1F88}"] + "\u{1FAD}".match(/[[:word:]]/).to_a.should == ["\u{1FAD}"] + "\u{01C5}".match(/[[:word:]]/).to_a.should == ["\u{01C5}"] + end + + it "matches Unicode decimal digits with [[:word:]]" do + "\u{FF10}".match(/[[:word:]]/).to_a.should == ["\u{FF10}"] + "\u{096C}".match(/[[:word:]]/).to_a.should == ["\u{096C}"] + end + + it "matches Unicode marks with [[:word:]]" do + "\u{36F}".match(/[[:word:]]/).to_a.should == ["\u{36F}"] + end + + it "match Unicode Nl characters with [[:word:]]" 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 + it "doesn't match Unicode punctuation characters with [[:word:]]" do + "\u{3F}".match(/[[:word:]]/).should be_nil + end + + it "doesn't match Unicode control characters with [[:word:]]" do + "\u{16}".match(/[[:word:]]/).should be_nil + end + + it "doesn't match Unicode format characters with [[:word:]]" do + "\u{2060}".match(/[[:word:]]/).should be_nil + end + + it "doesn't match Unicode private-use characters with [[:word:]]" do + "\u{E001}".match(/[[:word:]]/).should be_nil + end + + it "matches unicode named character properties" do + "a1".match(/\p{Alpha}/).to_a.should == ["a"] + end + + it "matches unicode abbreviated character properties" do + "a1".match(/\p{L}/).to_a.should == ["a"] + end + + it "matches unicode script properties" do + "a\u06E9b".match(/\p{Arabic}/).to_a.should == ["\u06E9"] + end + + it "matches unicode Han properties" do + "松本行弘 Ruby".match(/\p{Han}+/u).to_a.should == ["松本行弘"] + end + + it "matches unicode Hiragana properties" do + "Ruby(ルビー)、まつもとゆきひろ".match(/\p{Hiragana}+/u).to_a.should == ["まつもとゆきひろ"] + end + + it "matches unicode Katakana properties" do + "Ruby(ルビー)、まつもとゆきひろ".match(/\p{Katakana}+/u).to_a.should == ["ルビ"] + end + + it "matches unicode Hangul properties" do + "루비(Ruby)".match(/\p{Hangul}+/u).to_a.should == ["루비"] + end + + it "supports negated property condition" do + "a".match(eval("/\P{L}/")).should be_nil + "1".match(eval("/\P{N}/")).should be_nil + end + + it "raises a RegexpError for an unterminated unicode property" do + -> { Regexp.new('\p{') }.should raise_error(RegexpError) + end + + it "supports \\X (unicode 9.0 with UTR #51 workarounds)" do + # 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 == ["🤘🏽"] + + # 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 == ["👩👩👧👦"] + + # 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 new file mode 100644 index 0000000000..ceb9cf823a --- /dev/null +++ b/spec/ruby/language/regexp/encoding_spec.rb @@ -0,0 +1,152 @@ +# 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".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".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".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 + /./e.encoding.should == Encoding::EUC_JP + end + + it 'preserves EUC-JP as /e encoding through interpolation' do + /#{/./}/e.encoding.should == Encoding::EUC_JP + end + + it "supports /n (No encoding)" do + /./n.match("\303\251").to_a.should == ["\303"] + end + + it "supports /n (No encoding) with interpolation" do + /#{/./}/n.match("\303\251").to_a.should == ["\303"] + end + + it "supports /n (No encoding) with interpolation /o" 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 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 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".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".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".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 + /./s.encoding.should == Encoding::Windows_31J + end + + it 'preserves Windows-31J as /s encoding through interpolation' do + /#{/./}/s.encoding.should == Encoding::Windows_31J + end + + it "supports /u (UTF8 encoding)" do + /./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".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".dup.force_encoding('utf-8')).to_a.should == ["\u{e9}"] + end + + it 'uses UTF-8 as /u encoding' do + /./u.encoding.should == Encoding::UTF_8 + end + + it 'preserves UTF-8 as /u encoding through interpolation' do + /#{/./}/u.encoding.should == Encoding::UTF_8 + end + + 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 new file mode 100644 index 0000000000..541998b937 --- /dev/null +++ b/spec/ruby/language/regexp/escapes_spec.rb @@ -0,0 +1,169 @@ +# 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 "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 + /\r/.match("\r").to_a.should == ["\r"] # return + /\f/.match("\f").to_a.should == ["\f"] # form feed + /\a/.match("\a").to_a.should == ["\a"] # bell + /\e/.match("\e").to_a.should == ["\e"] # escape + + # \nnn octal char (encoded byte value) + end + + it "supports quoting meta-characters via escape sequence" do + # parenthesis, etc + /\(/.match("(").to_a.should == ["("] + /\)/.match(")").to_a.should == [")"] + /\[/.match("[").to_a.should == ["["] + /\]/.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 + /\?/.match("?").to_a.should == ["?"] + /\./.match(".").to_a.should == ["."] + /\*/.match("*").to_a.should == ["*"] + /\+/.match("+").to_a.should == ["+"] + # line anchors + /\^/.match("^").to_a.should == ["^"] + /\$/.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 "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 + -> { eval('/\xG/') }.should raise_error(SyntaxError) + + # \x{7HHHHHHH} wide hexadecimal char (character code point value) + end + + 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"] + /\c(\cH\ch/.match("\b\b\b").to_a.should == ["\b\b\b"] + /\c)\cI\ci/.match("\t\t\t").to_a.should == ["\t\t\t"] + /\c*\cJ\cj/.match("\n\n\n").to_a.should == ["\n\n\n"] + /\c+\cK\ck/.match("\v\v\v").to_a.should == ["\v\v\v"] + /\c,\cL\cl/.match("\f\f\f").to_a.should == ["\f\f\f"] + /\c-\cM\cm/.match("\r\r\r").to_a.should == ["\r\r\r"] + + /\cJ/.match("\r").should be_nil + + # Parsing precedence + /\cJ+/.match("\n\n").to_a.should == ["\n\n"] # Quantifiers apply to entire escape sequence + /\\cJ/.match("\\cJ").to_a.should == ["\\cJ"] + -> { eval('/[abc\x]/') }.should raise_error(SyntaxError) # \x is treated as a escape sequence even inside a character class + # Syntax error + -> { 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 new file mode 100644 index 0000000000..313858f714 --- /dev/null +++ b/spec/ruby/language/regexp/grouping_spec.rb @@ -0,0 +1,63 @@ +require_relative '../../spec_helper' +require_relative '../fixtures/classes' + +describe "Regexps with grouping" do + it "support ()" do + /(a)/.match("a").to_a.should == ["a", "a"] + end + + it "allows groups to be nested" do + md = /(hay(st)a)ck/.match('haystack') + md.to_a.should == ['haystack','haysta', 'st'] + end + + it "raises a SyntaxError when parentheses aren't balanced" do + -> { eval "/(hay(st)ack/" }.should raise_error(SyntaxError) + end + + it "supports (?: ) (non-capturing group)" do + /(?:foo)(bar)/.match("foobar").to_a.should == ["foobar", "bar"] + # 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 new file mode 100644 index 0000000000..6951fd38ca --- /dev/null +++ b/spec/ruby/language/regexp/interpolation_spec.rb @@ -0,0 +1,58 @@ +require_relative '../../spec_helper' +require_relative '../fixtures/classes' + +describe "Regexps with interpolation" do + + it "allows interpolation of strings" do + str = "foo|bar" + /#{str}/.should == /foo|bar/ + end + + it "allows interpolation of literal regexps" do + re = /foo|bar/ + /#{re}/.should == /(?-mix:foo|bar)/ + end + + it "allows interpolation of any object that responds to to_s" do + o = Object.new + def o.to_s + "object_with_to_s" + end + /#{o}/.should == /object_with_to_s/ + end + + it "allows interpolation which mixes modifiers" do + re = /foo/i + /#{re} bar/m.should == /(?i-mx:foo) bar/m + end + + it "allows interpolation to interact with other Regexp constructs" do + str = "foo)|(bar" + /(#{str})/.should == /(foo)|(bar)/ + + str = "a" + /[#{str}-z]/.should == /[a-z]/ + end + + it "gives precedence to escape sequences over substitution" do + str = "J" + /\c#{str}/.to_s.should include('{str}') + end + + it "throws RegexpError for malformed interpolation" do + s = "" + -> { /(#{s}/ }.should raise_error(RegexpError) + s = "(" + -> { /#{s}/ }.should raise_error(RegexpError) + end + + it "allows interpolation in extended mode" do + var = "#comment\n foo #comment\n | bar" + (/#{var}/x =~ "foo").should == (/foo|bar/ =~ "foo") + end + + it "allows escape sequences in interpolated regexps" do + escape_seq = %r{"\x80"}n + %r{#{escape_seq}}n.should == /(?-mix:"\x80")/n + end +end diff --git a/spec/ruby/language/regexp/modifiers_spec.rb b/spec/ruby/language/regexp/modifiers_spec.rb new file mode 100644 index 0000000000..2f5522bc8a --- /dev/null +++ b/spec/ruby/language/regexp/modifiers_spec.rb @@ -0,0 +1,115 @@ +require_relative '../../spec_helper' +require_relative '../fixtures/classes' + +describe "Regexps with modifiers" do + it "supports /i (case-insensitive)" do + /foo/i.match("FOO").to_a.should == ["FOO"] + end + + it "supports /m (multiline)" do + /foo.bar/m.match("foo\nbar").to_a.should == ["foo\nbar"] + /foo.bar/.match("foo\nbar").should be_nil + end + + it "supports /x (extended syntax)" do + /\d +/x.match("abc123").to_a.should == ["123"] # Quantifiers can be separated from the expression they apply to + end + + it "supports /o (once)" do + 2.times do |i| + /#{i}/o.should == /0/ + end + end + + it "invokes substitutions for /o only once" do + ScratchPad.record [] + o = Object.new + def o.to_s + ScratchPad << :to_s + "class_with_to_s" + end + eval "2.times { /#{o}/o }" + ScratchPad.recorded.should == [:to_s] + end + + it "supports modifier combinations" do + /foo/imox.match("foo").to_a.should == ["foo"] + /foo/imoximox.match("foo").to_a.should == ["foo"] + + -> { eval('/foo/a') }.should raise_error(SyntaxError) + 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 + /(?i)foo/.match("FOO").to_a.should == ["FOO"] + /foo(?i)/.match("FOO").should be_nil + # Interaction with /i + /(?-i)foo/i.match("FOO").should be_nil + /foo(?-i)/i.match("FOO").to_a.should == ["FOO"] + # Multiple uses + /foo (?i)bar (?-i)baz/.match("foo BAR baz").to_a.should == ["foo BAR baz"] + /foo (?i)bar (?-i)baz/.match("foo BAR BAZ").should be_nil + + /(?m)./.match("\n").to_a.should == ["\n"] + /.(?m)/.match("\n").should be_nil + # Interaction with /m + /(?-m)./m.match("\n").should be_nil + /.(?-m)/m.match("\n").to_a.should == ["\n"] + # Multiple uses + /. (?m). (?-m)./.match(". \n .").to_a.should == [". \n ."] + /. (?m). (?-m)./.match(". \n \n").should be_nil + + /(?x) foo /.match("foo").to_a.should == ["foo"] + / foo (?x)/.match("foo").should be_nil + # Interaction with /x + /(?-x) foo /x.match("foo").should be_nil + / foo (?-x)/x.match("foo").to_a.should == ["foo"] + # Multiple uses + /( foo )(?x)( bar )(?-x)( baz )/.match(" foo bar baz ").to_a.should == [" foo bar baz ", " foo ", "bar", " baz "] + /( foo )(?x)( bar )(?-x)( baz )/.match(" foo barbaz").should be_nil + + # Parsing + /(?i-i)foo/.match("FOO").should be_nil + /(?ii)foo/.match("FOO").to_a.should == ["FOO"] + /(?-)foo/.match("foo").to_a.should == ["foo"] + -> { eval('/(?o)/') }.should raise_error(SyntaxError) + end + + it "supports (?imx-imx:expr) (scoped inline modifiers)" do + /foo (?i:bar) baz/.match("foo BAR baz").to_a.should == ["foo BAR baz"] + /foo (?i:bar) baz/.match("foo BAR BAZ").should be_nil + /foo (?-i:bar) baz/i.match("foo BAR BAZ").should be_nil + + /. (?m:.) ./.match(". \n .").to_a.should == [". \n ."] + /. (?m:.) ./.match(". \n \n").should be_nil + /. (?-m:.) ./m.match("\n \n \n").should be_nil + + /( foo )(?x: bar )( baz )/.match(" foo bar baz ").to_a.should == [" foo bar baz ", " foo ", " baz "] + /( foo )(?x: bar )( baz )/.match(" foo barbaz").should be_nil + /( foo )(?-x: bar )( baz )/x.match("foo bar baz").to_a.should == ["foo bar baz", "foo", "baz"] + + # Parsing + /(?i-i:foo)/.match("FOO").should be_nil + /(?ii:foo)/.match("FOO").to_a.should == ["FOO"] + /(?-:)foo/.match("foo").to_a.should == ["foo"] + -> { eval('/(?o:)/') }.should raise_error(SyntaxError) + end + + it "supports . with /m" do + # Basic matching + /./m.match("\n").to_a.should == ["\n"] + end + + 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"] + eval('/(?a)\w+/').match("a\u3042").to_a.should == ["a"] + eval('/(?d)\w+/').match("a\u3042").to_a.should == ["a"] + eval('/(?u)\w+/').match("a\u3042").to_a.should == ["a\u3042"] + end +end diff --git a/spec/ruby/language/regexp/repetition_spec.rb b/spec/ruby/language/regexp/repetition_spec.rb new file mode 100644 index 0000000000..d76619688f --- /dev/null +++ b/spec/ruby/language/regexp/repetition_spec.rb @@ -0,0 +1,138 @@ +require_relative '../../spec_helper' +require_relative '../fixtures/classes' + +describe "Regexps with repetition" do + it "supports * (0 or more of previous subexpression)" do + /a*/.match("aaa").to_a.should == ["aaa"] + /a*/.match("bbb").to_a.should == [""] + /<.*>/.match("<a>foo</a>").to_a.should == ["<a>foo</a>"] # it is greedy + end + + it "supports *? (0 or more of previous subexpression - lazy)" do + /a*?/.match("aaa").to_a.should == [""] + /<.*?>/.match("<a>foo</a>").to_a.should == ["<a>"] + end + + it "supports + (1 or more of previous subexpression)" do + /a+/.match("aaa").to_a.should == ["aaa"] + /a+/.match("bbb").should be_nil + /<.+>/.match("<a>foo</a>").to_a.should == ["<a>foo</a>"] # it is greedy + end + + it "supports +? (0 or more of previous subexpression - lazy)" do + /a+?/.match("aaa").to_a.should == ["a"] + /<.+?>/.match("<a>foo</a>").to_a.should == ["<a>"] + end + + it "supports {m,n} (m to n of previous subexpression)" do + /a{2,4}/.match("aaaaaa").to_a.should == ["aaaa"] + /<.{1,}>/.match("<a>foo</a>").to_a.should == ["<a>foo</a>"] # it is greedy + end + + it "supports {m,n}? (m to n of previous subexpression) - lazy)" do + /<.{1,}?>/.match("<a>foo</a>").to_a.should == ["<a>"] + /.([0-9]){3,5}?foo/.match("9876543210foo").to_a.should == ["543210foo", "0"] + end + + it "does not treat {m,n}+ as possessive" do + -> { + @regexp = eval "/foo(A{0,1}+)Abar/" + }.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 new file mode 100644 index 0000000000..ce344b5b05 --- /dev/null +++ b/spec/ruby/language/regexp_spec.rb @@ -0,0 +1,167 @@ +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 + -> { + eval <<-EOR + $_ = nil + (true if /foo/).should_not == true + + $_ = "foo" + (true if /foo/).should == true + EOR + }.should complain(/regex literal in condition/) + end + + it "yields a Regexp" 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| + rs << /foo/ + end + rs[0].should equal(rs[1]) + end + + it "throws SyntaxError for malformed literals" do + -> { eval('/(/') }.should raise_error(SyntaxError) + end + + ############################################################################# + # %r + ############################################################################# + + it "supports paired delimiters with %r" do + LanguageSpecs.paired_delimiters.each do |p0, p1| + eval("%r#{p0} foo #{p1}").should == / foo / + end + end + + it "supports grouping constructs that are also paired delimiters" do + LanguageSpecs.paired_delimiters.each do |p0, p1| + eval("%r#{p0} () [c]{1} #{p1}").should == / () [c]{1} / + end + end + + it "allows second part of paired delimiters to be used as non-paired delimiters" do + LanguageSpecs.paired_delimiters.each do |p0, p1| + eval("%r#{p1} foo #{p1}").should == / foo / + end + end + + it "disallows first part of paired delimiters to be used as non-paired delimiters" do + LanguageSpecs.paired_delimiters.each do |p0, p1| + -> { eval("%r#{p0} foo #{p0}") }.should raise_error(SyntaxError) + end + end + + it "supports non-paired delimiters with %r" do + LanguageSpecs.non_paired_delimiters.each do |c| + eval("%r#{c} foo #{c}").should == / foo / + end + end + + it "disallows alphabets as non-paired delimiter with %r" do + -> { eval('%ra foo a') }.should raise_error(SyntaxError) + end + + it "disallows spaces after %r and delimiter" do + -> { eval('%r !foo!') }.should raise_error(SyntaxError) + end + + it "allows unescaped / to be used with %r" do + %r[/].to_s.should == /\//.to_s + end + + + ############################################################################# + # Specs for the matching semantics + ############################################################################# + + it "supports . (any character except line terminator)" do + # Basic matching + /./.match("foo").to_a.should == ["f"] + # Basic non-matching + /./.match("").should be_nil + /./.match("\n").should be_nil + /./.match("\0").to_a.should == ["\0"] + end + + it "supports | (alternations)" do + /a|b/.match("a").to_a.should == ["a"] + end + + it "supports (?> ) (embedded subexpression)" do + /(?>foo)(?>bar)/.match("foobar").to_a.should == ["foobar"] + /(?>foo*)obar/.match("foooooooobar").should be_nil # it is possessive + end + + it "supports (?# )" do + /foo(?#comment)bar/.match("foobar").to_a.should == ["foobar"] + /foo(?#)bar/.match("foobar").to_a.should == ["foobar"] + end + + it "supports (?<= ) (positive lookbehind)" 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 + + it "supports \\g (named backreference)" do + /(?<foo>foo.)bar\g<foo>/.match("foo1barfoo2").to_a.should == ["foo1barfoo2", "foo2"] + end + + it "supports character class composition" do + /[a-z&&[^a-c]]+/.match("abcdef").to_a.should == ["def"] + /[a-z&&[^d-i&&[^d-f]]]+/.match("abcdefghi").to_a.should == ["abcdef"] + end + + it "supports possessive quantifiers" do + /fooA++bar/.match("fooAAAbar").to_a.should == ["fooAAAbar"] + + /fooA++Abar/.match("fooAAAbar").should be_nil + /fooA?+Abar/.match("fooAAAbar").should be_nil + /fooA*+Abar/.match("fooAAAbar").should be_nil + end + + it "supports conditional regular expressions with positional capture groups" do + pattern = /\A(foo)?(?(1)(T)|(F))\z/ + + pattern.should =~ 'fooT' + pattern.should =~ 'F' + pattern.should_not =~ 'fooF' + pattern.should_not =~ 'T' + end + + it "supports conditional regular expressions with named capture groups" do + pattern = /\A(?<word>foo)?(?(<word>)(T)|(F))\z/ + + pattern.should =~ 'fooT' + pattern.should =~ 'F' + 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 new file mode 100644 index 0000000000..6be3bfd023 --- /dev/null +++ b/spec/ruby/language/rescue_spec.rb @@ -0,0 +1,616 @@ +require_relative '../spec_helper' +require_relative 'fixtures/rescue' + +class SpecificExampleException < StandardError +end +class OtherCustomException < StandardError +end +class ArbitraryException < StandardError +end + +exception_list = [SpecificExampleException, ArbitraryException] + +describe "The rescue keyword" do + before :each do + ScratchPad.record [] + end + + it "can be used to handle a specific exception" do + begin + raise SpecificExampleException, "Raising this to be handled below" + rescue SpecificExampleException + :caught + end.should == :caught + end + + 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 + + it "returns value from `rescue` if an exception was raised" do + begin + raise + rescue + :caught + end.should == :caught + end + + it "returns value from `else` section if no exceptions were raised" do + result = begin + :begin + rescue + :rescue + else + :else + ensure + :ensure + end + + result.should == :else + end + + it "can rescue multiple raised exceptions with a single rescue block" do + [->{raise ArbitraryException}, ->{raise SpecificExampleException}].map do |block| + begin + block.call + rescue SpecificExampleException, ArbitraryException + :caught + end + end.should == [:caught, :caught] + end + + it "can rescue a splatted list of exceptions" do + caught_it = false + begin + raise SpecificExampleException, "not important" + rescue *exception_list + caught_it = true + end + caught_it.should be_true + caught = [] + [->{raise ArbitraryException}, ->{raise SpecificExampleException}].each do |block| + begin + block.call + rescue *exception_list + caught << $! + end + end + caught.size.should == 2 + exception_list.each do |exception_class| + caught.map{|e| e.class}.should include(exception_class) + end + end + + it "converts the splatted list of exceptions using #to_a" do + exceptions = mock("to_a") + exceptions.should_receive(:to_a).and_return(exception_list) + caught_it = false + begin + raise SpecificExampleException, "not important" + rescue *exceptions + caught_it = true + end + caught_it.should be_true + end + + it "can combine a splatted list of exceptions with a literal list of exceptions" do + caught_it = false + begin + raise SpecificExampleException, "not important" + rescue ArbitraryException, *exception_list + caught_it = true + end + caught_it.should be_true + caught = [] + [->{raise ArbitraryException}, ->{raise SpecificExampleException}].each do |block| + begin + block.call + rescue ArbitraryException, *exception_list + caught << $! + end + end + caught.size.should == 2 + exception_list.each do |exception_class| + caught.map{|e| e.class}.should include(exception_class) + end + end + + it "will only rescue the specified exceptions when doing a splat rescue" do + -> do + begin + raise OtherCustomException, "not rescued!" + rescue *exception_list + end + end.should raise_error(OtherCustomException) + end + + it "can rescue different types of exceptions in different ways" do + begin + raise Exception + rescue RuntimeError + rescue StandardError + rescue Exception + ScratchPad << :exception + end + + ScratchPad.recorded.should == [:exception] + end + + it "rescues exception within the first suitable section in order of declaration" do + begin + raise StandardError + rescue RuntimeError + ScratchPad << :runtime_error + rescue StandardError + ScratchPad << :standard_error + rescue Exception + ScratchPad << :exception + end + + 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 + rescue + ScratchPad << :does_not_run + else + ScratchPad << :two + :val + end + result.should == :val + ScratchPad.recorded.should == [:one, :two] + end + + it "will execute an else block with ensure only if no exceptions were raised" do + result = begin + ScratchPad << :one + rescue + ScratchPad << :does_not_run + else + ScratchPad << :two + :val + ensure + ScratchPad << :ensure + :ensure_val + end + result.should == :val + ScratchPad.recorded.should == [:one, :two, :ensure] + end + + it "will execute an else block only if no exceptions were raised in a method" do + result = RescueSpecs.begin_else(false) + result.should == :val + ScratchPad.recorded.should == [:one, :else_ran] + end + + it "will execute an else block with ensure only if no exceptions were raised in a method" do + result = RescueSpecs.begin_else_ensure(false) + result.should == :val + ScratchPad.recorded.should == [:one, :else_ran, :ensure_ran] + end + + it "will execute an else block but use the outer scope return value in a method" do + result = RescueSpecs.begin_else_return(false) + result.should == :return_val + ScratchPad.recorded.should == [:one, :else_ran, :outside_begin] + end + + it "will execute an else block with ensure but use the outer scope return value in a method" do + result = RescueSpecs.begin_else_return_ensure(false) + result.should == :return_val + ScratchPad.recorded.should == [:one, :else_ran, :ensure_ran, :outside_begin] + end + + it "raises SyntaxError when else is used without rescue and ensure" do + -> { + eval <<-ruby + begin + ScratchPad << :begin + else + ScratchPad << :else + end + ruby + }.should raise_error(SyntaxError, /else without rescue is useless/) + end + + it "will not execute an else block if an exception was raised" do + result = begin + ScratchPad << :one + raise "an error occurred" + rescue + ScratchPad << :two + :val + else + ScratchPad << :does_not_run + end + result.should == :val + ScratchPad.recorded.should == [:one, :two] + end + + it "will not execute an else block with ensure if an exception was raised" do + result = begin + ScratchPad << :one + raise "an error occurred" + rescue + ScratchPad << :two + :val + else + ScratchPad << :does_not_run + ensure + ScratchPad << :ensure + :ensure_val + end + result.should == :val + ScratchPad.recorded.should == [:one, :two, :ensure] + end + + it "will not execute an else block if an exception was raised in a method" do + result = RescueSpecs.begin_else(true) + result.should == :rescue_val + ScratchPad.recorded.should == [:one, :rescue_ran] + end + + it "will not execute an else block with ensure if an exception was raised in a method" do + result = RescueSpecs.begin_else_ensure(true) + result.should == :rescue_val + ScratchPad.recorded.should == [:one, :rescue_ran, :ensure_ran] + end + + it "will not execute an else block but use the outer scope return value in a method" do + result = RescueSpecs.begin_else_return(true) + result.should == :return_val + ScratchPad.recorded.should == [:one, :rescue_ran, :outside_begin] + end + + it "will not execute an else block with ensure but use the outer scope return value in a method" do + result = RescueSpecs.begin_else_return_ensure(true) + result.should == :return_val + ScratchPad.recorded.should == [:one, :rescue_ran, :ensure_ran, :outside_begin] + end + + it "will not rescue errors raised in an else block in the rescue block above it" do + -> do + begin + ScratchPad << :one + rescue Exception + ScratchPad << :does_not_run + else + ScratchPad << :two + raise SpecificExampleException, "an error from else" + end + end.should raise_error(SpecificExampleException) + ScratchPad.recorded.should == [:one, :two] + end + + it "parses 'a += b rescue c' as 'a += (b rescue c)'" do + a = 'a' + c = 'c' + a += b rescue c + a.should == 'ac' + end + + context "without rescue expression" do + it "will rescue only StandardError and its subclasses" do + begin + raise StandardError + rescue + ScratchPad << :caught + end + + ScratchPad.recorded.should == [:caught] + end + + it "will not rescue exceptions except StandardError" do + [ Exception.new, NoMemoryError.new, ScriptError.new, SecurityError.new, + SignalException.new('INT'), SystemExit.new, SystemStackError.new + ].each do |exception| + -> { + begin + raise exception + rescue + ScratchPad << :caught + end + }.should raise_error(exception.class) + end + ScratchPad.recorded.should == [] + end + end + + it "uses === to compare against rescued classes" do + rescuer = Class.new + + def rescuer.===(exception) + true + end + + begin + raise Exception + rescue rescuer + rescued = :success + rescue Exception + rescued = :failure + end + + rescued.should == :success + end + + it "only accepts Module or Class in rescue clauses" do + rescuer = 42 + -> { + begin + raise "error" + rescue rescuer + end + }.should raise_error(TypeError) { |e| + e.message.should =~ /class or module required for rescue clause/ + } + end + + it "only accepts Module or Class in splatted rescue clauses" do + rescuer = [42] + -> { + begin + raise "error" + rescue *rescuer + end + }.should raise_error(TypeError) { |e| + e.message.should =~ /class or module required for rescue clause/ + } + end + + it "evaluates rescue expressions only when needed" do + begin + 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 + begin + raise "raise" + rescue *(RuntimeError) => e + :expected + end.should == :expected + end + + it "allows rescue in class" do + eval <<-ruby + class RescueInClassExample + raise SpecificExampleException + rescue SpecificExampleException + ScratchPad << :caught + end + ruby + + ScratchPad.recorded.should == [:caught] + end + + it "does not allow rescue in {} block" do + -> { + eval <<-ruby + lambda { + raise SpecificExampleException + rescue SpecificExampleException + :caught + } + ruby + }.should raise_error(SyntaxError) + end + + 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 + + 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 + + ruby_version_is "3.4" do + it "does not introduce extra backtrace entries" do + def foo + begin + raise "oops" + rescue + return caller(0, 2) + end + end + line = __LINE__ + foo[0].should =~ /#{__FILE__}:#{line-3}:in 'foo'/ + foo[1].should =~ /#{__FILE__}:#{line+2}:in 'block/ + end + end + + describe "inline form" do + it "can be inlined" do + a = 1/0 rescue 1 + a.should == 1 + end + + it "doesn't except rescue expression" do + -> { + eval <<-ruby + a = 1 rescue RuntimeError 2 + ruby + }.should raise_error(SyntaxError) + end + + it "rescues only StandardError and its subclasses" do + a = raise(StandardError) rescue 1 + a.should == 1 + + -> { + 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 new file mode 100644 index 0000000000..669d5f0ff5 --- /dev/null +++ b/spec/ruby/language/retry_spec.rb @@ -0,0 +1,55 @@ +require_relative '../spec_helper' + +describe "The retry statement" do + it "re-executes the closest block" do + retry_first = true + retry_second = true + results = [] + begin + results << 1 + raise + rescue + results << 2 + if retry_first + results << 3 + retry_first = false + retry + end + begin + results << 4 + raise + rescue + results << 5 + if retry_second + results << 6 + retry_second = false + retry + end + end + end + + results.should == [1, 2, 3, 1, 2, 4, 5, 6, 4, 5] + end + + 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 + +describe "The retry keyword inside a begin block's rescue block" do + it "causes the begin block to be executed again" do + counter = 0 + + begin + counter += 1 + raise "An exception" + rescue + retry unless counter == 7 + end + + counter.should == 7 + end +end diff --git a/spec/ruby/language/return_spec.rb b/spec/ruby/language/return_spec.rb new file mode 100644 index 0000000000..a62ed1242d --- /dev/null +++ b/spec/ruby/language/return_spec.rb @@ -0,0 +1,490 @@ +require_relative '../spec_helper' +require_relative 'fixtures/return' + +describe "The return keyword" do + it "returns any object directly" do + def r; return 1; end + r().should == 1 + end + + it "returns an single element array directly" do + def r; return [1]; end + r().should == [1] + end + + it "returns an multi element array directly" do + def r; return [1,2]; end + r().should == [1,2] + end + + it "returns nil by default" do + def r; return; end + r().should be_nil + end + + describe "in a Thread" do + it "raises a LocalJumpError if used to exit a thread" do + t = Thread.new { + begin + return + rescue LocalJumpError => e + e + end + } + t.value.should be_an_instance_of(LocalJumpError) + end + end + + describe "when passed a splat" do + it "returns [] when the ary is empty" do + def r; ary = []; return *ary; end + r.should == [] + end + + it "returns the array when the array is size of 1" do + def r; ary = [1]; return *ary; end + r.should == [1] + end + + it "returns the whole array when size is greater than 1" do + def r; ary = [1,2]; return *ary; end + r.should == [1,2] + + def r; ary = [1,2,3]; return *ary; end + r.should == [1,2,3] + end + + it "returns an array when used as a splat" do + def r; value = 1; return *value; end + r.should == [1] + end + + it "calls 'to_a' on the splatted value first" do + def r + obj = Object.new + def obj.to_a + [1,2] + end + + return *obj + end + + r().should == [1,2] + end + end + + describe "within a begin" do + before :each do + ScratchPad.record [] + end + + it "executes ensure before returning" do + def f() + begin + ScratchPad << :begin + return :begin + ScratchPad << :after_begin + ensure + ScratchPad << :ensure + end + ScratchPad << :function + end + f().should == :begin + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "returns last value returned in ensure" do + def f() + begin + ScratchPad << :begin + return :begin + ScratchPad << :after_begin + ensure + ScratchPad << :ensure + return :ensure + ScratchPad << :after_ensure + end + ScratchPad << :function + end + f().should == :ensure + ScratchPad.recorded.should == [:begin, :ensure] + end + + it "executes nested ensures before returning" do + def f() + begin + begin + ScratchPad << :inner_begin + return :inner_begin + ScratchPad << :after_inner_begin + ensure + ScratchPad << :inner_ensure + end + ScratchPad << :outer_begin + return :outer_begin + ScratchPad << :after_outer_begin + ensure + ScratchPad << :outer_ensure + end + ScratchPad << :function + end + f().should == :inner_begin + ScratchPad.recorded.should == [:inner_begin, :inner_ensure, :outer_ensure] + end + + it "returns last value returned in nested ensures" do + def f() + begin + begin + ScratchPad << :inner_begin + return :inner_begin + ScratchPad << :after_inner_begin + ensure + ScratchPad << :inner_ensure + return :inner_ensure + ScratchPad << :after_inner_ensure + end + ScratchPad << :outer_begin + return :outer_begin + ScratchPad << :after_outer_begin + ensure + ScratchPad << :outer_ensure + return :outer_ensure + ScratchPad << :after_outer_ensure + end + ScratchPad << :function + end + f().should == :outer_ensure + ScratchPad.recorded.should == [:inner_begin, :inner_ensure, :outer_ensure] + end + + it "executes the ensure clause when begin/ensure are inside a lambda" do + -> do + begin + return + ensure + ScratchPad.recorded << :ensure + end + end.call + ScratchPad.recorded.should == [:ensure] + end + end + + describe "within a block" do + before :each do + ScratchPad.clear + end + + it "causes lambda to return nil if invoked without any arguments" do + -> { return; 456 }.call.should be_nil + end + + it "causes lambda to return nil if invoked with an empty expression" do + -> { return (); 456 }.call.should be_nil + end + + it "causes lambda to return the value passed to return" do + -> { return 123; 456 }.call.should == 123 + end + + it "causes the method that lexically encloses the block to return" do + ReturnSpecs::Blocks.new.enclosing_method.should == :return_value + ScratchPad.recorded.should == :before_return + end + + it "returns from the lexically enclosing method even in case of chained calls" do + ReturnSpecs::NestedCalls.new.enclosing_method.should == :return_value + ScratchPad.recorded.should == :before_return + end + + it "returns from the lexically enclosing method even in case of chained calls(in yield)" do + ReturnSpecs::NestedBlocks.new.enclosing_method.should == :return_value + ScratchPad.recorded.should == :before_return + end + + it "causes the method to return even when the immediate parent has already returned" do + ReturnSpecs::SavedInnerBlock.new.start.should == :return_value + ScratchPad.recorded.should == :before_return + end + + # jruby/jruby#3143 + describe "downstream from a lambda" do + it "returns to its own return-capturing lexical enclosure" do + def a + ->{ yield }.call + return 2 + end + def b + a { return 1 } + end + + b.should == 1 + end + end + + end + + describe "within two blocks" do + it "causes the method that lexically encloses the block to return" do + def f + 1.times { 1.times {return true}; false}; false + end + f.should be_true + end + end + + describe "within define_method" do + it "goes through the method via a closure" do + ReturnSpecs::ThroughDefineMethod.new.outer.should == :good + end + + it "stops at the method when the return is used directly" do + ReturnSpecs::DefineMethod.new.outer.should == :good + end + end + + describe "invoked with a method call without parentheses with a block" do + it "returns the value returned from the method call" do + ReturnSpecs::MethodWithBlock.new.method1.should == 5 + ReturnSpecs::MethodWithBlock.new.method2.should == [0, 1, 2] + end + end + + describe "at top level" do + before :each do + @filename = tmp("top_return.rb") + ScratchPad.record [] + 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 + + 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 + + ScratchPad << "after if" + END_OF_CODE + + load @filename + ScratchPad.recorded.should == ["before if"] + end + 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 while" + END_OF_CODE + + load @filename + ScratchPad.recorded.should == ["before while"] + end + 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 begin" + END_OF_CODE + + 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 + + ScratchPad << "after begin" + END_OF_CODE + + 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 + + ScratchPad << "after begin" + END_OF_CODE + + load @filename + ScratchPad.recorded.should == ["before begin"] + end + + it "fires ensure block before returning" do + ruby_exe(<<-END_OF_CODE).should == "within ensure\n" + begin + return + ensure + puts "within ensure" + end + + puts "after begin" + END_OF_CODE + 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 + + ScratchPad << "after begin" + END_OF_CODE + + load @filename + ScratchPad.recorded.should == ["before begin", "within ensure"] + end + + 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 + end + + 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 + + load @filename + ScratchPad.recorded.should == ["before call"] + end + end + + describe "within a class" do + it "raises a SyntaxError" do + File.write(@filename, <<-END_OF_CODE) + class ReturnSpecs::A + ScratchPad << "before return" + return + + ScratchPad << "after return" + end + END_OF_CODE + + -> { load @filename }.should raise_error(SyntaxError) + end + end + + 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" + 1.times { return } + ScratchPad << "after return" + end + END_OF_CODE + + -> { load @filename }.should raise_error(LocalJumpError) + end + end + + describe "within BEGIN" do + it "is allowed" do + File.write(@filename, <<-END_OF_CODE) + BEGIN { + ScratchPad << "before call" + return + ScratchPad << "after call" + } + END_OF_CODE + + load @filename + ScratchPad.recorded.should == ["before call"] + end + end + + 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 + + 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 +end diff --git a/spec/ruby/language/safe_navigator_spec.rb b/spec/ruby/language/safe_navigator_spec.rb new file mode 100644 index 0000000000..b1e28c3963 --- /dev/null +++ b/spec/ruby/language/safe_navigator_spec.rb @@ -0,0 +1,147 @@ +require_relative '../spec_helper' + +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 + + it "can be chained" do + 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 + + 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 truthy" do + it "calls the method" do + 1&.to_s.should == "1" + + -> { 1&.unknown }.should raise_error(NoMethodError) + end + end + + 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 + + it "allows assignment methods" do + klass = Class.new do + attr_reader :foo + def foo=(val) + @foo = val + 42 + end + end + obj = klass.new + + (obj&.foo = 3).should == 3 + obj.foo.should == 3 + + obj = nil + (obj&.foo = 3).should == nil + end + + it "allows assignment operators" do + klass = Class.new do + attr_reader :m + + def initialize + @m = 0 + end + + def m=(v) + @m = v + 42 + end + end + + obj = klass.new + + obj&.m += 3 + obj.m.should == 3 + + obj = nil + (obj&.m += 3).should == nil + end + + it "allows ||= operator" do + klass = Class.new do + attr_reader :m + + def initialize + @m = false + end + + def m=(v) + @m = v + 42 + end + end + + obj = klass.new + + (obj&.m ||= true).should == true + obj.m.should == true + + obj = nil + (obj&.m ||= true).should == nil + obj.should == nil + end + + it "allows &&= operator" do + klass = Class.new do + attr_accessor :m + + def initialize + @m = true + end + end + + obj = klass.new + + (obj&.m &&= false).should == false + obj.m.should == false + + obj = nil + (obj&.m &&= false).should == nil + obj.should == nil + end + + it "does not call the operator method lazily with an assignment operator" do + 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 new file mode 100644 index 0000000000..5d6340ffc5 --- /dev/null +++ b/spec/ruby/language/send_spec.rb @@ -0,0 +1,570 @@ +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 - Mandatory Args +# O - Optional Arg +# R - Rest Arg +# Q - Post Mandatory Args + +specs = LangSendSpecs + +describe "Invoking a method" do + describe "with zero arguments" do + it "requires no arguments passed" do + specs.fooM0.should == 100 + end + + it "raises ArgumentError if the method has a positive arity" do + -> { + specs.fooM1 + }.should raise_error(ArgumentError) + end + end + + describe "with only mandatory arguments" do + it "requires exactly the same number of passed values" do + specs.fooM1(1).should == [1] + specs.fooM2(1,2).should == [1,2] + specs.fooM3(1,2,3).should == [1,2,3] + specs.fooM4(1,2,3,4).should == [1,2,3,4] + specs.fooM5(1,2,3,4,5).should == [1,2,3,4,5] + end + + it "raises ArgumentError if the methods arity doesn't match" do + -> { + specs.fooM1(1,2) + }.should raise_error(ArgumentError) + end + end + + describe "with optional arguments" do + it "uses the optional argument if none is passed" do + specs.fooM0O1.should == [1] + end + + it "uses the passed argument if available" do + specs.fooM0O1(2).should == [2] + end + + it "raises ArgumentError if extra arguments are passed" do + -> { + specs.fooM0O1(2,3) + }.should raise_error(ArgumentError) + end + end + + describe "with mandatory and optional arguments" do + it "uses the passed values in left to right order" do + specs.fooM1O1(2).should == [2,1] + end + + it "raises an ArgumentError if there are no values for the mandatory args" do + -> { + specs.fooM1O1 + }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if too many values are passed" do + -> { + specs.fooM1O1(1,2,3) + }.should raise_error(ArgumentError) + end + end + + describe "with a rest argument" do + it "is an empty array if there are no additional arguments" do + specs.fooM0R().should == [] + specs.fooM1R(1).should == [1, []] + end + + it "gathers unused arguments" do + specs.fooM0R(1).should == [1] + specs.fooM1R(1,2).should == [1, [2]] + end + end + + it "with a block makes it available to yield" do + specs.oneb(10) { 200 }.should == [10,200] + end + + it "with a block converts the block to a Proc" do + prc = specs.makeproc { "hello" } + prc.should be_kind_of(Proc) + prc.call.should == "hello" + end + + it "with an object as a block uses 'to_proc' for coercion" do + o = LangSendSpecs::ToProc.new(:from_to_proc) + + specs.makeproc(&o).call.should == :from_to_proc + + 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 + -> { + eval "specs.oneb(10, &l){ 42 }" + }.should raise_error(SyntaxError) + end + + it "with same names as existing variables is ok" do + foobar = 100 + + def foobar; 200; end + + foobar.should == 100 + foobar().should == 200 + end + + it "with splat operator makes the object the direct arguments" do + a = [1,2,3] + specs.fooM3(*a).should == [1,2,3] + end + + it "without parentheses works" do + (specs.fooM3 1,2,3).should == [1,2,3] + end + + it "with a space separating method name and parenthesis treats expression in parenthesis as first argument" do + specs.weird_parens().should == "55" + end + + describe "allows []=" do + before :each do + @obj = LangSendSpecs::AttrSet.new + end + + it "with *args in the [] expanded to individual arguments" do + ary = [2,3] + (@obj[1, *ary] = 4).should == 4 + @obj.result.should == [1,2,3,4] + end + + it "with multiple *args" do + ary = [2,3] + post = [4,5] + (@obj[1, *ary] = *post).should == [4,5] + @obj.result.should == [1,2,3,[4,5]] + end + + it "with multiple *args and does not unwrap the last splat" do + ary = [2,3] + post = [4] + (@obj[1, *ary] = *post).should == [4] + @obj.result.should == [1,2,3,[4]] + end + + it "with a *args and multiple rhs args" do + ary = [2,3] + (@obj[1, *ary] = 4, 5).should == [4,5] + @obj.result.should == [1,2,3,[4,5]] + end + end + + it "passes literal hashes without curly braces as the last parameter" do + specs.fooM3('abc', 456, 'rbx' => 'cool', + 'specs' => 'fail sometimes', 'oh' => 'weh').should == \ + ['abc', 456, {'rbx' => 'cool', 'specs' => 'fail sometimes', 'oh' => 'weh'}] + end + + it "passes a literal hash without curly braces or parens" do + (specs.fooM3 'abc', 456, 'rbx' => 'cool', + 'specs' => 'fail sometimes', 'oh' => 'weh').should == \ + ['abc', 456, { 'rbx' => 'cool', 'specs' => 'fail sometimes', 'oh' => 'weh'}] + end + + it "allows to literal hashes without curly braces as the only parameter" do + specs.fooM1(rbx: :cool, specs: :fail_sometimes).should == + [{ rbx: :cool, specs: :fail_sometimes }] + + (specs.fooM1 rbx: :cool, specs: :fail_sometimes).should == + [{ rbx: :cool, specs: :fail_sometimes }] + end + + describe "when the method is not available" do + it "invokes method_missing if it is defined" do + o = LangSendSpecs::MethodMissing.new + o.not_there(1,2) + o.message.should == :not_there + o.args.should == [1,2] + end + + it "raises NameError if invoked as a vcall" do + -> { 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 + -> { 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 + +end + +describe "Invoking a public setter method" do + it 'returns the set value' do + klass = Class.new do + def foobar=(*) + 1 + end + end + + (klass.new.foobar = 'bar').should == 'bar' + (klass.new.foobar = 'bar', 'baz').should == ["bar", "baz"] + end +end + +describe "Invoking []= methods" do + it 'returns the set value' do + klass = Class.new do + def []=(*) + 1 + end + end + + (klass.new[33] = 'bar').should == 'bar' + (klass.new[33] = 'bar', 'baz').should == ['bar', 'baz'] + (klass.new[33, 34] = 'bar', 'baz').should == ['bar', 'baz'] + end +end + +describe "Invoking a private setter method" do + describe "permits self as a receiver" do + it "for normal assignment" do + receiver = LangSendSpecs::PrivateSetter.new + receiver.call_self_foo_equals(42) + receiver.foo.should == 42 + end + + it "for multiple assignment" do + receiver = LangSendSpecs::PrivateSetter.new + receiver.call_self_foo_equals_masgn(42) + receiver.foo.should == 42 + end + end +end + +describe "Invoking a private getter method" do + it "permits self as a receiver" do + receiver = LangSendSpecs::PrivateGetter.new + receiver.call_self_foo_or_equals(6) + receiver.call_self_foo.should == 6 + end +end + +describe "Invoking a method" do + describe "with required args after the rest arguments" do + it "binds the required arguments first" do + specs.fooM0RQ1(1).should == [[], 1] + specs.fooM0RQ1(1,2).should == [[1], 2] + specs.fooM0RQ1(1,2,3).should == [[1,2], 3] + + specs.fooM1RQ1(1,2).should == [1, [], 2] + specs.fooM1RQ1(1,2,3).should == [1, [2], 3] + specs.fooM1RQ1(1,2,3,4).should == [1, [2, 3], 4] + + specs.fooM1O1RQ1(1,2).should == [1, 9, [], 2] + specs.fooM1O1RQ1(1,2,3).should == [1, 2, [], 3] + specs.fooM1O1RQ1(1,2,3,4).should == [1, 2, [3], 4] + + specs.fooM1O1RQ2(1,2,3).should == [1, 9, [], 2, 3] + specs.fooM1O1RQ2(1,2,3,4).should == [1, 2, [], 3, 4] + specs.fooM1O1RQ2(1,2,3,4,5).should == [1, 2, [3], 4, 5] + end + end + + describe "with mandatory arguments after optional arguments" do + it "binds the required arguments first" do + specs.fooO1Q1(0,1).should == [0,1] + specs.fooO1Q1(2).should == [1,2] + + specs.fooM1O1Q1(2,3,4).should == [2,3,4] + specs.fooM1O1Q1(1,3).should == [1,2,3] + + specs.fooM2O1Q1(1,2,4).should == [1,2,3,4] + + specs.fooM2O2Q1(1,2,3,4,5).should == [1,2,3,4,5] + specs.fooM2O2Q1(1,2,3,5).should == [1,2,3,4,5] + specs.fooM2O2Q1(1,2,5).should == [1,2,3,4,5] + + specs.fooO4Q1(1,2,3,4,5).should == [1,2,3,4,5] + specs.fooO4Q1(1,2,3,5).should == [1,2,3,4,5] + specs.fooO4Q1(1,2,5).should == [1,2,3,4,5] + specs.fooO4Q1(1,5).should == [1,2,3,4,5] + specs.fooO4Q1(5).should == [1,2,3,4,5] + + specs.fooO4Q2(1,2,3,4,5,6).should == [1,2,3,4,5,6] + specs.fooO4Q2(1,2,3,5,6).should == [1,2,3,4,5,6] + specs.fooO4Q2(1,2,5,6).should == [1,2,3,4,5,6] + specs.fooO4Q2(1,5,6).should == [1,2,3,4,5,6] + specs.fooO4Q2(5,6).should == [1,2,3,4,5,6] + end + end + + it "with .() invokes #call" do + q = proc { |z| z } + q.(1).should == 1 + + obj = mock("paren call") + obj.should_receive(:call).and_return(:called) + obj.().should == :called + end + + it "allows a vestigial trailing ',' in the arguments" do + specs.fooM1(1,).should == [1] + end + + it "with splat operator attempts to coerce it to an Array if the object respond_to?(:to_a)" do + ary = [2,3,4] + obj = mock("to_a") + obj.should_receive(:to_a).and_return(ary).twice + specs.fooM0R(*obj).should == ary + specs.fooM1R(1,*obj).should == [1, ary] + end + + it "with splat operator * and non-Array value uses value unchanged if it does not respond_to?(:to_ary)" do + obj = Object.new + obj.should_not respond_to(:to_a) + + specs.fooM0R(*obj).should == [obj] + specs.fooM1R(1,*obj).should == [1, [obj]] + end + + it "accepts additional arguments after splat expansion" do + a = [1,2] + specs.fooM4(*a,3,4).should == [1,2,3,4] + specs.fooM4(0,*a,3).should == [0,1,2,3] + end + + it "does not expand final array arguments after a splat expansion" do + a = [1, 2] + specs.fooM3(*a, [3, 4]).should == [1, 2, [3, 4]] + end + + it "accepts final explicit literal Hash arguments after the splat" do + a = [1, 2] + specs.fooM0RQ1(*a, { a: 1 }).should == [[1, 2], { a: 1 }] + end + + it "accepts final implicit literal Hash arguments after the splat" do + a = [1, 2] + specs.fooM0RQ1(*a, a: 1).should == [[1, 2], { a: 1 }] + end + + it "accepts final Hash arguments after the splat" do + a = [1, 2] + b = { a: 1 } + specs.fooM0RQ1(*a, b).should == [[1, 2], { a: 1 }] + end + + it "accepts mandatory and explicit literal Hash arguments after the splat" do + a = [1, 2] + specs.fooM0RQ2(*a, 3, { a: 1 }).should == [[1, 2], 3, { a: 1 }] + end + + it "accepts mandatory and implicit literal Hash arguments after the splat" do + a = [1, 2] + specs.fooM0RQ2(*a, 3, a: 1).should == [[1, 2], 3, { a: 1 }] + end + + it "accepts mandatory and Hash arguments after the splat" do + a = [1, 2] + b = { a: 1 } + specs.fooM0RQ2(*a, 3, b).should == [[1, 2], 3, { a: 1 }] + end + + it "converts a final splatted explicit Hash to an Array" do + a = [1, 2] + specs.fooR(*a, 3, *{ a: 1 }).should == [1, 2, 3, [:a, 1]] + end + + it "calls #to_a to convert a final splatted Hash object to an Array" do + a = [1, 2] + b = { a: 1 } + b.should_receive(:to_a).and_return([:a, 1]) + + specs.fooR(*a, 3, *b).should == [1, 2, 3, :a, 1] + end + + it "accepts multiple splat expansions in the same argument list" do + a = [1,2,3] + b = 7 + c = mock("pseudo-array") + c.should_receive(:to_a).and_return([0,0]) + + d = [4,5] + specs.rest_len(*a,*d,6,*b).should == 7 + specs.rest_len(*a,*a,*a).should == 9 + 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 + + it "expands an array to arguments grouped in parentheses and ignores any rest arguments in the array" do + specs.destructure2([40,2,84]).should == 42 + end + + it "expands an array to arguments grouped in parentheses and sets not specified arguments to nil" do + specs.destructure2b([42]).should == [42, nil] + end + + it "expands an array to arguments grouped in parentheses which in turn takes rest arguments" do + specs.destructure4r([1, 2, 3]).should == [1, 2, [], 3, nil] + specs.destructure4r([1, 2, 3, 4]).should == [1, 2, [], 3, 4] + specs.destructure4r([1, 2, 3, 4, 5]).should == [1, 2, [3], 4, 5] + end + + it "with optional argument(s), expands an array to arguments grouped in parentheses" do + specs.destructure4o(1, [2, 3]).should == [1, 1, nil, [2, 3]] + specs.destructure4o(1, [], 2).should == [1, nil, nil, 2] + specs.destructure4os(1, [2, 3]).should == [1, 2, [3]] + specs.destructure5o(1, [2, 3]).should == [1, 2, 1, nil, [2, 3]] + specs.destructure7o(1, [2, 3]).should == [1, 2, 1, nil, 2, 3] + specs.destructure7b(1, [2, 3]) do |(a,*b,c)| + [a, c] + end.should == [1, 3] + end + + describe "new-style hash arguments" do + describe "as the only parameter" do + it "passes without curly braces" do + specs.fooM1(rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == + [{ rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] + end + + it "passes without curly braces or parens" do + (specs.fooM1 rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == + [{ rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] + end + + it "handles a hanging comma without curly braces" do + specs.fooM1(abc: 123,).should == [{abc: 123}] + specs.fooM1(rbx: 'cool', specs: :fail_sometimes, non_sym: 1234,).should == + [{ rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] + end + end + + describe "as the last parameter" do + it "passes without curly braces" do + specs.fooM3('abc', 123, rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == + ['abc', 123, { rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] + end + + it "passes without curly braces or parens" do + (specs.fooM3 'abc', 123, rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == + ['abc', 123, { rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] + end + + it "handles a hanging comma without curly braces" do + specs.fooM3('abc', 123, abc: 123,).should == ['abc', 123, {abc: 123}] + specs.fooM3('abc', 123, rbx: 'cool', specs: :fail_sometimes, non_sym: 1234,).should == + ['abc', 123, { rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] + end + end + end + + describe "mixed new- and old-style hash arguments" do + describe "as the only parameter" do + it "passes without curly braces" do + specs.fooM1(rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == + [{ rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] + end + + it "passes without curly braces or parens" do + (specs.fooM1 rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == + [{ rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] + end + + it "handles a hanging comma without curly braces" do + specs.fooM1(rbx: 'cool', specs: :fail_sometimes, non_sym: 1234,).should == + [{ rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] + end + end + + describe "as the last parameter" do + it "passes without curly braces" do + specs.fooM3('abc', 123, rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == + ['abc', 123, { rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] + end + + it "passes without curly braces or parens" do + (specs.fooM3 'abc', 123, rbx: 'cool', specs: :fail_sometimes, non_sym: 1234).should == + ['abc', 123, { rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] + end + + it "handles a hanging comma without curly braces" do + specs.fooM3('abc', 123, rbx: 'cool', specs: :fail_sometimes, non_sym: 1234,).should == + ['abc', 123, { rbx: 'cool', specs: :fail_sometimes, non_sym: 1234 }] + end + end + end + +end + +describe "allows []= with arguments after splat" do + before :each do + @obj = LangSendSpecs::Attr19Set.new + @ary = ["a"] + end + + it "with *args in the [] and post args" do + @obj[1,*@ary,123] = 2 + @obj.result.should == [1, "a", 123, 2] + end +end diff --git a/spec/ruby/language/shared/__FILE__.rb b/spec/ruby/language/shared/__FILE__.rb new file mode 100644 index 0000000000..3e4f5c958d --- /dev/null +++ b/spec/ruby/language/shared/__FILE__.rb @@ -0,0 +1,23 @@ +describe :language___FILE__, shared: true do + before :each do + CodeLoadingSpecs.spec_setup + @path = File.join(CODE_LOADING_DIR, "file_fixture.rb") + end + + after :each do + CodeLoadingSpecs.spec_cleanup + end + + it "equals the absolute path of a file loaded by an absolute path" do + @object.send(@method, @path).should be_true + ScratchPad.recorded.should == [@path] + end + + it "equals the absolute path of a file loaded by a relative path" do + $LOAD_PATH << "." + Dir.chdir CODE_LOADING_DIR do + @object.send(@method, "file_fixture.rb").should be_true + end + ScratchPad.recorded.should == [@path] + end +end diff --git a/spec/ruby/language/shared/__LINE__.rb b/spec/ruby/language/shared/__LINE__.rb new file mode 100644 index 0000000000..076b74b3ba --- /dev/null +++ b/spec/ruby/language/shared/__LINE__.rb @@ -0,0 +1,15 @@ +describe :language___LINE__, shared: true do + before :each do + CodeLoadingSpecs.spec_setup + @path = File.expand_path("line_fixture.rb", CODE_LOADING_DIR) + end + + after :each do + CodeLoadingSpecs.spec_cleanup + end + + it "equals the line number of the text in a loaded file" do + @object.send(@method, @path).should be_true + ScratchPad.recorded.should == [1, 5] + end +end diff --git a/spec/ruby/language/singleton_class_spec.rb b/spec/ruby/language/singleton_class_spec.rb new file mode 100644 index 0000000000..45e1f7f3ad --- /dev/null +++ b/spec/ruby/language/singleton_class_spec.rb @@ -0,0 +1,317 @@ +require_relative '../spec_helper' +require_relative '../fixtures/class' + +describe "A singleton class" do + it "is TrueClass for true" do + true.singleton_class.should == TrueClass + end + + it "is FalseClass for false" do + false.singleton_class.should == FalseClass + end + + it "is NilClass for nil" do + nil.singleton_class.should == NilClass + end + + it "raises a TypeError for Integer's" do + -> { 1.singleton_class }.should raise_error(TypeError) + end + + it "raises a TypeError for symbols" do + -> { :symbol.singleton_class }.should raise_error(TypeError) + end + + it "is a singleton Class instance" do + o = mock('x') + o.singleton_class.should be_kind_of(Class) + o.singleton_class.should_not equal(Object) + o.should be_kind_of(o.singleton_class) + end + + it "is a Class for classes" do + ClassSpecs::A.singleton_class.should be_kind_of(Class) + end + + it "inherits from Class for classes" do + Class.should be_ancestor_of(Object.singleton_class) + end + + it "is a subclass of Class's singleton class" do + ec = ClassSpecs::A.singleton_class + ec.should be_kind_of(Class.singleton_class) + end + + it "is a subclass of the same level of Class's singleton class" do + ecec = ClassSpecs::A.singleton_class.singleton_class + class_ec = Class.singleton_class + + ecec.should be_kind_of(class_ec.singleton_class) + ecec.should be_kind_of(class_ec) + end + + it "is a subclass of a superclass's singleton class" do + ClassSpecs::K.singleton_class.superclass.should == + ClassSpecs::H.singleton_class + end + + it "is a subclass of the same level of superclass's singleton class" do + ClassSpecs::K.singleton_class.singleton_class.superclass.should == + ClassSpecs::H.singleton_class.singleton_class + end + + it "for BasicObject has Class as it's superclass" do + BasicObject.singleton_class.superclass.should == Class + end + + it "for BasicObject has the proper level of superclass for Class" do + BasicObject.singleton_class.singleton_class.superclass.should == + Class.singleton_class + end + + it "has class String as the superclass of a String instance" do + "blah".dup.singleton_class.superclass.should == String + end + + it "doesn't have singleton class" do + -> { bignum_value.singleton_class }.should raise_error(TypeError) + end +end + +describe "A constant on a singleton class" do + before :each do + @object = Object.new + class << @object + CONST = self + end + end + + it "can be accessed after the singleton class body is reopened" do + class << @object + CONST.should == self + end + end + + it "can be accessed via self::CONST" do + class << @object + self::CONST.should == self + end + end + + it "can be accessed via const_get" do + class << @object + const_get(:CONST).should == self + end + end + + it "is not defined on the object's class" do + @object.class.const_defined?(:CONST).should be_false + end + + it "is not defined in the singleton class opener's scope" do + class << @object + CONST + end + -> { CONST }.should raise_error(NameError) + end + + it "cannot be accessed via object::CONST" do + -> do + @object::CONST + end.should raise_error(TypeError) + end + + it "raises a NameError for anonymous_module::CONST" do + @object = Class.new + class << @object + CONST = 100 + end + + -> do + @object::CONST + end.should raise_error(NameError) + end + + it "appears in the singleton class constant list" do + @object.singleton_class.should have_constant(:CONST) + end + + it "does not appear in the object's class constant list" do + @object.class.should_not have_constant(:CONST) + end + + it "is not preserved when the object is duped" do + @object = @object.dup + + -> do + class << @object; CONST; end + end.should raise_error(NameError) + end + + it "is preserved when the object is cloned" do + @object = @object.clone + + class << @object + CONST.should_not be_nil + end + end +end + +describe "Defining instance methods on a singleton class" do + before :each do + @k = ClassSpecs::K.new + class << @k + def singleton_method; 1 end + end + + @k_sc = @k.singleton_class + end + + it "defines public methods" do + @k_sc.should have_public_instance_method(:singleton_method) + end +end + +describe "Instance methods of a singleton class" do + before :each do + k = ClassSpecs::K.new + @k_sc = k.singleton_class + @a_sc = ClassSpecs::A.new.singleton_class + @a_c_sc = ClassSpecs::A.singleton_class + end + + it "include ones of the object's class" do + @k_sc.should have_instance_method(:example_instance_method) + end + + it "does not include class methods of the object's class" do + @k_sc.should_not have_instance_method(:example_class_method) + end + + it "include instance methods of Object" do + @a_sc.should have_instance_method(:example_instance_method_of_object) + end + + it "does not include class methods of Object" do + @a_sc.should_not have_instance_method(:example_class_method_of_object) + end + + describe "for a class" do + it "include instance methods of Class" do + @a_c_sc.should have_instance_method(:example_instance_method_of_class) + end + + it "does not include class methods of Class" do + @a_c_sc.should_not have_instance_method(:example_class_method_of_class) + end + + it "does not include instance methods of the singleton class of Class" do + @a_c_sc.should_not have_instance_method(:example_instance_method_of_singleton_class) + end + + it "does not include class methods of the singleton class of Class" do + @a_c_sc.should_not have_instance_method(:example_class_method_of_singleton_class) + end + end + + describe "for a singleton class" do + it "includes instance methods of the singleton class of Class" do + @a_c_sc.singleton_class.should have_instance_method(:example_instance_method_of_singleton_class) + end + + it "does not include class methods of the singleton class of Class" do + @a_c_sc.singleton_class.should_not have_instance_method(:example_class_method_of_singleton_class) + end + end +end + +describe "Class methods of a singleton class" do + before :each do + k = ClassSpecs::K.new + @k_sc = k.singleton_class + @a_sc = ClassSpecs::A.new.singleton_class + @a_c_sc = ClassSpecs::A.singleton_class + end + + it "include ones of the object's class" do + @k_sc.should have_method(:example_class_method) + end + + it "does not include instance methods of the object's class" do + @k_sc.should_not have_method(:example_instance_method) + end + + it "include instance methods of Class" do + @a_sc.should have_method(:example_instance_method_of_class) + end + + it "does not include class methods of Class" do + @a_sc.should_not have_method(:example_class_method_of_class) + end + + describe "for a class" do + it "include instance methods of Class" do + @a_c_sc.should have_method(:example_instance_method_of_class) + end + + it "include class methods of Class" do + @a_c_sc.should have_method(:example_class_method_of_class) + end + + it "include instance methods of the singleton class of Class" do + @a_c_sc.should have_method(:example_instance_method_of_singleton_class) + end + + it "does not include class methods of the singleton class of Class" do + @a_c_sc.should_not have_method(:example_class_method_of_singleton_class) + end + end + + describe "for a singleton class" do + it "include instance methods of the singleton class of Class" do + @a_c_sc.singleton_class.should have_method(:example_instance_method_of_singleton_class) + end + + it "include class methods of the singleton class of Class" do + @a_c_sc.singleton_class.should have_method(:example_class_method_of_singleton_class) + end + end +end + +describe "Instantiating a singleton class" do + it "raises a TypeError when new is called" do + -> { + Object.new.singleton_class.new + }.should raise_error(TypeError) + end + + it "raises a TypeError when allocate is called" do + -> { + 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 new file mode 100644 index 0000000000..f287731bed --- /dev/null +++ b/spec/ruby/language/string_spec.rb @@ -0,0 +1,301 @@ +# encoding: binary + +require_relative '../spec_helper' +require_relative 'fixtures/class_with_class_variable' + +# TODO: rewrite these horrid specs. it "are..." seriously?! + +describe "Ruby character strings" do + + before :each do + @ip = 'xxx' # used for interpolation + $ip = 'xxx' + end + + it "don't get interpolated when put in single quotes" do + '#{@ip}'.should == '#{@ip}' + end + + it 'get interpolated with #{} when put in double quotes' do + "#{@ip}".should == 'xxx' + end + + it "interpolate instance variables just with the # character" do + "#@ip".should == 'xxx' + end + + it "interpolate global variables just with the # character" 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.' + "#@ip(".should == 'xxx(' + "#@ip=".should == 'xxx=' + "#@ip?".should == 'xxx?' + "#@ip!".should == 'xxx!' + "#@ip#@ip".should == 'xxxxxx' + end + + 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 + %(hey #{@ip}).should == "hey xxx" + %[hey #{@ip}].should == "hey xxx" + %{hey #{@ip}}.should == "hey xxx" + %<hey #{@ip}>.should == "hey xxx" + %!hey #{@ip}!.should == "hey xxx" + %@hey #{@ip}@.should == "hey xxx" + %#hey hey#.should == "hey hey" + %%hey #{@ip}%.should == "hey xxx" + %^hey #{@ip}^.should == "hey xxx" + %&hey #{@ip}&.should == "hey xxx" + %*hey #{@ip}*.should == "hey xxx" + %-hey #{@ip}-.should == "hey xxx" + %_hey #{@ip}_.should == "hey xxx" + %=hey #{@ip}=.should == "hey xxx" + %+hey #{@ip}+.should == "hey xxx" + %~hey #{@ip}~.should == "hey xxx" + %:hey #{@ip}:.should == "hey xxx" + %;hey #{@ip};.should == "hey xxx" + %"hey #{@ip}".should == "hey xxx" + %|hey #{@ip}|.should == "hey xxx" + %?hey #{@ip}?.should == "hey xxx" + %/hey #{@ip}/.should == "hey xxx" + %,hey #{@ip},.should == "hey xxx" + %.hey #{@ip}..should == "hey xxx" + + # surprised? huh + %'hey #{@ip}'.should == "hey xxx" + %\hey #{@ip}\.should == "hey xxx" + %`hey #{@ip}`.should == "hey xxx" + %$hey #{@ip}$.should == "hey xxx" + end + + it "using percent with 'q', stopping interpolation" do + %q(#{@ip}).should == '#{@ip}' + end + + it "using percent with 'Q' to interpolate" do + %Q(#{@ip}).should == 'xxx' + end + + # The backslashes : + # + # \t (tab), \n (newline), \r (carriage return), \f (form feed), \b + # (backspace), \a (bell), \e (escape), \s (whitespace), \nnn (octal), + # \xnn (hexadecimal), \cx (control x), \C-x (control x), \M-x (meta x), + # \M-\C-x (meta control x) + + it "backslashes follow the same rules as interpolation" do + "\t\n\r\f\b\a\e\s\075\x62\cx".should == "\t\n\r\f\b\a\e =b\030" + '\t\n\r\f\b\a\e =b\030'.should == "\\t\\n\\r\\f\\b\\a\\e =b\\030" + end + + it "calls #to_s when the object is not a String" do + obj = mock('to_s') + obj.stub!(:to_s).and_return('42') + + "#{obj}".should == '42' + end + + it "calls #to_s as a private method" do + obj = mock('to_s') + obj.stub!(:to_s).and_return('42') + + class << obj + private :to_s + end + + "#{obj}".should == '42' + end + + it "uses an internal representation when #to_s doesn't return a String" do + obj = mock('to_s') + obj.stub!(:to_s).and_return(42) + + # See rubyspec commit 787c132d by yugui. There is value in + # ensuring that this behavior works. So rather than removing + # this spec completely, the only thing that can be asserted + # is that if you interpolate an object that fails to return + # a String, you will still get a String and not raise an + # exception. + "#{obj}".should be_an_instance_of(String) + end + + it "allows a dynamic string to parse a nested do...end block as an argument to a call without parens, interpolated" do + s = eval 'eval "#{proc do; 1; end.call}"' + s.should == 1 + end + + it "are produced from character shortcuts" do + ?z.should == 'z' + end + + it "are produced from control character shortcuts" do + # Control-Z + ?\C-z.should == "\x1A" + + # Meta-Z + ?\M-z.should == "\xFA" + + # Meta-Control-Z + ?\M-\C-z.should == "\x9A" + end + + describe "Unicode escaping" do + it "can be done with \\u and four hex digits" do + [ ["\u0000", 0x0000], + ["\u2020", 0x2020] + ].should be_computed_by(:ord) + end + + it "can be done with \\u{} and one to six hex digits" do + [ ["\u{a}", 0xa], + ["\u{ab}", 0xab], + ["\u{abc}", 0xabc], + ["\u{1abc}", 0x1abc], + ["\u{12abc}", 0x12abc], + ["\u{100000}", 0x100000] + ].should be_computed_by(:ord) + end + + # 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::BINARY + end + + it "produces an ASCII string when escaping ASCII characters via \\u{}" do + "\u{0000}".encoding.should == Encoding::BINARY + end + + it "produces a UTF-8-encoded string when escaping non-ASCII characters via \\u" do + "\u1234".encoding.should == Encoding::UTF_8 + end + + it "produces a UTF-8-encoded string when escaping non-ASCII characters via \\u{}" do + "\u{1234}".encoding.should == Encoding::UTF_8 + end + end + end +end + +# TODO: rewrite all specs above this + +describe "Ruby String literals" do + def str_concat + "foo" "bar" "baz" + end + + def long_string_literals + "Beautiful is better than ugly." \ + "Explicit is better than implicit." + end + + it "on a single line with spaces in between are concatenated together" do + str_concat.should == "foobarbaz" + end + + it "on multiple lines with newlines and backslash in between are concatenated together" do + long_string_literals.should == "Beautiful is better than ugly.Explicit is better than implicit." + 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 in different files" do + ruby_exe(fixture(__FILE__, "freeze_magic_comment_across_files.rb")).chomp.should == "true" + end + + guard -> { !(eval("'test'").frozen? && "test".equal?("test")) } do + it "produces different objects for literals with the same content in different files if the other file doesn't have the comment and String literals aren't frozen by default" do + ruby_exe(fixture(__FILE__, "freeze_magic_comment_across_files_no_comment.rb")).chomp.should == "true" + end + end + + guard -> { eval("'test'").frozen? && "test".equal?("test") } do + it "produces the same objects for literals with the same content in different files if the other file doesn't have the comment and String literals are frozen by default" do + ruby_exe(fixture(__FILE__, "freeze_magic_comment_across_files_no_comment.rb")).chomp.should == "false" + end + end + + it "produce different objects for literals with the same content in different files if they have different encodings" do + ruby_exe(fixture(__FILE__, "freeze_magic_comment_across_files_diff_enc.rb")).chomp.should == "true" + end + end + +end + +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 + + 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 + + b = "abc".encode("utf-8") + "#{b}".encoding.should == Encoding::BINARY + end + + it "raises an Encoding::CompatibilityError if the Encodings are not compatible" do + a = "\u3042" + b = "\xff".dup.force_encoding "binary" + + -> { "#{a} #{b}" }.should raise_error(Encoding::CompatibilityError) + end + + it "creates a non-frozen String" do + code = <<~'RUBY' + "a#{6*7}c" + RUBY + eval(code).should_not.frozen? + end + + it "creates a non-frozen String when # frozen-string-literal: true is used" do + code = <<~'RUBY' + # frozen-string-literal: true + "a#{6*7}c" + RUBY + eval(code).should_not.frozen? + end +end diff --git a/spec/ruby/language/super_spec.rb b/spec/ruby/language/super_spec.rb new file mode 100644 index 0000000000..7d9e896d8b --- /dev/null +++ b/spec/ruby/language/super_spec.rb @@ -0,0 +1,464 @@ +require_relative '../spec_helper' +require_relative 'fixtures/super' + +describe "The super keyword" do + it "calls the method on the calling class" do + 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 + 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 + 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 + 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 + 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 + SuperSpecs::MultiSuperTargets::A.new.foo.should == :BaseA + SuperSpecs::MultiSuperTargets::B.new.foo.should == :BaseB + end + + it "searches class methods including modules" do + 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 + SuperSpecs::IncludesFromBasic.new.__send__(:foobar).should == 43 + end + + it "searches BasicObject through another module for methods defined there" do + SuperSpecs::IncludesIntermediate.new.__send__(:foobar).should == 42 + end + + it "calls the correct method when the method visibility is modified" do + SuperSpecs::MS4::A.new.example.should == 5 + end + + it "calls the correct method when the superclass argument list is different from the subclass" do + 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 when super method does not exist" do + sup = Class.new + sub_normal = Class.new(sup) do + def foo + super() + end + end + sub_zsuper = Class.new(sup) do + def foo + super + end + end + + -> {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 + c1 = Class.new do + def m + yield + end + end + c2 = Class.new(c1) do + def m(v) + super() + end + end + + 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 + SuperSpecs::S6.new.here.should == :good + end + + it "calls the superclass method when initial method is defined_method'd" do + SuperSpecs::S7.new.here.should == :good + end + + it "can call through a define_method multiple times (caching check)" do + obj = SuperSpecs::S7.new + + 2.times do + obj.here.should == :good + end + end + + it "supers up appropriate name even if used for multiple method names" do + sup = Class.new do + def a; "a"; end + def b; "b"; end + end + + sub = Class.new(sup) do + [:a, :b].each do |name| + define_method name do + super() + end + end + end + + sub.new.a.should == "a" + sub.new.b.should == "b" + sub.new.a.should == "a" + end + + it "raises a RuntimeError when called with implicit arguments from a method defined with define_method" do + super_class = Class.new do + def a(arg) + arg + end + end + + klass = Class.new super_class do + define_method :a do |arg| + super + end + end + + -> { klass.new.a(:a_called) }.should raise_error(RuntimeError) + end + + it "is able to navigate to super, when a method is defined dynamically on the singleton class" do + foo_class = Class.new do + def bar + "bar" + end + end + + mixin_module = Module.new do + def bar + "super_" + super + end + end + + foo = foo_class.new + foo.singleton_class.define_method(:bar, mixin_module.instance_method(:bar)) + + foo.bar.should == "super_bar" + end + + # Rubinius ticket github#157 + it "calls method_missing when a superclass method is not found" do + SuperSpecs::MM_B.new.is_a?(Hash).should == false + end + + # Rubinius ticket github#180 + it "respects the original module a method is aliased from" do + SuperSpecs::Alias3.new.name3.should == [:alias2, :alias1] + end + + it "sees the included version of a module a method is alias from" do + SuperSpecs::AliasWithSuper::Trigger.foo.should == [:b, :a] + end + + it "find super from a singleton class" do + obj = SuperSpecs::SingletonCase::Foo.new + def obj.foobar(array) + array << :singleton + super + end + obj.foobar([]).should == [:singleton, :foo, :base] + end + + it "finds super on other objects if a singleton class aliased the method" do + orig_obj = SuperSpecs::SingletonAliasCase::Foo.new + orig_obj.alias_on_singleton + orig_obj.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 + SuperSpecs::RestArgsWithSuper::B.new.a("bar").should == ["bar", "foo"] + end + + it "passes along modified rest args when they were originally empty" do + 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 + 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 + SuperSpecs::ZSuperWithBlock::B.new.a.should == 14 + end + + it "passes along block via reference to method expecting a reference" do + SuperSpecs::ZSuperWithBlock::B.new.b.should == [14, 15] + end + + it "passes along a block via reference to a method that yields" do + SuperSpecs::ZSuperWithBlock::B.new.c.should == 16 + end + + it "without explicit arguments passes optional arguments that have a default value" do + SuperSpecs::ZSuperWithOptional::B.new.m(1, 2).should == 14 + end + + it "without explicit arguments passes optional arguments that have a non-default value" do + 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 + 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 + SuperSpecs::ZSuperWithOptional::C.new.m(1, 2, 3).should == 100 + end + + it "without explicit arguments passes rest arguments" do + SuperSpecs::ZSuperWithRest::B.new.m(1, 2, 3).should == [1, 2, 3] + end + + it "without explicit arguments passes rest arguments including any modifications" do + SuperSpecs::ZSuperWithRest::B.new.m_modified(1, 2, 3).should == [1, 14, 3] + end + + it "without explicit arguments passes arguments and rest arguments" do + 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 + SuperSpecs::ZSuperWithRestAndOthers::B.new.m_modified(1, 2, 3, 4, 5).should == [3, 14, 5] + end + + it "without explicit arguments that are '_'" do + SuperSpecs::ZSuperWithUnderscores::B.new.m(1, 2).should == [1, 2] + SuperSpecs::ZSuperWithUnderscores::B.new.m3(1, 2, 3).should == [1, 2, 3] + SuperSpecs::ZSuperWithUnderscores::B.new.m4(1, 2, 3, 4).should == [1, 2, 3, 4] + SuperSpecs::ZSuperWithUnderscores::B.new.m_default(1).should == [1] + SuperSpecs::ZSuperWithUnderscores::B.new.m_default.should == [0] + SuperSpecs::ZSuperWithUnderscores::B.new.m_pre_default_rest_post(1, 2, 3, 4, 5, 6, 7).should == [1, 2, 3, 4, 5, 6, 7] + SuperSpecs::ZSuperWithUnderscores::B.new.m_rest(1, 2).should == [1, 2] + SuperSpecs::ZSuperWithUnderscores::B.new.m_kwrest(a: 1).should == {a: 1} + end + + it "without explicit arguments that are '_' including any modifications" do + SuperSpecs::ZSuperWithUnderscores::B.new.m_modified(1, 2).should == [14, 2] + end + + it "should pass method arguments when called within a closure" do + SuperSpecs::ZSuperInBlock::B.new.m(arg: 1).should == 1 + end + + describe 'when using keyword arguments' do + before :each do + @req = SuperSpecs::Keywords::RequiredArguments.new + @opts = SuperSpecs::Keywords::OptionalArguments.new + @etc = SuperSpecs::Keywords::PlaceholderArguments.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 = SuperSpecs::Keywords::RequiredAndOptionalAndPlaceholderArguments.new + end + + it 'does not pass any arguments to the parent when none are given' do + @etc.foo.should == {} + end + + it 'passes only required arguments to the parent when no optional arguments are given' do + [@req, @req_and_etc].each do |obj| + obj.foo(a: 'a').should == {a: 'a'} + end + end + + it 'passes default argument values to the parent' do + [@opts, @opts_and_etc].each do |obj| + obj.foo.should == {b: 'b'} + end + + [@req_and_opts, @opts_and_etc, @req_and_opts_and_etc].each do |obj| + obj.foo(a: 'a').should == {a: 'a', b: 'b'} + end + end + + it 'passes any given arguments including optional keyword arguments to the parent' do + [@etc, @req_and_opts, @req_and_etc, @opts_and_etc, @req_and_opts_and_etc].each do |obj| + obj.foo(a: 'a', b: 'b').should == {a: 'a', b: 'b'} + end + + [@etc, @req_and_etc, @opts_and_etc, @req_and_opts_and_etc].each do |obj| + obj.foo(a: 'a', b: 'b', c: 'c').should == {a: 'a', b: 'b', c: 'c'} + end + end + end + + describe 'when using regular and keyword arguments' do + before :each do + @req = SuperSpecs::RegularAndKeywords::RequiredArguments.new + @opts = SuperSpecs::RegularAndKeywords::OptionalArguments.new + @etc = SuperSpecs::RegularAndKeywords::PlaceholderArguments.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 = SuperSpecs::RegularAndKeywords::RequiredAndOptionalAndPlaceholderArguments.new + end + + it 'passes only required regular arguments to the parent when no optional keyword arguments are given' do + @etc.foo('a').should == ['a', {}] + end + + it 'passes only required regular and keyword arguments to the parent when no optional keyword arguments are given' do + [@req, @req_and_etc].each do |obj| + obj.foo('a', b: 'b').should == ['a', {b: 'b'}] + end + end + + it 'passes default argument values to the parent' do + [@opts, @opts_and_etc].each do |obj| + obj.foo('a').should == ['a', {c: 'c'}] + end + + [@req_and_opts, @opts_and_etc, @req_and_opts_and_etc].each do |obj| + obj.foo('a', b: 'b').should == ['a', {b: 'b', c: 'c'}] + end + end + + it 'passes any given regular and keyword arguments including optional keyword arguments to the parent' do + [@etc, @req_and_opts, @req_and_etc, @opts_and_etc, @req_and_opts_and_etc].each do |obj| + obj.foo('a', b: 'b', c: 'c').should == ['a', {b: 'b', c: 'c'}] + end + + [@etc, @req_and_etc, @opts_and_etc, @req_and_opts_and_etc].each do |obj| + obj.foo('a', b: 'b', c: 'c', d: 'd').should == ['a', {b: 'b', c: 'c', d: 'd'}] + end + end + end + + describe 'when using splat and keyword arguments' do + before :each do + @all = SuperSpecs::SplatAndKeywords::AllArguments.new + end + + it 'does not pass any arguments to the parent when none are given' do + @all.foo.should == [[], {}] + end + + it 'passes only splat arguments to the parent when no keyword arguments are given' do + @all.foo('a').should == [['a'], {}] + end + + it 'passes only keyword arguments to the parent when no splat arguments are given' do + @all.foo(b: 'b').should == [[], {b: 'b'}] + end + + it 'passes any given splat and keyword arguments to the parent' do + @all.foo('a', b: 'b').should == [['a'], {b: 'b'}] + end + end +end diff --git a/spec/ruby/language/symbol_spec.rb b/spec/ruby/language/symbol_spec.rb new file mode 100644 index 0000000000..0801d3223e --- /dev/null +++ b/spec/ruby/language/symbol_spec.rb @@ -0,0 +1,108 @@ +require_relative '../spec_helper' + +describe "A Symbol literal" do + it "is a ':' followed by any number of valid characters" do + a = :foo + a.should be_kind_of(Symbol) + a.inspect.should == ':foo' + end + + it "is a ':' followed by any valid variable, method, or constant name" do + # Add more of these? + [ :Foo, + :foo, + :@foo, + :@@foo, + :$foo, + :_, + :~, + :- , + :FOO, + :_Foo, + :&, + :_9 + ].each { |s| s.should be_kind_of(Symbol) } + end + + it "is a ':' followed by a single- or double-quoted string that may contain otherwise invalid characters" do + [ [:'foo bar', ':"foo bar"'], + [:'++', ':"++"'], + [:'9', ':"9"'], + [:"foo #{1 + 1}", ':"foo 2"'], + [:"foo\nbar", ':"foo\nbar"'], + ].each { |sym, str| + sym.should be_kind_of(Symbol) + sym.inspect.should == str + } + end + + 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]\n#{Encoding::BINARY.name}\n" + end + + it "may contain '::' in the string" do + :'Some::Class'.should be_kind_of(Symbol) + end + + it "is converted to a literal, unquoted representation if the symbol contains only valid characters" do + a, b, c = :'foo', :'+', :'Foo__9' + a.should be_kind_of(Symbol) + a.inspect.should == ':foo' + b.should be_kind_of(Symbol) + b.inspect.should == ':+' + c.should be_kind_of(Symbol) + c.inspect.should == ':Foo__9' + end + + it "can be created by the %s-delimited expression" do + a, b = :'foo bar', %s{foo bar} + b.should be_kind_of(Symbol) + b.inspect.should == ':"foo bar"' + b.should == a + end + + it "is the same object when created from identical strings" do + var = "@@var" + [ [:symbol, :symbol], + [:'a string', :'a string'], + [:"#{var}", :"#{var}"] + ].each { |a, b| + a.should equal(b) + } + end + + it "can contain null in the string" do + eval(':"\0" ').inspect.should == ':"\\x00"' + end + + it "can be an empty string" do + c = :'' + c.should be_kind_of(Symbol) + c.inspect.should == ':""' + end + + it "can be :!, :!=, or :!~" do + %w{'!', '!=', '!~'}.each do |sym| + sym.to_sym.to_s.should == sym + end + end + + it "can be created from list syntax %i{a b c} without interpolation" do + %i{a b #{c}}.should == [:a, :b, :"\#{c}"] + end + + it "can be created from list syntax %I{a b c} with interpolation" do + %I{a b #{"c"}}.should == [:a, :b, :c] + end + + 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 new file mode 100644 index 0000000000..d723843688 --- /dev/null +++ b/spec/ruby/language/throw_spec.rb @@ -0,0 +1,81 @@ +require_relative '../spec_helper' + +describe "The throw keyword" do + it "abandons processing" do + i = 0 + catch(:done) do + loop do + i += 1 + throw :done if i > 4 + end + i += 1 + end + i.should == 5 + end + + it "supports a second parameter" do + msg = catch(:exit) do + throw :exit,:msg + end + msg.should == :msg + end + + it "uses nil as a default second parameter" do + msg = catch(:exit) do + throw :exit + end + msg.should == nil + end + + it "clears the current exception" do + catch :exit do + begin + raise "exception" + rescue + throw :exit + end + end + $!.should be_nil + end + + it "allows any object as its argument" do + catch(1) { throw 1, 2 }.should == 2 + o = Object.new + catch(o) { throw o, o }.should == o + end + + it "does not convert strings to a symbol" do + -> { catch(:exit) { throw "exit" } }.should raise_error(ArgumentError) + end + + it "unwinds stack from within a method" do + def throw_method(handler, val) + throw handler, val + end + + catch(:exit) do + throw_method(:exit, 5) + end.should == 5 + end + + it "unwinds stack from within a lambda" do + c = -> { throw :foo, :msg } + catch(:foo) { c.call }.should == :msg + end + + it "raises an ArgumentError if outside of scope of a matching catch" do + -> { throw :test, 5 }.should raise_error(ArgumentError) + -> { catch(:different) { throw :test, 5 } }.should raise_error(ArgumentError) + end + + it "raises an UncaughtThrowError if used to exit a thread" do + catch(:what) do + t = Thread.new { + -> { + throw :what + }.should raise_error(UncaughtThrowError) + } + t.join + end + end +end diff --git a/spec/ruby/language/undef_spec.rb b/spec/ruby/language/undef_spec.rb new file mode 100644 index 0000000000..268c0b84c3 --- /dev/null +++ b/spec/ruby/language/undef_spec.rb @@ -0,0 +1,79 @@ +require_relative '../spec_helper' + +describe "The undef keyword" do + describe "undefines a method" do + before :each do + @undef_class = Class.new do + def meth(o); o; end + end + @obj = @undef_class.new + @obj.meth(5).should == 5 + end + + it "with an identifier" do + @undef_class.class_eval do + undef meth + end + -> { @obj.meth(5) }.should raise_error(NoMethodError) + end + + it "with a simple symbol" do + @undef_class.class_eval do + undef :meth + end + -> { @obj.meth(5) }.should raise_error(NoMethodError) + end + + it "with a single quoted symbol" do + @undef_class.class_eval do + undef :'meth' + end + -> { @obj.meth(5) }.should raise_error(NoMethodError) + end + + it "with a double quoted symbol" do + @undef_class.class_eval do + undef :"meth" + end + -> { @obj.meth(5) }.should raise_error(NoMethodError) + end + + it "with an interpolated symbol" do + @undef_class.class_eval do + undef :"#{'meth'}" + end + -> { @obj.meth(5) }.should raise_error(NoMethodError) + end + + it "with an interpolated symbol when interpolated expression is not a String literal" do + @undef_class.class_eval do + undef :"#{'meth'.to_sym}" + end + -> { @obj.meth(5) }.should raise_error(NoMethodError) + end + end + + it "allows undefining multiple methods at a time" do + undef_multiple = Class.new do + def method1; end + def method2; :nope; end + + undef :method1, :method2 + end + + obj = undef_multiple.new + obj.respond_to?(:method1).should == false + obj.respond_to?(:method2).should == false + end + + it "raises a NameError when passed a missing name" do + Class.new do + -> { + undef not_exist + }.should raise_error(NameError) { |e| + # a NameError and not a NoMethodError + e.class.should == NameError + } + end + end +end diff --git a/spec/ruby/language/unless_spec.rb b/spec/ruby/language/unless_spec.rb new file mode 100644 index 0000000000..98acdc083b --- /dev/null +++ b/spec/ruby/language/unless_spec.rb @@ -0,0 +1,43 @@ +require_relative '../spec_helper' + +describe "The unless expression" do + it "evaluates the unless body when the expression is false" do + unless false + a = true + else + a = false + end + + a.should == true + end + + it "returns the last statement in the body" do + unless false + 'foo' + 'bar' + 'baz' + end.should == 'baz' + end + + it "evaluates the else body when the expression is true" do + unless true + 'foo' + else + 'bar' + end.should == 'bar' + end + + it "takes an optional then after the expression" do + unless false then + 'baz' + end.should == 'baz' + end + + it "does not return a value when the expression is true" do + unless true; end.should == nil + end + + it "allows expression and body to be on one line (using 'then')" do + unless false then 'foo'; else 'bar'; end.should == 'foo' + end +end diff --git a/spec/ruby/language/until_spec.rb b/spec/ruby/language/until_spec.rb new file mode 100644 index 0000000000..78c289ff56 --- /dev/null +++ b/spec/ruby/language/until_spec.rb @@ -0,0 +1,234 @@ +require_relative '../spec_helper' + +# until bool-expr [do] +# body +# end +# +# begin +# body +# end until bool-expr +# +# expr until bool-expr +describe "The until expression" do + it "runs while the expression is false" do + i = 0 + until i > 9 + i += 1 + end + + i.should == 10 + end + + it "optionally takes a 'do' after the expression" do + i = 0 + until i > 9 do + i += 1 + end + + i.should == 10 + end + + it "allows body begin on the same line if do is used" do + i = 0 + until i > 9 do i += 1 + end + + i.should == 10 + end + + it "executes code in containing variable scope" do + i = 0 + until i == 1 + a = 123 + i = 1 + end + + a.should == 123 + end + + it "executes code in containing variable scope with 'do'" do + i = 0 + until i == 1 do + a = 123 + i = 1 + end + + a.should == 123 + end + + it "returns nil if ended when condition became true" do + i = 0 + until i > 9 + i += 1 + end.should == nil + end + + it "evaluates the body if expression is empty" do + a = [] + until () + a << :body_evaluated + break + end + a.should == [:body_evaluated] + end + + it "stops running body if interrupted by break" do + i = 0 + until i > 9 + i += 1 + break if i > 5 + end + i.should == 6 + end + + it "returns value passed to break if interrupted by break" do + until false + break 123 + end.should == 123 + end + + it "returns nil if interrupted by break with no arguments" do + until false + break + end.should == nil + end + + it "skips to end of body with next" do + a = [] + i = 0 + until (i+=1)>=5 + next if i==3 + a << i + end + a.should == [1, 2, 4] + end + + it "restarts the current iteration without reevaluating condition with redo" do + a = [] + i = 0 + j = 0 + until (i+=1)>=3 + a << i + j+=1 + redo if j<3 + end + a.should == [1, 1, 1, 2] + end +end + +describe "The until modifier" do + it "runs preceding statement while the condition is false" do + i = 0 + i += 1 until i > 9 + i.should == 10 + end + + it "evaluates condition before statement execution" do + a = [] + i = 0 + a << i until (i+=1) >= 3 + a.should == [1, 2] + end + + it "does not run preceding statement if the condition is true" do + i = 0 + i += 1 until true + i.should == 0 + end + + it "returns nil if ended when condition became true" do + i = 0 + (i += 1 until i>9).should == nil + end + + it "returns value passed to break if interrupted by break" do + (break 123 until false).should == 123 + end + + it "returns nil if interrupted by break with no arguments" do + (break until false).should == nil + end + + it "skips to end of body with next" do + i = 0 + j = 0 + ((i+=1) == 3 ? next : j+=i) until i > 10 + j.should == 63 + end + + it "restarts the current iteration without reevaluating condition with redo" do + i = 0 + j = 0 + (i+=1) == 4 ? redo : j+=i until (i+=1) > 10 + j.should == 34 + end +end + +describe "The until modifier with begin .. end block" do + it "runs block while the expression is false" do + i = 0 + begin + i += 1 + end until i > 9 + + i.should == 10 + end + + it "stops running block if interrupted by break" do + i = 0 + begin + i += 1 + break if i > 5 + end until i > 9 + + i.should == 6 + end + + it "returns value passed to break if interrupted by break" do + (begin; break 123; end until false).should == 123 + end + + it "returns nil if interrupted by break with no arguments" do + (begin; break; end until false).should == nil + end + + it "runs block at least once (even if the expression is true)" do + i = 0 + begin + i += 1 + end until true + + i.should == 1 + end + + it "evaluates condition after block execution" do + a = [] + i = 0 + begin + a << i + end until (i+=1)>=5 + a.should == [0, 1, 2, 3, 4] + end + + it "skips to end of body with next" do + a = [] + i = 0 + begin + next if i==3 + a << i + end until (i+=1)>=5 + a.should == [0, 1, 2, 4] + end + + it "restart the current iteration without reevaluating condition with redo" do + a = [] + i = 0 + j = 0 + begin + a << i + j+=1 + redo if j<3 + end until (i+=1)>=3 + a.should == [0, 0, 0, 1, 2] + end +end diff --git a/spec/ruby/language/variables_spec.rb b/spec/ruby/language/variables_spec.rb new file mode 100644 index 0000000000..e134271939 --- /dev/null +++ b/spec/ruby/language/variables_spec.rb @@ -0,0 +1,930 @@ +require_relative '../spec_helper' +require_relative 'fixtures/variables' + +describe "Evaluation order during assignment" do + context "with single assignment" do + it "evaluates from left to right" do + obj = VariablesSpecs::EvalOrder.new + obj.instance_eval do + foo[0] = a + end + + obj.order.should == ["foo", "a", "foo[]="] + end + end + + context "with multiple assignment" do + it "evaluates from left to right, receivers first then methods" do + obj = VariablesSpecs::EvalOrder.new + obj.instance_eval do + foo[0], bar.baz = a, b + end + + obj.order.should == ["foo", "bar", "a", "b", "foo[]=", "bar.baz="] + end + + it "can be used to swap variables with nested method calls" do + node = VariablesSpecs::EvalOrder.new.node + + original_node = node + original_node_left = node.left + original_node_left_right = node.left.right + + node.left, node.left.right, node = node.left.right, node, node.left + # Should evaluate in the order of: + # LHS: node, node.left(original_node_left) + # RHS: original_node_left_right, original_node, original_node_left + # Ops: + # * node(original_node), original_node.left = original_node_left_right + # * original_node_left.right = original_node + # * node = original_node_left + + node.should == original_node_left + node.right.should == original_node + node.right.left.should == original_node_left_right + end + end +end + +describe "Multiple assignment" do + context "with a single RHS value" do + it "assigns a simple MLHS" do + (a, b, c = 1).should == 1 + [a, b, c].should == [1, nil, nil] + end + + it "calls #to_ary to convert an Object RHS when assigning a simple MLHS" do + x = mock("multi-assign single RHS") + x.should_receive(:to_ary).and_return([1, 2]) + + (a, b, c = x).should == x + [a, b, c].should == [1, 2, nil] + end + + it "calls #to_ary if it is private" do + x = mock("multi-assign single RHS") + x.should_receive(:to_ary).and_return([1, 2]) + class << x; private :to_ary; end + + (a, b, c = x).should == x + [a, b, c].should == [1, 2, nil] + end + + it "does not call #to_ary if #respond_to? returns false" do + x = mock("multi-assign single RHS") + x.should_receive(:respond_to?).with(:to_ary, true).and_return(false) + x.should_not_receive(:to_ary) + + (a, b, c = x).should == x + [a, b, c].should == [x, nil, nil] + end + + it "wraps the Object in an Array if #to_ary returns nil" do + x = mock("multi-assign single RHS") + x.should_receive(:to_ary).and_return(nil) + + (a, b, c = x).should == x + [a, b, c].should == [x, nil, nil] + end + + it "raises a TypeError of #to_ary does not return an Array" do + x = mock("multi-assign single RHS") + x.should_receive(:to_ary).and_return(1) + + -> { 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 + x = mock("multi-assign single RHS") + x.should_not_receive(:to_a) + + (a, b, c = x).should == x + [a, b, c].should == [x, nil, nil] + end + + it "does not call #to_ary on an Array instance" do + x = [1, 2] + x.should_not_receive(:to_ary) + + (a, b = x).should == x + [a, b].should == [1, 2] + end + + it "does not call #to_a on an Array instance" do + x = [1, 2] + x.should_not_receive(:to_a) + + (a, b = x).should == x + [a, b].should == [1, 2] + end + + it "returns the RHS when it is an Array" do + ary = [1, 2] + + x = (a, b = ary) + x.should equal(ary) + end + + it "returns the RHS when it is an Array subclass" do + cls = Class.new(Array) + ary = cls.new [1, 2] + + x = (a, b = ary) + x.should equal(ary) + end + + it "does not call #to_ary on an Array subclass instance" do + x = Class.new(Array).new [1, 2] + x.should_not_receive(:to_ary) + + (a, b = x).should == x + [a, b].should == [1, 2] + end + + it "does not call #to_a on an Array subclass instance" do + x = Class.new(Array).new [1, 2] + x.should_not_receive(:to_a) + + (a, b = x).should == x + [a, b].should == [1, 2] + end + + it "assigns a MLHS with a trailing comma" do + a, = 1 + b, c, = [] + [a, b, c].should == [1, nil, nil] + end + + it "assigns a single LHS splat" do + (*a = 1).should == 1 + a.should == [1] + end + + it "calls #to_ary to convert an Object RHS" do + x = mock("multi-assign splat") + x.should_receive(:to_ary).and_return([1, 2]) + + (*a = x).should == x + a.should == [1, 2] + end + + it "raises a TypeError if #to_ary does not return an Array" do + x = mock("multi-assign splat") + x.should_receive(:to_ary).and_return(1) + + -> { *a = x }.should raise_error(TypeError) + end + + it "does not call #to_ary on an Array subclass" do + cls = Class.new(Array) + ary = cls.new [1, 2] + ary.should_not_receive(:to_ary) + + (*a = ary).should == [1, 2] + a.should == [1, 2] + end + + it "assigns an Array when the RHS is an Array subclass" do + cls = Class.new(Array) + ary = cls.new [1, 2] + + x = (*a = ary) + x.should equal(ary) + a.should be_an_instance_of(Array) + end + + it "calls #to_ary to convert an Object RHS with MLHS" do + x = mock("multi-assign splat") + x.should_receive(:to_ary).and_return([1, 2]) + + (a, *b, c = x).should == x + [a, b, c].should == [1, [], 2] + end + + it "raises a TypeError if #to_ary does not return an Array with MLHS" do + x = mock("multi-assign splat") + x.should_receive(:to_ary).and_return(1) + + -> { a, *b, c = x }.should raise_error(TypeError) + end + + it "does not call #to_a to convert an Object RHS with a MLHS" do + x = mock("multi-assign splat") + x.should_not_receive(:to_a) + + (a, *b = x).should == x + [a, b].should == [x, []] + end + + it "assigns a MLHS with leading splat" do + (*a, b, c = 1).should == 1 + [a, b, c].should == [[], 1, nil] + end + + it "assigns a MLHS with a middle splat" do + a, b, *c, d, e = 1 + [a, b, c, d, e].should == [1, nil, [], nil, nil] + end + + it "assigns a MLHS with a trailing splat" do + a, b, *c = 1 + [a, b, c].should == [1, nil, []] + end + + it "assigns a grouped LHS without splat" do + ((a, b), c), (d, (e,), (f, (g, h))) = 1 + [a, b, c, d, e, f, g, h].should == [1, nil, nil, nil, nil, nil, nil, nil] + end + + it "assigns a single grouped LHS splat" do + (*a) = nil + a.should == [nil] + end + + it "assigns a grouped LHS with splats" do + (a, *b), c, (*d, (e, *f, g)) = 1 + [a, b, c, d, e, f, g].should == [1, [], nil, [], nil, [], nil] + end + + it "consumes values for an anonymous splat" do + (* = 1).should == 1 + end + + it "consumes values for a grouped anonymous splat" do + ((*) = 1).should == 1 + end + + it "does not mutate a RHS Array" do + x = [1, 2, 3, 4] + a, *b, c, d = x + [a, b, c, d].should == [1, [2], 3, 4] + x.should == [1, 2, 3, 4] + end + + it "assigns values from a RHS method call" do + def x() 1 end + + (a, b = x).should == 1 + [a, b].should == [1, nil] + end + + it "assigns values from a RHS method call with arguments" do + def x(a) a end + + (a, b = x []).should == [] + [a, b].should == [nil, nil] + end + + it "assigns values from a RHS method call with receiver" do + x = mock("multi-assign attributes") + x.should_receive(:m).and_return([1, 2, 3]) + + a, b = x.m + [a, b].should == [1, 2] + end + + it "calls #to_ary on the value returned by the method call" do + y = mock("multi-assign method return value") + y.should_receive(:to_ary).and_return([1, 2]) + + x = mock("multi-assign attributes") + x.should_receive(:m).and_return(y) + + (a, b = x.m).should == y + [a, b].should == [1, 2] + end + + it "raises a TypeError if #to_ary does not return an Array on a single RHS" do + y = mock("multi-assign method return value") + y.should_receive(:to_ary).and_return(1) + + x = mock("multi-assign attributes") + x.should_receive(:m).and_return(y) + + -> { a, b = x.m }.should raise_error(TypeError) + end + + it "assigns values from a RHS method call with receiver and arguments" do + x = mock("multi-assign attributes") + x.should_receive(:m).with(1, 2).and_return([1, 2, 3]) + + a, b = x.m 1, 2 + [a, b].should == [1, 2] + end + + it "assigns global variables" do + $spec_a, $spec_b = 1 + [$spec_a, $spec_b].should == [1, nil] + end + + it "assigns instance variables" do + @a, @b = 1 + [@a, @b].should == [1, nil] + end + + it "assigns attributes" do + a = mock("multi-assign attributes") + a.should_receive(:x=).with(1) + a.should_receive(:y=).with(nil) + + a.x, a.y = 1 + end + + it "assigns indexed elements" do + a = [] + 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 + module VariableSpecs + 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 + + context "with a single splatted RHS value" do + it "assigns a single grouped LHS splat" do + (*a) = *1 + a.should == [1] + end + + it "assigns an empty Array to a single LHS value when passed nil" do + (a = *nil).should == [] + a.should == [] + end + + 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 == [] + 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 + ary = [1, 2] + ary.should_not_receive(:to_a) + + (a = *ary).should == [1, 2] + a.should == [1, 2] + end + + it "returns a copy of a splatted Array" do + ary = [1, 2] + + (a = *ary).should == [1, 2] + a.should_not equal(ary) + end + + it "does not call #to_a on an Array subclass" do + cls = Class.new(Array) + ary = cls.new [1, 2] + ary.should_not_receive(:to_a) + + (a = *ary).should == [1, 2] + a.should == [1, 2] + end + + it "returns an Array when the splatted object is an Array subclass" do + cls = Class.new(Array) + ary = cls.new [1, 2] + + x = (a = *ary) + + x.should == [1, 2] + x.should be_an_instance_of(Array) + + a.should == [1, 2] + 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] + end + + it "consumes values for a grouped anonymous splat" do + ((*) = *1).should == [1] + end + + it "assigns a single LHS splat" do + x = 1 + (*a = *x).should == [1] + a.should == [1] + end + + it "calls #to_a to convert an Object RHS with a single splat LHS" do + x = mock("multi-assign RHS splat") + x.should_receive(:to_a).and_return([1, 2]) + + (*a = *x).should == [1, 2] + a.should == [1, 2] + end + + it "calls #to_a if it is private" do + x = mock("multi-assign RHS splat") + x.should_receive(:to_a).and_return([1, 2]) + class << x; private :to_a; end + + (*a = *x).should == [1, 2] + a.should == [1, 2] + end + + it "does not call #to_a if #respond_to? returns false" do + x = mock("multi-assign RHS splat") + x.should_receive(:respond_to?).with(:to_a, true).and_return(false) + x.should_not_receive(:to_a) + + (*a = *x).should == [x] + a.should == [x] + end + + it "wraps the Object in an Array if #to_a returns nil" do + x = mock("multi-assign RHS splat") + x.should_receive(:to_a).and_return(nil) + + (*a = *x).should == [x] + a.should == [x] + end + + it "raises a TypeError if #to_a does not return an Array" do + x = mock("multi-assign RHS splat") + x.should_receive(:to_a).and_return(1) + + -> { *a = *x }.should raise_error(TypeError) + end + + it "does not call #to_ary to convert an Object RHS with a single splat LHS" do + x = mock("multi-assign RHS splat") + x.should_not_receive(:to_ary) + + (*a = *x).should == [x] + a.should == [x] + end + + it "assigns a MLHS with leading splat" do + (*a, b, c = *1).should == [1] + [a, b, c].should == [[], 1, nil] + end + + it "assigns a MLHS with a middle splat" do + a, b, *c, d, e = *1 + [a, b, c, d, e].should == [1, nil, [], nil, nil] + end + + it "assigns a MLHS with a trailing splat" do + a, b, *c = *nil + [a, b, c].should == [nil, nil, []] + end + + it "calls #to_a to convert an Object RHS with a single LHS" do + x = mock("multi-assign RHS splat") + x.should_receive(:to_a).and_return([1, 2]) + + (a = *x).should == [1, 2] + a.should == [1, 2] + end + + it "does not call #to_ary to convert an Object RHS with a single LHS" do + x = mock("multi-assign RHS splat") + x.should_not_receive(:to_ary) + + (a = *x).should == [x] + a.should == [x] + end + + it "raises a TypeError if #to_a does not return an Array with a single LHS" do + x = mock("multi-assign splat") + x.should_receive(:to_a).and_return(1) + + -> { a = *x }.should raise_error(TypeError) + end + + it "calls #to_a to convert an Object splat RHS when assigned to a simple MLHS" do + x = mock("multi-assign splat") + x.should_receive(:to_a).and_return([1, 2]) + + (a, b, c = *x).should == [1, 2] + [a, b, c].should == [1, 2, nil] + end + + it "raises a TypeError if #to_a does not return an Array with a simple MLHS" do + x = mock("multi-assign splat") + x.should_receive(:to_a).and_return(1) + + -> { 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 + x = mock("multi-assign splat") + x.should_not_receive(:to_ary) + + (a, b, c = *x).should == [x] + [a, b, c].should == [x, nil, nil] + end + + it "calls #to_a to convert an Object RHS with MLHS" do + x = mock("multi-assign splat") + x.should_receive(:to_a).and_return([1, 2]) + + a, *b, c = *x + [a, b, c].should == [1, [], 2] + end + + it "raises a TypeError if #to_a does not return an Array with MLHS" do + x = mock("multi-assign splat") + x.should_receive(:to_a).and_return(1) + + -> { a, *b, c = *x }.should raise_error(TypeError) + end + + it "does not call #to_ary to convert an Object RHS with a MLHS" do + x = mock("multi-assign splat") + x.should_not_receive(:to_ary) + + a, *b = *x + [a, b].should == [x, []] + end + + it "assigns a grouped LHS without splats" do + ((a, b), c), (d, (e,), (f, (g, h))) = *1 + [a, b, c, d, e, f, g, h].should == [1, nil, nil, nil, nil, nil, nil, nil] + end + + it "assigns a grouped LHS with splats" do + (a, *b), c, (*d, (e, *f, g)) = *1 + [a, b, c, d, e, f, g].should == [1, [], nil, [], nil, [], nil] + end + + it "does not mutate a RHS Array" do + x = [1, 2, 3, 4] + a, *b, c, d = *x + [a, b, c, d].should == [1, [2], 3, 4] + x.should == [1, 2, 3, 4] + end + + it "assigns constants" do + module VariableSpecs + (*SINGLE_SPLATTED_RHS) = *1 + SINGLE_SPLATTED_RHS.should == [1] + end + ensure + VariableSpecs.send(:remove_const, :SINGLE_SPLATTED_RHS) + end + end + + context "with a MRHS value" do + it "consumes values for an anonymous splat" do + (* = 1, 2, 3).should == [1, 2, 3] + end + + it "consumes values for a grouped anonymous splat" do + ((*) = 1, 2, 3).should == [1, 2, 3] + end + + it "consumes values for multiple '_' variables" do + a, _, b, _, c = 1, 2, 3, 4, 5 + [a, b, c].should == [1, 3, 5] + end + + it "does not call #to_a to convert an Object in a MRHS" do + x = mock("multi-assign MRHS") + x.should_not_receive(:to_a) + + (a, b = 1, x).should == [1, x] + [a, b].should == [1, x] + end + + it "does not call #to_ary to convert an Object in a MRHS" do + x = mock("multi-assign MRHS") + x.should_not_receive(:to_ary) + + (a, b = 1, x).should == [1, x] + [a, b].should == [1, x] + end + + it "calls #to_a to convert a splatted Object as part of a MRHS with a splat MLHS" do + x = mock("multi-assign splat MRHS") + x.should_receive(:to_a).and_return([3, 4]) + + (a, *b = 1, *x).should == [1, 3, 4] + [a, b].should == [1, [3, 4]] + end + + it "raises a TypeError if #to_a does not return an Array with a splat MLHS" do + x = mock("multi-assign splat MRHS") + x.should_receive(:to_a).and_return(1) + + -> { 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 + x = mock("multi-assign splat MRHS") + x.should_not_receive(:to_ary) + + (a, *b = 1, *x).should == [1, x] + [a, b].should == [1, [x]] + end + + it "calls #to_a to convert a splatted Object as part of a MRHS" do + x = mock("multi-assign splat MRHS") + x.should_receive(:to_a).and_return([3, 4]) + + (a, *b = *x, 1).should == [3, 4, 1] + [a, b].should == [3, [4, 1]] + end + + it "raises a TypeError if #to_a does not return an Array with a splat MRHS" do + x = mock("multi-assign splat MRHS") + x.should_receive(:to_a).and_return(1) + + -> { 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 + x = mock("multi-assign splat MRHS") + x.should_not_receive(:to_ary) + + (a, *b = *x, 1).should == [x, 1] + [a, b].should == [x, [1]] + end + + it "assigns a grouped LHS without splat from a simple Array" do + ((a, b), c), (d, (e,), (f, (g, h))) = 1, 2, 3, 4, 5 + [a, b, c, d, e, f, g, h].should == [1, nil, nil, 2, nil, nil, nil, nil] + end + + it "assigns a grouped LHS without splat from nested Arrays" do + ary = [[1, 2, 3], 4], [[5], [6, 7], [8, [9, 10]]] + ((a, b), c), (d, (e,), (f, (g, h))) = ary + [a, b, c, d, e, f, g, h].should == [1, 2, 4, [5], 6, 8, 9, 10] + end + + it "assigns a single grouped LHS splat" do + (*a) = 1, 2, 3 + a.should == [1, 2, 3] + end + + it "assigns a grouped LHS with splats from nested Arrays for simple values" do + (a, *b), c, (*d, (e, *f, g)) = 1, 2, 3, 4 + [a, b, c, d, e, f, g].should == [1, [], 2, [], 3, [], nil] + end + + it "assigns a grouped LHS with splats from nested Arrays for nested arrays" do + (a, *b), c, (*d, (e, *f, g)) = [1, [2, 3]], [4, 5], [6, 7, 8] + [a, b, c, d, e, f, g].should == [1, [[2, 3]], [4, 5], [6, 7], 8, [], nil] + end + + it "calls #to_ary to convert an Object when the position receiving the value is a multiple assignment" do + x = mock("multi-assign mixed RHS") + x.should_receive(:to_ary).and_return([1, 2]) + + (a, (b, c), d, e = 1, x, 3, 4).should == [1, x, 3, 4] + [a, b, c, d, e].should == [1, 1, 2, 3, 4] + end + + it "raises a TypeError if #to_ary does not return an Array" do + x = mock("multi-assign mixed RHS") + x.should_receive(:to_ary).and_return(x) + + -> { a, (b, c), d = 1, x, 3, 4 }.should raise_error(TypeError) + end + + it "calls #to_a to convert a splatted Object value in a MRHS" do + x = mock("multi-assign mixed splatted RHS") + x.should_receive(:to_a).and_return([4, 5]) + + (a, *b, (c, d) = 1, 2, 3, *x).should == [1, 2, 3, 4, 5] + [a, b, c, d].should == [1, [2, 3, 4], 5, nil] + + end + + it "calls #to_ary to convert a splatted Object when the position receiving the value is a multiple assignment" do + x = mock("multi-assign mixed splatted RHS") + x.should_receive(:to_ary).and_return([4, 5]) + + (a, *b, (c, d) = 1, 2, 3, *x).should == [1, 2, 3, x] + [a, b, c, d].should == [1, [2, 3], 4, 5] + end + + it "raises a TypeError if #to_ary does not return an Array in a MRHS" do + x = mock("multi-assign mixed splatted RHS") + x.should_receive(:to_ary).and_return(x) + + -> { a, *b, (c, d) = 1, 2, 3, *x }.should raise_error(TypeError) + end + + it "does not call #to_ary to convert an Object when the position receiving the value is a simple variable" do + x = mock("multi-assign mixed RHS") + x.should_not_receive(:to_ary) + + a, b, c, d = 1, x, 3, 4 + [a, b, c, d].should == [1, x, 3, 4] + end + + it "does not call #to_ary to convert an Object when the position receiving the value is a rest variable" do + x = mock("multi-assign mixed RHS") + x.should_not_receive(:to_ary) + + a, *b, c, d = 1, x, 3, 4 + [a, b, c, d].should == [1, [x], 3, 4] + end + + it "does not call #to_ary to convert a splatted Object when the position receiving the value is a simple variable" do + x = mock("multi-assign mixed splatted RHS") + x.should_not_receive(:to_ary) + + a, *b, c = 1, 2, *x + [a, b, c].should == [1, [2], x] + end + + it "does not call #to_ary to convert a splatted Object when the position receiving the value is a rest variable" do + x = mock("multi-assign mixed splatted RHS") + x.should_not_receive(:to_ary) + + a, b, *c = 1, 2, *x + [a, b, c].should == [1, 2, [x]] + end + + it "does not mutate the assigned Array" do + x = ((a, *b, c, d) = 1, 2, 3, 4, 5) + 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 + module VariableSpecs + MRHS_VALUES = 1, 2, 3 + MRHS_VALUES.should == [1, 2, 3] + end + ensure + VariableSpecs.send(:remove_const, :MRHS_VALUES) + end + end + + context "with a RHS assignment value" do + it "consumes values for an anonymous splat" do + (* = (a = 1)).should == 1 + a.should == 1 + end + + it "does not mutate a RHS Array" do + a, *b, c, d = (e = [1, 2, 3, 4]) + [a, b, c, d].should == [1, [2], 3, 4] + e.should == [1, 2, 3, 4] + end + end +end + +describe "A local variable assigned only within a conditional block" do + context "accessed from a later closure" do + it "is defined?" do + if VariablesSpecs.false + a = 1 + end + + 1.times do + defined?(a).should == "local-variable" + end + end + + it "is nil" do + if VariablesSpecs.false + a = 1 + end + + 1.times do + a.inspect.should == "nil" + end + 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 new file mode 100644 index 0000000000..e172453ca6 --- /dev/null +++ b/spec/ruby/language/while_spec.rb @@ -0,0 +1,344 @@ +require_relative '../spec_helper' + +# while bool-expr [do] +# body +# end +# +# begin +# body +# end while bool-expr +# +# expr while bool-expr +describe "The while expression" do + it "runs while the expression is true" do + i = 0 + while i < 3 + i += 1 + end + i.should == 3 + end + + it "optionally takes a 'do' after the expression" do + i = 0 + while i < 3 do + i += 1 + end + + i.should == 3 + end + + it "allows body begin on the same line if do is used" do + i = 0 + while i < 3 do i += 1 + end + + i.should == 3 + end + + it "executes code in containing variable scope" do + i = 0 + while i != 1 + a = 123 + i = 1 + end + + a.should == 123 + end + + it "executes code in containing variable scope with 'do'" do + i = 0 + while i != 1 do + a = 123 + i = 1 + end + + a.should == 123 + end + + it "returns nil if ended when condition became false" do + i = 0 + while i < 3 + i += 1 + end.should == nil + end + + it "does not evaluate the body if expression is empty" do + a = [] + while () + a << :body_evaluated + end + a.should == [] + end + + it "stops running body if interrupted by break" do + i = 0 + while i < 10 + i += 1 + break if i > 5 + end + i.should == 6 + end + + it "stops running body if interrupted by break in a parenthesized element op-assign-or value" do + c = true + a = [] + while c + a[1] ||= + ( + break if c + c = false + ) + end.should be_nil + end + + it "stops running body if interrupted by break in a begin ... end element op-assign-or value" do + c = true + a = [] + while c + a[1] ||= begin + break if c + c = false + end + end.should be_nil + end + + it "stops running body if interrupted by break in a parenthesized element op-assign value" do + c = true + a = [1, 2] + while c + a[1] += + ( + break if c + c = false + ) + end.should be_nil + a.should == [1, 2] + end + + it "stops running body if interrupted by break in a begin ... end element op-assign value" do + c = true + a = [1, 2] + while c + a[1] += begin + break if c + c = false + end + end.should be_nil + a.should == [1, 2] + end + + it "stops running body if interrupted by break with unless in a parenthesized attribute op-assign-or value" do + a = mock("attribute assignment break") + a.should_receive(:m).twice.and_return(nil) + a.should_receive(:m=) + + c = d = true + while c + a.m ||= + ( + break unless d + d = false + ) + end.should be_nil + end + + it "stops running body if interrupted by break with unless in a begin ... end attribute op-assign-or value" do + a = mock("attribute assignment break") + a.should_receive(:m).twice.and_return(nil) + a.should_receive(:m=) + + c = d = true + while c + a.m ||= begin + break unless d + d = false + end + end.should be_nil + end + + it "stops running body if interrupted by break in a parenthesized attribute op-assign-or value" do + a = mock("attribute assignment break") + a.should_receive(:m).and_return(nil) + a.should_not_receive(:m=) + + c = true + while c + a.m += + ( + break if c + c = false + ) + end.should be_nil + end + + it "stops running body if interrupted by break in a begin ... end attribute op-assign-or value" do + a = mock("attribute assignment break") + a.should_receive(:m).and_return(nil) + a.should_not_receive(:m=) + + c = true + while c + a.m += begin + break if c + c = false + end + end.should be_nil + end + + it "returns value passed to break if interrupted by break" do + while true + break 123 + end.should == 123 + end + + it "returns nil if interrupted by break with no arguments" do + while true + break + end.should == nil + end + + it "skips to end of body with next" do + a = [] + i = 0 + while (i+=1)<5 + next if i==3 + a << i + end + a.should == [1, 2, 4] + end + + it "restarts the current iteration without reevaluating condition with redo" do + a = [] + i = 0 + j = 0 + while (i+=1)<3 + a << i + j+=1 + redo if j<3 + end + a.should == [1, 1, 1, 2] + end +end + +describe "The while modifier" do + it "runs preceding statement while the condition is true" do + i = 0 + i += 1 while i < 3 + i.should == 3 + end + + it "evaluates condition before statement execution" do + a = [] + i = 0 + a << i while (i+=1) < 3 + a.should == [1, 2] + end + + it "does not run preceding statement if the condition is false" do + i = 0 + i += 1 while false + i.should == 0 + end + + it "does not run preceding statement if the condition is empty" do + i = 0 + i += 1 while () + i.should == 0 + end + + it "returns nil if ended when condition became false" do + i = 0 + (i += 1 while i<10).should == nil + end + + it "returns value passed to break if interrupted by break" do + (break 123 while true).should == 123 + end + + it "returns nil if interrupted by break with no arguments" do + (break while true).should == nil + end + + it "skips to end of body with next" do + i = 0 + j = 0 + ((i+=1) == 3 ? next : j+=i) while i <= 10 + j.should == 63 + end + + it "restarts the current iteration without reevaluating condition with redo" do + i = 0 + j = 0 + (i+=1) == 4 ? redo : j+=i while (i+=1) <= 10 + j.should == 34 + end +end + +describe "The while modifier with begin .. end block" do + it "runs block while the expression is true" do + i = 0 + begin + i += 1 + end while i < 3 + + i.should == 3 + end + + it "stops running block if interrupted by break" do + i = 0 + begin + i += 1 + break if i > 5 + end while i < 10 + + i.should == 6 + end + + it "returns value passed to break if interrupted by break" do + (begin; break 123; end while true).should == 123 + end + + it "returns nil if interrupted by break with no arguments" do + (begin; break; end while true).should == nil + end + + it "runs block at least once (even if the expression is false)" do + i = 0 + begin + i += 1 + end while false + + i.should == 1 + end + + it "evaluates condition after block execution" do + a = [] + i = 0 + begin + a << i + end while (i+=1)<5 + a.should == [0, 1, 2, 3, 4] + end + + it "skips to end of body with next" do + a = [] + i = 0 + begin + next if i==3 + a << i + end while (i+=1)<5 + a.should == [0, 1, 2, 4] + end + + it "restarts the current iteration without reevaluating condition with redo" do + a = [] + i = 0 + j = 0 + begin + a << i + j+=1 + redo if j<3 + end while (i+=1)<3 + a.should == [0, 0, 0, 1, 2] + end +end diff --git a/spec/ruby/language/yield_spec.rb b/spec/ruby/language/yield_spec.rb new file mode 100644 index 0000000000..e125cf8e73 --- /dev/null +++ b/spec/ruby/language/yield_spec.rb @@ -0,0 +1,220 @@ +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 +# because the outer Array is a consequence of |*a| but it is necessary to +# clearly distinguish some behaviors. + +describe "The yield call" do + before :each do + @y = YieldSpecs::Yielder.new + end + + describe "taking no arguments" do + it "raises a LocalJumpError when the method is not passed a block" do + -> { @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 + -> { @y.s(1) }.should raise_error(LocalJumpError) + end + end + + describe "yielding to a literal block" do + it "passes an empty Array when the argument is an empty Array" do + @y.s([]) { |*a| a }.should == [[]] + end + + it "passes nil as a value" do + @y.s(nil) { |*a| a }.should == [nil] + end + + it "passes a single value" do + @y.s(1) { |*a| a }.should == [1] + end + + it "passes a single, multi-value Array" do + @y.s([1, 2, 3]) { |*a| a }.should == [[1, 2, 3]] + end + end + + describe "yielding to a lambda" do + it "passes an empty Array when the argument is an empty Array" do + @y.s([], &-> *a { a }).should == [[]] + end + + it "passes nil as a value" do + @y.s(nil, &-> *a { a }).should == [nil] + end + + it "passes a single value" do + @y.s(1, &-> *a { a }).should == [1] + end + + it "passes a single, multi-value Array" do + @y.s([1, 2, 3], &-> *a { a }).should == [[1, 2, 3]] + end + + it "raises an ArgumentError if too few arguments are passed" do + -> { + @y.s(1, &-> 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 + -> { @y.m(1, 2, 3) }.should raise_error(LocalJumpError) + end + + it "passes the arguments to the block" do + @y.m(1, 2, 3) { |*a| a }.should == [1, 2, 3] + end + + it "passes only the first argument if the block takes one parameter" do + @y.m(1, 2, 3) { |a| a }.should == 1 + end + + it "raises an ArgumentError if too many arguments are passed to a lambda" do + -> { + @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 + -> { + @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 + -> { @y.r(0) }.should raise_error(LocalJumpError) + end + + it "passes a single value" do + @y.r(1) { |*a| a }.should == [1] + end + + it "passes no arguments when the argument is an empty Array" do + @y.r([]) { |*a| a }.should == [] + end + + it "passes the value when the argument is an Array containing a single value" do + @y.r([1]) { |*a| a }.should == [1] + end + + it "passes the values of the Array as individual arguments" do + @y.r([1, 2, 3]) { |*a| a }.should == [1, 2, 3] + end + + it "passes the element of a single element Array" do + @y.r([[1, 2]]) { |*a| a }.should == [[1, 2]] + @y.r([nil]) { |*a| a }.should == [nil] + @y.r([[]]) { |*a| a }.should == [[]] + end + + it "passes no values when give nil as an argument" do + @y.r(nil) { |*a| a }.should == [] + end + end + + describe "taking multiple arguments with a splat" do + it "raises a LocalJumpError when the method is not passed a block" do + -> { @y.rs(1, 2, [3, 4]) }.should raise_error(LocalJumpError) + end + + it "passes the arguments to the block" do + @y.rs(1, 2, 3) { |*a| a }.should == [1, 2, 3] + end + + it "does not pass an argument value if the splatted argument is an empty Array" do + @y.rs(1, 2, []) { |*a| a }.should == [1, 2] + end + + it "passes the Array elements as arguments if the splatted argument is a non-empty Array" do + @y.rs(1, 2, [3]) { |*a| a }.should == [1, 2, 3] + @y.rs(1, 2, [nil]) { |*a| a }.should == [1, 2, nil] + @y.rs(1, 2, [[]]) { |*a| a }.should == [1, 2, []] + @y.rs(1, 2, [3, 4, 5]) { |*a| a }.should == [1, 2, 3, 4, 5] + end + + it "does not pass an argument value if the splatted argument is nil" do + @y.rs(1, 2, nil) { |*a| a }.should == [1, 2] + end + end + + describe "taking matching arguments with splats and post args" do + it "raises a LocalJumpError when the method is not passed a block" do + -> { @y.rs(1, 2, [3, 4]) }.should raise_error(LocalJumpError) + end + + it "passes the arguments to the block" do + @y.rs([1, 2], 3, 4) { |(*a, b), c, d| [a, b, c, d] }.should == [[1], 2, 3, 4] + 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 |
