diff options
Diffstat (limited to 'spec/ruby/language')
157 files changed, 24925 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..762a8db0c0 --- /dev/null +++ b/spec/ruby/language/END_spec.rb @@ -0,0 +1,15 @@ +require_relative '../spec_helper' + +describe "The END keyword" do + it "runs only once for multiple calls" do + ruby_exe("10.times { END { puts 'foo' }; } ").should == "foo\n" + end + + it "runs last in a given code unit" do + ruby_exe("END { puts 'bar' }; puts'foo'; ").should == "foo\nbar\n" + end + + it "runs multiple ends in LIFO order" do + ruby_exe("END { puts 'foo' }; END { puts 'bar' }").should == "bar\nfoo\n" + 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..d1d06e3fac --- /dev/null +++ b/spec/ruby/language/alias_spec.rb @@ -0,0 +1,263 @@ +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 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 "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", escape: true).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 +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/block_spec.rb b/spec/ruby/language/block_spec.rb new file mode 100644 index 0000000000..b2a3cc84c4 --- /dev/null +++ b/spec/ruby/language/block_spec.rb @@ -0,0 +1,1006 @@ +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 required arguments when a keyword rest argument is present" do + m([1, 2]) { |a, **k| [a, k] }.should == [1, {}] + end + + ruby_version_is ''..."3.0" do + it "assigns elements to mixed argument types" do + suppress_keyword_warning do + result = m([1, 2, 3, {x: 9}]) { |a, b=5, *c, d, e: 2, **k| [a, b, c, d, e, k] } + result.should == [1, 2, [], 3, 2, {x: 9}] + end + end + + it "assigns symbol keys from a Hash to keyword arguments" do + suppress_keyword_warning do + result = m(["a" => 1, a: 10]) { |a=nil, **b| [a, b] } + result.should == [{"a" => 1}, a: 10] + end + end + + it "assigns symbol keys from a Hash returned by #to_hash to keyword arguments" do + suppress_keyword_warning do + obj = mock("coerce block keyword arguments") + obj.should_receive(:to_hash).and_return({"a" => 1, b: 2}) + + result = m([obj]) { |a=nil, **b| [a, b] } + result.should == [{"a" => 1}, b: 2] + end + end + end + + ruby_version_is "3.0" do + it "assigns elements to mixed argument types" do + result = m([1, 2, 3, {x: 9}]) { |a, b=5, *c, d, e: 2, **k| [a, b, c, d, e, k] } + result.should == [1, 2, [3], {x: 9}, 2, {}] + end + + it "does not treat final Hash as keyword arguments and does not autosplat" do + result = m(["a" => 1, a: 10]) { |a=nil, **b| [a, b] } + result.should == [[{"a" => 1, a: 10}], {}] + end + + it "does not call #to_hash on final argument to get keyword arguments and does not autosplat" do + suppress_keyword_warning do + obj = mock("coerce block keyword arguments") + obj.should_not_receive(:to_hash) + + result = m([obj]) { |a=nil, **b| [a, b] } + result.should == [[obj], {}] + end + end + end + + ruby_version_is ""..."2.7" do + it "calls #to_hash on the argument and uses resulting hash as first argument when optional argument and keyword argument accepted" do + obj = mock("coerce block keyword arguments") + obj.should_receive(:to_hash).and_return({"a" => 1, "b" => 2}) + + result = m([obj]) { |a=nil, **b| [a, b] } + result.should == [{"a" => 1, "b" => 2}, {}] + end + end + + ruby_version_is "2.7"...'3.0' do + it "calls #to_hash on the argument but ignores result when optional argument and keyword argument accepted" do + obj = mock("coerce block keyword arguments") + obj.should_receive(:to_hash).and_return({"a" => 1, "b" => 2}) + + result = m([obj]) { |a=nil, **b| [a, b] } + result.should == [obj, {}] + end + end + + ruby_version_is "3.0" do + it "does not call #to_hash on the argument when optional argument and keyword argument accepted and does not autosplat" do + 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 + + describe "when non-symbol keys are in a keyword arguments Hash" do + ruby_version_is ""..."3.0" do + it "separates non-symbol keys and symbol keys" do + suppress_keyword_warning do + result = m(["a" => 10, b: 2]) { |a=nil, **b| [a, b] } + result.should == [{"a" => 10}, {b: 2}] + end + end + end + ruby_version_is "3.0" do + it "does not separate non-symbol keys and symbol keys and does not autosplat" do + suppress_keyword_warning do + result = m(["a" => 10, b: 2]) { |a=nil, **b| [a, b] } + result.should == [[{"a" => 10, b: 2}], {}] + end + end + end + end + + ruby_version_is ""..."3.0" do + it "does not treat hashes with string keys as keyword arguments" do + result = m(["a" => 10]) { |a = nil, **b| [a, b] } + result.should == [{"a" => 10}, {}] + end + end + + ruby_version_is "3.0" do + it "does not treat hashes with string keys as keyword arguments and does not autosplat" do + result = m(["a" => 10]) { |a = nil, **b| [a, b] } + result.should == [[{"a" => 10}], {}] + end + end + + ruby_version_is ''...'3.0' do + it "calls #to_hash on the last element if keyword arguments are present" do + suppress_keyword_warning do + obj = mock("destructure block keyword arguments") + obj.should_receive(:to_hash).and_return({x: 9}) + + result = m([1, 2, 3, obj]) { |a, *b, c, **k| [a, b, c, k] } + result.should == [1, [2], 3, {x: 9}] + end + end + + it "assigns the last element to a non-keyword argument if #to_hash returns nil" do + suppress_keyword_warning do + obj = mock("destructure block keyword arguments") + obj.should_receive(:to_hash).and_return(nil) + + result = m([1, 2, 3, obj]) { |a, *b, c, **k| [a, b, c, k] } + result.should == [1, [2, 3], obj, {}] + end + end + + it "calls #to_hash on the last element when there are more arguments than parameters" do + suppress_keyword_warning do + x = mock("destructure matching block keyword argument") + x.should_receive(:to_hash).and_return({x: 9}) + + result = m([1, 2, 3, {y: 9}, 4, 5, x]) { |a, b=5, c, **k| [a, b, c, k] } + result.should == [1, 2, 3, {x: 9}] + end + end + + it "raises a TypeError if #to_hash does not return a Hash" do + obj = mock("destructure block keyword arguments") + obj.should_receive(:to_hash).and_return(1) + + -> { m([1, 2, 3, obj]) { |a, *b, c, **k| } }.should raise_error(TypeError) + end + + it "raises the error raised inside #to_hash" do + obj = mock("destructure block keyword arguments") + error = RuntimeError.new("error while converting to a hash") + obj.should_receive(:to_hash).and_raise(error) + + -> { m([1, 2, 3, obj]) { |a, *b, c, **k| } }.should raise_error(error) + end + end + + ruby_version_is '3.0' do + it "does not call #to_hash on the last element if keyword arguments are present" do + obj = mock("destructure block keyword arguments") + obj.should_not_receive(:to_hash) + + 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 + 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 "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 + 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 + eval("@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 + eval('@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 + eval('@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 + eval("lambda { |_,_| }").should be_an_instance_of(Proc) + eval("->(_,_) {}").should be_an_instance_of(Proc) + eval("Proc.new { |_,_| }").should be_an_instance_of(Proc) + 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 ''...'2.7' do + it "warns and uses a nil value when there is an existing local variable with same name" do + a = 1 + -> { + @proc = eval "proc { |a=a| a }" + }.should complain(/circular argument reference/) + @proc.call.should == nil + end + + it "warns and uses a nil value when there is an existing method with same name" do + def a; 1; end + -> { + @proc = eval "proc { |a=a| a }" + }.should complain(/circular argument reference/) + @proc.call.should == nil + end + end + + ruby_version_is '2.7' do + it "raises a SyntaxError if using an existing local with the same name as the argument" do + a = 1 + -> { + @proc = eval "proc { |a=a| a }" + }.should raise_error(SyntaxError) + end + + it "raises a SyntaxError if there is an existing method with the same name as the argument" do + def a; 1; end + -> { + @proc = eval "proc { |a=a| a }" + }.should raise_error(SyntaxError) + end + 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 + 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 diff --git a/spec/ruby/language/break_spec.rb b/spec/ruby/language/break_spec.rb new file mode 100644 index 0000000000..627cb4a071 --- /dev/null +++ b/spec/ruby/language/break_spec.rb @@ -0,0 +1,383 @@ +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 + + +# 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 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..410cb9afb1 --- /dev/null +++ b/spec/ruby/language/case_spec.rb @@ -0,0 +1,436 @@ +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 regexp interpolated within another 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 string interpolated in a 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 "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 +end + +describe "The 'case'-construct with no target expression" do + it "evaluates the body of the first clause when at least one of its condition expressions is true" do + case + when true, false; 'foo' + end.should == 'foo' + end + + it "evaluates the body of the first when clause that is not false/nil" do + case + when false; 'foo' + when 2; 'bar' + when 1 == 1; 'baz' + end.should == 'bar' + + case + when false; 'foo' + when nil; 'foo' + when 1 == 1; 'bar' + end.should == 'bar' + end + + it "evaluates the body of the else clause if all when clauses are false/nil" do + case + when false; 'foo' + when nil; 'foo' + when 1 == 2; 'bar' + else 'baz' + end.should == 'baz' + end + + it "evaluates multiple conditional expressions as a boolean disjunction" do + case + when true, false; 'foo' + else 'bar' + end.should == 'foo' + + case + when false, true; 'foo' + else 'bar' + end.should == 'foo' + end + + it "evaluates true as only 'true' when true is the first clause" do + case 1 + when true; "bad" + when Integer; "good" + 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 + + # 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 +end diff --git a/spec/ruby/language/class_spec.rb b/spec/ruby/language/class_spec.rb new file mode 100644 index 0000000000..83db164e1a --- /dev/null +++ b/spec/ruby/language/class_spec.rb @@ -0,0 +1,350 @@ +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 +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) + 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' + 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 + + ruby_version_is ""..."3.0" do + it "allows accessing the block of the original scope" do + suppress_warning do + ClassSpecs.sclass_with_block { 123 }.should == 123 + end + end + end + + ruby_version_is "3.0" do + it "does not allow accessing the block of the original scope" do + -> { + ClassSpecs.sclass_with_block { 123 } + }.should raise_error(SyntaxError) + end + 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 +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..f98deaa081 --- /dev/null +++ b/spec/ruby/language/class_variable_spec.rb @@ -0,0 +1,116 @@ +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 + +ruby_version_is "3.0" do + 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 +end diff --git a/spec/ruby/language/comment_spec.rb b/spec/ruby/language/comment_spec.rb new file mode 100644 index 0000000000..690e844dc0 --- /dev/null +++ b/spec/ruby/language/comment_spec.rb @@ -0,0 +1,15 @@ +require_relative '../spec_helper' + +describe "The comment" do + ruby_version_is "2.7" do + it "can be placed between fluent dot now" do + code = <<~CODE + 10 + # some comment + .to_s + CODE + + eval(code).should == '10' + end + end +end diff --git a/spec/ruby/language/constants_spec.rb b/spec/ruby/language/constants_spec.rb new file mode 100644 index 0000000000..03f1272401 --- /dev/null +++ b/spec/ruby/language/constants_spec.rb @@ -0,0 +1,720 @@ +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 + 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 + 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 + 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 + end + + it "searches the superclass chain" do + ConstantSpecs::ModuleA::CS_CONST105 = :const105 + ConstantSpecs::ContainerB::ChildB::CS_CONST105.should == :const105 + end + + it "searches Object if no class or module qualifier is given" do + CS_CONST106 = :const106 + CS_CONST106.should == :const106 + end + + it "searches Object if a toplevel qualifier (::X) is given" do + ::CS_CONST107 = :const107 + ::CS_CONST107.should == :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) + 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 + end + + it "evaluates the right hand side before evaluating a constant path" do + mod = Module.new + + mod.module_eval <<-EOC + ConstantSpecsRHS::B = begin + module ConstantSpecsRHS; end + + "hello" + end + EOC + + mod::ConstantSpecsRHS::B.should == 'hello' + 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 + + ruby_version_is "3.0" do + it "uses the module or class #name to craft the error message" do + mod = Module.new do + def self.name + "ModuleName" + end + + def self.inspect + "<unusable info>" + end + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + end + + it "does not search the lexical scope of the caller" do + ConstantSpecs::ClassB::CS_CONST209 = :const209 + + -> { ConstantSpecs::ClassB.const209 }.should raise_error(NameError) + 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 + 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 + 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 + 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) + 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 + 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 + 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 + 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 + 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 diff --git a/spec/ruby/language/def_spec.rb b/spec/ruby/language/def_spec.rb new file mode 100644 index 0000000000..6b0be19d90 --- /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| + e.message.should.start_with? "can't modify frozen module" + } + + -> { + Class.new do + self.freeze + def foo; end + end + }.should raise_error(FrozenError){ |e| + e.message.should.start_with? "can't modify frozen class" + } + 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 ''...'2.7' do + it "warns and uses a nil value when there is an existing local method with same name" do + def bar + 1 + end + -> { + eval "def foo(bar = bar) + bar + end" + }.should complain(/circular argument reference/) + foo.should == nil + foo(2).should == 2 + end + end + + ruby_version_is '2.7' do + it "raises a syntaxError an existing method with the same name as the local variable" do + def bar + 1 + end + -> { + eval "def foo(bar = bar) + bar + end" + }.should raise_error(SyntaxError) + 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 + -> { def obj.foo; end }.should raise_error(FrozenError){ |e| + e.message.should.start_with? "can't modify frozen object" + } + + c = obj.singleton_class + -> { def c.foo; end }.should raise_error(FrozenError){ |e| + e.message.should.start_with? "can't modify frozen Class" + } + + m = Module.new + m.freeze + -> { def m.foo; end }.should raise_error(FrozenError){ |e| + e.message.should.start_with? "can't modify frozen Module" + } + 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 + 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 + 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..38345c3727 --- /dev/null +++ b/spec/ruby/language/defined_spec.rb @@ -0,0 +1,1157 @@ +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 + 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 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 +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 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?(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 + + 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 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 "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..8e4183cbcc --- /dev/null +++ b/spec/ruby/language/delegation_spec.rb @@ -0,0 +1,68 @@ +require_relative '../spec_helper' +require_relative 'fixtures/delegation' + +ruby_version_is "2.7" do + describe "delegation with def(...)" do + it "delegates rest and kwargs" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate(...) + target(...) + end + RUBY + + a.new.delegate(1, b: 2).should == [[1], {b: 2}] + end + + it "delegates block" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate_block(...) + target_block(...) + end + RUBY + + a.new.delegate_block(1, b: 2) { |x| x }.should == [{b: 2}, [1]] + end + + it "parses as open endless Range when brackets are omitted" do + a = Class.new(DelegationSpecs::Target) + suppress_warning do + a.class_eval(<<-RUBY) + def delegate(...) + target ... + end + RUBY + end + + a.new.delegate(1, b: 2).should == Range.new([[], {}], nil, true) + end + end +end + +ruby_version_is "2.7.3" do + describe "delegation with def(x, ...)" do + it "delegates rest and kwargs" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate(x, ...) + target(...) + end + RUBY + + a.new.delegate(0, 1, b: 2).should == [[1], {b: 2}] + end + + it "delegates block" do + a = Class.new(DelegationSpecs::Target) + a.class_eval(<<-RUBY) + def delegate_block(x, ...) + target_block(...) + end + RUBY + + a.new.delegate_block(0, 1, b: 2) { |x| x }.should == [{b: 2}, [1]] + end + + end +end diff --git a/spec/ruby/language/encoding_spec.rb b/spec/ruby/language/encoding_spec.rb new file mode 100644 index 0000000000..5430c9cb98 --- /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__".force_encoding("US-ASCII")).should == Encoding::US_ASCII + eval("__ENCODING__".force_encoding("BINARY")).should == Encoding::BINARY + end + + it "is the encoding specified by a magic comment inside an eval" do + code = "# encoding: BINARY\n__ENCODING__".force_encoding("US-ASCII") + eval(code).should == Encoding::BINARY + + code = "# encoding: us-ascii\n__ENCODING__".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..e893904bcb --- /dev/null +++ b/spec/ruby/language/ensure_spec.rb @@ -0,0 +1,331 @@ +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 +end diff --git a/spec/ruby/language/execution_spec.rb b/spec/ruby/language/execution_spec.rb new file mode 100644 index 0000000000..4e0310946d --- /dev/null +++ b/spec/ruby/language/execution_spec.rb @@ -0,0 +1,15 @@ +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 +end + +describe "%x" do + it "is the same as ``" do + ip = 'world' + %x(echo disc #{ip}).should == "disc world\n" + end +end diff --git a/spec/ruby/language/file_spec.rb b/spec/ruby/language/file_spec.rb new file mode 100644 index 0000000000..729dee1008 --- /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 + + it "equals (eval) inside an eval" do + eval("__FILE__").should == "(eval)" + end +end + +describe "The __FILE__ pseudo-variable" do + it_behaves_like :language___FILE__, :require, CodeLoadingSpecs::Method.new +end + +describe "The __FILE__ pseudo-variable" do + it_behaves_like :language___FILE__, :require, Kernel +end + +describe "The __FILE__ pseudo-variable" do + it_behaves_like :language___FILE__, :load, CodeLoadingSpecs::Method.new +end + +describe "The __FILE__ pseudo-variable" do + 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/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..8b6004c19f --- /dev/null +++ b/spec/ruby/language/fixtures/defined.rb @@ -0,0 +1,303 @@ +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 + + 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 + + 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..527d928390 --- /dev/null +++ b/spec/ruby/language/fixtures/delegation.rb @@ -0,0 +1,11 @@ +module DelegationSpecs + class Target + def target(*args, **kwargs) + [args, kwargs] + 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 Binary files differnew file mode 100644 index 0000000000..d0558a2251 --- /dev/null +++ b/spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb diff --git a/spec/ruby/language/fixtures/freeze_magic_comment_required_no_comment.rb b/spec/ruby/language/fixtures/freeze_magic_comment_required_no_comment.rb 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..a4d655ad02 --- /dev/null +++ b/spec/ruby/language/fixtures/freeze_magic_comment_two_literals.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +p "abc".object_id == "abc".object_id 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..33d323846e --- /dev/null +++ b/spec/ruby/language/fixtures/module.rb @@ -0,0 +1,24 @@ +module ModuleSpecs + module Modules + class Klass + end + + A = "Module" + B = 1 + C = nil + D = true + E = false + end + + module Anonymous + end + + module IncludedInObject + module IncludedModuleSpecs + end + end +end + +class Object + include ModuleSpecs::IncludedInObject +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..96f73cea3f --- /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_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..918241e171 --- /dev/null +++ b/spec/ruby/language/fixtures/send.rb @@ -0,0 +1,141 @@ +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 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..218f1e4970 --- /dev/null +++ b/spec/ruby/language/fixtures/super.rb @@ -0,0 +1,742 @@ +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 m_modified(*args) + args + end + end + + class B < A + def m(_, _) + super + end + + def m_modified(_, _) + _ = 14 + super + 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..07265dbb2b --- /dev/null +++ b/spec/ruby/language/fixtures/variables.rb @@ -0,0 +1,85 @@ +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 +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..0ad5ea88af --- /dev/null +++ b/spec/ruby/language/for_spec.rb @@ -0,0 +1,182 @@ +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 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 + + # 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..2f8b97199a --- /dev/null +++ b/spec/ruby/language/hash_spec.rb @@ -0,0 +1,267 @@ +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 + + ruby_version_is "3.1" do + it "checks duplicated float keys on initialization" do + -> { + @h = eval "{1.0 => :bar, 1.0 => :foo}" + }.should complain(/key 1.0 is duplicated|duplicated key/) + @h.keys.size.should == 1 + @h.should == {1.0 => :foo} + end + end + + it "accepts a hanging comma" do + 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 + eval("{:a==>1}").should == {:"a=" => 1} + eval("{:a= =>1}").should == {:"a=" => 1} + eval("{:a= => 1}").should == {:"a=" => 1} + end + + it "with '==>' in the middle raises SyntaxError" 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} + eval('{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 + + it "expands an '**{}' 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} + 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 + + ruby_version_is ""..."2.7" do + it "raises a TypeError if any splatted elements keys are not symbols" do + h = {1 => 2, b: 3} + -> { {a: 1, **h} }.should raise_error(TypeError) + end + end + + ruby_version_is "2.7" do + it "allows splatted elements keys that are not symbols" do + h = {1 => 2, b: 3} + {a: 1, **h}.should == {a: 1, 1 => 2, b: 3} + end + 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 +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_version_is ""..."3.0" do + it "makes a caller-side 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 + + ruby_version_is "3.0" do + it "does not copy when calling a method taking a positional Hash" do + def m(h) + h.delete(:one); h + end + + h = { one: 1, two: 2 } + m(**h).should == { two: 2 } + m(**h).should.equal?(h) + h.should == { two: 2 } + end + end + + ruby_version_is "3.1" do + describe "hash with omitted value" do + it "accepts short notation 'key' for 'key: value' syntax" do + a, b, c = 1, 2, 3 + h = eval('{a:}') + {a: 1}.should == h + h = eval('{a:, b:, c:}') + {a: 1, b: 2, c: 3}.should == h + end + + it "ignores hanging comma on short notation" do + a, b, c = 1, 2, 3 + h = eval('{a:, b:, c:,}') + {a: 1, b: 2, c: 3}.should == h + end + + it "accepts mixed syntax" do + a, e = 1, 5 + h = eval('{a:, b: 2, "c" => 3, :d => 4, e:}') + eval('{a: 1, :b => 2, "c" => 3, "d": 4, e: 5}').should == h + end + + it "works with methods and local vars" do + a = Class.new + a.class_eval(<<-RUBY) + def bar + "baz" + end + + def foo(val) + {bar:, val:} + end + RUBY + + a.new.foo(1).should == {bar: "baz", val: 1} + end + end + end +end diff --git a/spec/ruby/language/heredoc_spec.rb b/spec/ruby/language/heredoc_spec.rb new file mode 100644 index 0000000000..61a27ad8e0 --- /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 + + ruby_version_is "2.7" do + it 'raises SyntaxError if quoted HEREDOC identifier is ending not on same line' do + -> { + eval %{<<"HERE\n"\nraises syntax error\nHERE} + }.should raise_error(SyntaxError) + end + end + + ruby_version_is ""..."2.7" do + it 'prints a warning if quoted HEREDOC identifier is ending not on same line' do + -> { + eval %{<<"HERE\n"\nit warns\nHERE} + }.should complain(/here document identifier ends with a newline/) + end + 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 +end diff --git a/spec/ruby/language/if_spec.rb b/spec/ruby/language/if_spec.rb new file mode 100644 index 0000000000..d1d95c1607 --- /dev/null +++ b/spec/ruby/language/if_spec.rb @@ -0,0 +1,371 @@ +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 + 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/lambda_spec.rb b/spec/ruby/language/lambda_spec.rb new file mode 100644 index 0000000000..6393fb5c47 --- /dev/null +++ b/spec/ruby/language/lambda_spec.rb @@ -0,0 +1,654 @@ +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 + + ruby_version_is ''...'3.0' do + evaluate <<-ruby do + @a = -> (*, **k) { k } + ruby + + @a.().should == {} + @a.(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} + + suppress_keyword_warning do + h = mock("keyword splat") + h.should_receive(:to_hash).and_return({a: 1}) + @a.(h).should == {a: 1} + end + end + end + + ruby_version_is '3.0' do + evaluate <<-ruby do + @a = -> (*, **k) { k } + ruby + + @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 + 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 ''...'2.7' do + it "warns and uses a nil value when there is an existing local variable with same name" do + a = 1 + -> { + @proc = eval "-> (a=a) { a }" + }.should complain(/circular argument reference/) + @proc.call.should == nil + end + + it "warns and uses a nil value when there is an existing method with same name" do + def a; 1; end + -> { + @proc = eval "-> (a=a) { a }" + }.should complain(/circular argument reference/) + @proc.call.should == nil + end + end + + ruby_version_is '2.7' do + it "raises a SyntaxError if using an existing local with the same name as the argument" do + a = 1 + -> { + @proc = eval "-> (a=a) { a }" + }.should raise_error(SyntaxError) + end + + it "raises a SyntaxError if there is an existing method with the same name as the argument" do + def a; 1; end + -> { + @proc = eval "-> (a=a) { a }" + }.should raise_error(SyntaxError) + end + 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 + + ruby_version_is ""..."2.7" do + it "can be created" do + implicit_lambda = nil + -> { + implicit_lambda = meth { 1 } + }.should complain(/tried to create Proc object without a block/) + + implicit_lambda.lambda?.should be_true + implicit_lambda.call.should == 1 + end + end + + ruby_version_is "2.7" do + it "raises ArgumentError" do + implicit_lambda = nil + suppress_warning do + -> { + meth { 1 } + }.should raise_error(ArgumentError, /tried to create Proc object without a block/) + end + 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 + + ruby_version_is ''...'3.0' do + evaluate <<-ruby do + @a = lambda { |*, **k| k } + ruby + + @a.().should == {} + @a.(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} + + suppress_keyword_warning do + h = mock("keyword splat") + h.should_receive(:to_hash).and_return({a: 1}) + @a.(h).should == {a: 1} + end + end + end + + ruby_version_is '3.0' do + evaluate <<-ruby do + @a = lambda { |*, **k| k } + ruby + + @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 + 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..f2bf3a08e5 --- /dev/null +++ b/spec/ruby/language/magic_comment_spec.rb @@ -0,0 +1,92 @@ +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 + it_behaves_like :magic_comments, :locale, -> 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..0a5bb99d0b --- /dev/null +++ b/spec/ruby/language/method_spec.rb @@ -0,0 +1,2018 @@ +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: 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 + + ruby_version_is "2.7" do + evaluate <<-ruby do + def m(**k); k end; + ruby + + m("a" => 1).should == { "a" => 1 } + end + 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 + + ruby_version_is ""..."3.0" do + evaluate <<-ruby do + def m(a, b: 1) [a, b] end + ruby + + m(2).should == [2, 1] + m(1, b: 2).should == [1, 2] + suppress_keyword_warning do + m("a" => 1, b: 2).should == [{"a" => 1, b: 2}, 1] + end + end + + evaluate <<-ruby do + def m(a, **) a end + ruby + + m(1).should == 1 + m(1, a: 2, b: 3).should == 1 + suppress_keyword_warning do + m("a" => 1, b: 2).should == {"a" => 1, b: 2} + end + end + + evaluate <<-ruby do + def m(a, **k) [a, k] end + ruby + + m(1).should == [1, {}] + m(1, a: 2, b: 3).should == [1, {a: 2, b: 3}] + suppress_keyword_warning do + m("a" => 1, b: 2).should == [{"a" => 1, b: 2}, {}] + end + end + end + + ruby_version_is "3.0" do + evaluate <<-ruby do + def m(a, b: 1) [a, b] end + ruby + + m(2).should == [2, 1] + m(1, b: 2).should == [1, 2] + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + def m(a, **) a end + ruby + + 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 + 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 + + ruby_version_is ""..."3.0" do + evaluate <<-ruby do + def m(a=1, b:) [a, b] end + ruby + + m(b: 2).should == [1, 2] + m(2, b: 1).should == [2, 1] + suppress_keyword_warning do + m("a" => 1, b: 2).should == [{"a" => 1}, 2] + end + end + + evaluate <<-ruby do + def m(a=1, b: 2) [a, b] end + ruby + + m().should == [1, 2] + m(2).should == [2, 2] + m(b: 3).should == [1, 3] + suppress_keyword_warning do + m("a" => 1, b: 2).should == [{"a" => 1}, 2] + end + end + end + + ruby_version_is "3.0" do + evaluate <<-ruby do + def m(a=1, b:) [a, b] end + ruby + + m(b: 2).should == [1, 2] + m(2, b: 1).should == [2, 1] + -> { m("a" => 1, b: 2) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + def m(a=1, b: 2) [a, b] end + ruby + + 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 + end + + ruby_version_is ""..."2.7" do + 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 == {"a" => 1} + end + end + + ruby_version_is "2.7" do + 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 + 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 + + ruby_version_is ""..."2.7" do + evaluate <<-ruby do + def m(*, a:) a end + ruby + + m(a: 1).should == 1 + m(1, 2, a: 3).should == 3 + suppress_keyword_warning do + m("a" => 1, a: 2).should == 2 + end + end + + evaluate <<-ruby do + def m(*a, b:) [a, b] end + ruby + + m(b: 1).should == [[], 1] + m(1, 2, b: 3).should == [[1, 2], 3] + suppress_keyword_warning do + m("a" => 1, b: 2).should == [[{"a" => 1}], 2] + end + end + + evaluate <<-ruby do + def m(*, a: 1) a end + ruby + + m().should == 1 + m(1, 2).should == 1 + m(a: 2).should == 2 + m(1, a: 2).should == 2 + suppress_keyword_warning do + m("a" => 1, a: 2).should == 2 + end + end + + evaluate <<-ruby do + def m(*a, b: 1) [a, b] end + ruby + + m().should == [[], 1] + m(1, 2, 3, b: 4).should == [[1, 2, 3], 4] + suppress_keyword_warning do + m("a" => 1, b: 2).should == [[{"a" => 1}], 2] + end + + a = mock("splat") + a.should_not_receive(:to_ary) + m(*a).should == [[a], 1] + end + + evaluate <<-ruby do + def m(*, **) end + ruby + + m().should be_nil + m(a: 1, b: 2).should be_nil + m(1, 2, 3, a: 4, b: 5).should be_nil + + h = mock("keyword splat") + h.should_receive(:to_hash).and_return({a: 1}) + suppress_keyword_warning do + m(h).should be_nil + end + + h = mock("keyword splat") + error = RuntimeError.new("error while converting to a hash") + h.should_receive(:to_hash).and_raise(error) + -> { m(h) }.should raise_error(error) + end + + evaluate <<-ruby do + def m(*a, **) a end + ruby + + m().should == [] + m(1, 2, 3, a: 4, b: 5).should == [1, 2, 3] + m("a" => 1, a: 1).should == [{"a" => 1}] + m(1, **{a: 2}).should == [1] + + h = mock("keyword splat") + h.should_receive(:to_hash) + -> { m(**h) }.should raise_error(TypeError) + end + + evaluate <<-ruby do + def m(*, **k) k end + ruby + + m().should == {} + m(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} + m("a" => 1, a: 1).should == {a: 1} + + h = mock("keyword splat") + h.should_receive(:to_hash).and_return({a: 1}) + m(h).should == {a: 1} + end + + evaluate <<-ruby do + def m(a = nil, **k) [a, k] end + ruby + + m().should == [nil, {}] + m("a" => 1).should == [{"a" => 1}, {}] + m(a: 1).should == [nil, {a: 1}] + m("a" => 1, a: 1).should == [{"a" => 1}, {a: 1}] + m({ "a" => 1 }, a: 1).should == [{"a" => 1}, {a: 1}] + m({a: 1}, {}).should == [{a: 1}, {}] + + h = {"a" => 1, b: 2} + m(h).should == [{"a" => 1}, {b: 2}] + h.should == {"a" => 1, b: 2} + + h = {"a" => 1} + m(h).first.should == h + + h = {} + r = m(h) + r.first.should be_nil + r.last.should == {} + + hh = {} + h = mock("keyword splat empty hash") + h.should_receive(:to_hash).and_return(hh) + r = m(h) + r.first.should be_nil + r.last.should == {} + + h = mock("keyword splat") + h.should_receive(:to_hash).and_return({"a" => 1, a: 2}) + m(h).should == [{"a" => 1}, {a: 2}] + end + + evaluate <<-ruby do + def m(*a, **k) [a, k] end + ruby + + m().should == [[], {}] + m(1).should == [[1], {}] + m(a: 1, b: 2).should == [[], {a: 1, b: 2}] + m(1, 2, 3, a: 2).should == [[1, 2, 3], {a: 2}] + + m("a" => 1).should == [[{"a" => 1}], {}] + m(a: 1).should == [[], {a: 1}] + m("a" => 1, a: 1).should == [[{"a" => 1}], {a: 1}] + m({ "a" => 1 }, a: 1).should == [[{"a" => 1}], {a: 1}] + m({a: 1}, {}).should == [[{a: 1}], {}] + m({a: 1}, {"a" => 1}).should == [[{a: 1}, {"a" => 1}], {}] + + bo = BasicObject.new + def bo.to_a; [1, 2, 3]; end + def bo.to_hash; {:b => 2, :c => 3}; end + + m(*bo, **bo).should == [[1, 2, 3], {:b => 2, :c => 3}] + end + end + + ruby_version_is "2.7"...'3.0' do + evaluate <<-ruby do + def m(*, a:) a end + ruby + + m(a: 1).should == 1 + m(1, 2, a: 3).should == 3 + suppress_keyword_warning do + m("a" => 1, a: 2).should == 2 + end + end + + evaluate <<-ruby do + def m(*a, b:) [a, b] end + ruby + + m(b: 1).should == [[], 1] + m(1, 2, b: 3).should == [[1, 2], 3] + suppress_keyword_warning do + m("a" => 1, b: 2).should == [[{"a" => 1}], 2] + end + end + + evaluate <<-ruby do + def m(*, a: 1) a end + ruby + + m().should == 1 + m(1, 2).should == 1 + m(a: 2).should == 2 + m(1, a: 2).should == 2 + suppress_keyword_warning do + m("a" => 1, a: 2).should == 2 + end + end + + evaluate <<-ruby do + def m(*a, b: 1) [a, b] end + ruby + + m().should == [[], 1] + m(1, 2, 3, b: 4).should == [[1, 2, 3], 4] + suppress_keyword_warning do + m("a" => 1, b: 2).should == [[{"a" => 1}], 2] + end + + a = mock("splat") + a.should_not_receive(:to_ary) + m(*a).should == [[a], 1] + end + + evaluate <<-ruby do + def m(*a, **) a end + ruby + + m().should == [] + m(1, 2, 3, a: 4, b: 5).should == [1, 2, 3] + m("a" => 1, a: 1).should == [] + m(1, **{a: 2}).should == [1] + + h = mock("keyword splat") + h.should_receive(:to_hash) + -> { m(**h) }.should raise_error(TypeError) + end + + evaluate <<-ruby do + def m(*, **k) k end + ruby + + m().should == {} + m(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} + m("a" => 1, a: 1).should == {"a" => 1, a: 1} + + h = mock("keyword splat") + h.should_receive(:to_hash).and_return({a: 1}) + suppress_warning do + m(h).should == {a: 1} + end + end + + evaluate <<-ruby do + def m(a = nil, **k) [a, k] end + ruby + + m().should == [nil, {}] + m("a" => 1).should == [nil, {"a" => 1}] + m(a: 1).should == [nil, {a: 1}] + m("a" => 1, a: 1).should == [nil, {"a" => 1, a: 1}] + m({ "a" => 1 }, a: 1).should == [{"a" => 1}, {a: 1}] + suppress_warning do + m({a: 1}, {}).should == [{a: 1}, {}] + + h = {"a" => 1, b: 2} + m(h).should == [{"a" => 1}, {b: 2}] + h.should == {"a" => 1, b: 2} + + h = {"a" => 1} + m(h).first.should == h + + h = {} + r = m(h) + r.first.should be_nil + r.last.should == {} + + hh = {} + h = mock("keyword splat empty hash") + h.should_receive(:to_hash).and_return(hh) + r = m(h) + r.first.should be_nil + r.last.should == {} + + h = mock("keyword splat") + h.should_receive(:to_hash).and_return({"a" => 1, a: 2}) + m(h).should == [{"a" => 1}, {a: 2}] + end + end + + evaluate <<-ruby do + def m(*a, **k) [a, k] end + ruby + + m().should == [[], {}] + m(1).should == [[1], {}] + m(a: 1, b: 2).should == [[], {a: 1, b: 2}] + m(1, 2, 3, a: 2).should == [[1, 2, 3], {a: 2}] + + m("a" => 1).should == [[], {"a" => 1}] + m(a: 1).should == [[], {a: 1}] + m("a" => 1, a: 1).should == [[], {"a" => 1, a: 1}] + m({ "a" => 1 }, a: 1).should == [[{"a" => 1}], {a: 1}] + suppress_warning do + m({a: 1}, {}).should == [[{a: 1}], {}] + end + m({a: 1}, {"a" => 1}).should == [[{a: 1}, {"a" => 1}], {}] + + bo = BasicObject.new + def bo.to_a; [1, 2, 3]; end + def bo.to_hash; {:b => 2, :c => 3}; end + + m(*bo, **bo).should == [[1, 2, 3], {:b => 2, :c => 3}] + end + + evaluate <<-ruby do + def m(*, a:) a end + ruby + + m(a: 1).should == 1 + m(1, 2, a: 3).should == 3 + suppress_keyword_warning do + m("a" => 1, a: 2).should == 2 + end + end + + evaluate <<-ruby do + def m(*a, b:) [a, b] end + ruby + + m(b: 1).should == [[], 1] + m(1, 2, b: 3).should == [[1, 2], 3] + suppress_keyword_warning do + m("a" => 1, b: 2).should == [[{"a" => 1}], 2] + end + end + + evaluate <<-ruby do + def m(*, a: 1) a end + ruby + + m().should == 1 + m(1, 2).should == 1 + m(a: 2).should == 2 + m(1, a: 2).should == 2 + suppress_keyword_warning do + m("a" => 1, a: 2).should == 2 + end + end + + evaluate <<-ruby do + def m(*a, b: 1) [a, b] end + ruby + + m().should == [[], 1] + m(1, 2, 3, b: 4).should == [[1, 2, 3], 4] + suppress_keyword_warning do + m("a" => 1, b: 2).should == [[{"a" => 1}], 2] + end + + a = mock("splat") + a.should_not_receive(:to_ary) + m(*a).should == [[a], 1] + end + + evaluate <<-ruby do + def m(*a, **) a end + ruby + + m().should == [] + m(1, 2, 3, a: 4, b: 5).should == [1, 2, 3] + m("a" => 1, a: 1).should == [] + m(1, **{a: 2}).should == [1] + + h = mock("keyword splat") + h.should_receive(:to_hash) + -> { m(**h) }.should raise_error(TypeError) + end + + evaluate <<-ruby do + def m(*, **k) k end + ruby + + m().should == {} + m(1, 2, 3, a: 4, b: 5).should == {a: 4, b: 5} + m("a" => 1, a: 1).should == {"a" => 1, a: 1} + + h = mock("keyword splat") + h.should_receive(:to_hash).and_return({a: 1}) + suppress_keyword_warning do + m(h).should == {a: 1} + end + end + + evaluate <<-ruby do + def m(a = nil, **k) [a, k] end + ruby + + m().should == [nil, {}] + m("a" => 1).should == [nil, {"a" => 1}] + m(a: 1).should == [nil, {a: 1}] + m("a" => 1, a: 1).should == [nil, {"a" => 1, a: 1}] + m({ "a" => 1 }, a: 1).should == [{"a" => 1}, {a: 1}] + suppress_keyword_warning do + m({a: 1}, {}).should == [{a: 1}, {}] + end + + h = {"a" => 1, b: 2} + suppress_keyword_warning do + m(h).should == [{"a" => 1}, {b: 2}] + end + h.should == {"a" => 1, b: 2} + + h = {"a" => 1} + m(h).first.should == h + + h = {} + suppress_keyword_warning do + m(h).should == [nil, {}] + end + + hh = {} + h = mock("keyword splat empty hash") + h.should_receive(:to_hash).and_return({a: 1}) + suppress_keyword_warning do + m(h).should == [nil, {a: 1}] + end + + h = mock("keyword splat") + h.should_receive(:to_hash).and_return({"a" => 1}) + m(h).should == [h, {}] + end + + evaluate <<-ruby do + def m(*a, **k) [a, k] end + ruby + + m().should == [[], {}] + m(1).should == [[1], {}] + m(a: 1, b: 2).should == [[], {a: 1, b: 2}] + m(1, 2, 3, a: 2).should == [[1, 2, 3], {a: 2}] + + m("a" => 1).should == [[], {"a" => 1}] + m(a: 1).should == [[], {a: 1}] + m("a" => 1, a: 1).should == [[], {"a" => 1, a: 1}] + m({ "a" => 1 }, a: 1).should == [[{"a" => 1}], {a: 1}] + suppress_keyword_warning do + m({a: 1}, {}).should == [[{a: 1}], {}] + end + m({a: 1}, {"a" => 1}).should == [[{a: 1}, {"a" => 1}], {}] + + bo = BasicObject.new + def bo.to_a; [1, 2, 3]; end + def bo.to_hash; {:b => 2, :c => 3}; end + + m(*bo, **bo).should == [[1, 2, 3], {:b => 2, :c => 3}] + end + end + + evaluate <<-ruby do + def m(*, &b) b end + ruby + + m().should be_nil + m(1, 2, 3, 4).should be_nil + m(&(l = ->{})).should equal(l) + 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 + + ruby_version_is ''...'2.7' do + evaluate <<-ruby do + def m(a:, **) a end + ruby + + m(a: 1).should == 1 + m(a: 1, b: 2).should == 1 + -> { m("a" => 1, a: 1, b: 2) }.should raise_error(ArgumentError) + end + + evaluate <<-ruby do + def m(a:, **k) [a, k] end + ruby + + m(a: 1).should == [1, {}] + m(a: 1, b: 2, c: 3).should == [1, {b: 2, c: 3}] + -> { m("a" => 1, a: 1, b: 2) }.should raise_error(ArgumentError) + end + end + + ruby_version_is '2.7' do + evaluate <<-ruby do + def m(a:, **) a end + ruby + + m(a: 1).should == 1 + m(a: 1, b: 2).should == 1 + m("a" => 1, a: 1, b: 2).should == 1 + end + + evaluate <<-ruby do + def m(a:, **k) [a, k] end + ruby + + 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 + 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 + + ruby_version_is "2.7" do + evaluate <<-ruby do + def m(a, **nil); a end; + ruby + + m({a: 1}).should == {a: 1} + m({"a" => 1}).should == {"a" => 1} + + -> { m(a: 1) }.should raise_error(ArgumentError) + -> { m(**{a: 1}) }.should raise_error(ArgumentError) + -> { m("a" => 1) }.should raise_error(ArgumentError) + end + end + + ruby_version_is ''...'3.0' do + evaluate <<-ruby do + def m(a, b = nil, c = nil, d, e: nil, **f) + [a, b, c, d, e, f] + end + ruby + + result = m(1, 2) + result.should == [1, nil, nil, 2, nil, {}] + + suppress_warning do + result = m(1, 2, {foo: :bar}) + result.should == [1, nil, nil, 2, nil, {foo: :bar}] + end + + result = m(1, {foo: :bar}) + result.should == [1, nil, nil, {foo: :bar}, nil, {}] + end + end + + ruby_version_is '3.0' do + evaluate <<-ruby do + def m(a, b = nil, c = nil, d, e: nil, **f) + [a, b, c, d, e, f] + end + ruby + + result = m(1, 2) + result.should == [1, nil, nil, 2, nil, {}] + + result = m(1, 2, {foo: :bar}) + result.should == [1, 2, nil, {foo: :bar}, nil, {}] + + result = m(1, {foo: :bar}) + result.should == [1, nil, nil, {foo: :bar}, nil, {}] + end + end + end + + ruby_version_is '2.7' do + context 'when passing an empty keyword splat to a method that does not accept keywords' do + evaluate <<-ruby do + def m(*a); a; end + ruby + + h = {} + m(**h).should == [] + end + end + end + + ruby_version_is '2.7'...'3.0' do + context 'when passing an empty keyword splat to a method that does not accept keywords' do + evaluate <<-ruby do + def m(a); a; end + ruby + h = {} + + -> do + m(**h).should == {} + end.should complain(/warning: Passing the keyword argument as the last hash parameter is deprecated/) + end + end + end + + ruby_version_is ''...'3.0' do + context "assigns keyword arguments from a passed Hash without modifying it" do + evaluate <<-ruby do + def m(a: nil); a; end + ruby + + options = {a: 1}.freeze + -> do + suppress_warning do + m(options).should == 1 + end + end.should_not raise_error + options.should == {a: 1} + end + end + end + + ruby_version_is '3.0' do + context 'when passing an empty keyword splat to a method that does not accept keywords' do + evaluate <<-ruby do + def m(a); a; end + ruby + h = {} + + -> do + m(**h).should == {} + end.should raise_error(ArgumentError) + end + end + + context "raises ArgumentError if passing hash as keyword arguments" do + evaluate <<-ruby do + def m(a: nil); a; end + ruby + + options = {a: 1}.freeze + -> do + m(options) + end.should raise_error(ArgumentError) + end + 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 + +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 provided" do + it "assigns it" do + args = m (1 == 1 ? true : false) + args.should == [true] + end + end + + context "when 2+ arguments provided" 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 + +ruby_version_is "3.0" do + describe "An endless method definition" do + context "without arguments" do + evaluate <<-ruby do + def m() = 42 + ruby + + m.should == 42 + 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 + + 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 +end + +ruby_version_is "3.1" do + describe "kwarg with omitted value in a method call" do + context "accepts short notation 'kwarg' in method call" do + evaluate <<-ruby do + def call(*args, **kwargs) = [args, kwargs] + ruby + + a, b, c = 1, 2, 3 + arr, h = eval('call a:') + h.should == {a: 1} + arr.should == [] + + arr, h = eval('call(a:, b:, c:)') + h.should == {a: 1, b: 2, c: 3} + arr.should == [] + + arr, h = eval('call(a:, b: 10, c:)') + h.should == {a: 1, b: 10, c: 3} + arr.should == [] + end + end + + context "with methods and local variables" do + evaluate <<-ruby do + def call(*args, **kwargs) = [args, kwargs] + + def bar + "baz" + end + + def foo(val) + call bar:, val: + end + ruby + + foo(1).should == [[], {bar: "baz", val: 1}] + end + end + end +end diff --git a/spec/ruby/language/module_spec.rb b/spec/ruby/language/module_spec.rb new file mode 100644 index 0000000000..1a5fcaf46f --- /dev/null +++ b/spec/ruby/language/module_spec.rb @@ -0,0 +1,101 @@ +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 + end + + it "reopens a module included in Object" do + module IncludedModuleSpecs; Reopened = true; end + ModuleSpecs::IncludedInObject::IncludedModuleSpecs::Reopened.should be_true + 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" + end + + ruby_version_is ""..."3.0" do + it "does not set the name of a module scoped by an anonymous module" do + a, b = Module.new, Module.new + a::B = b + b.name.should be_nil + end + end + + ruby_version_is "3.0" do + it "sets the name of a module scoped by an anonymous module" do + a, b = Module.new, Module.new + a::B = b + b.name.should.end_with? '::B' + end + 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" + 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..838822b2d6 --- /dev/null +++ b/spec/ruby/language/numbered_parameters_spec.rb @@ -0,0 +1,120 @@ +require_relative '../spec_helper' + +ruby_version_is "2.7" do + describe "Numbered parameters" do + it "provides default parameters _1, _2, ... in a block" do + -> { _1 }.call("a").should == "a" + proc { _1 }.call("a").should == "a" + lambda { _1 }.call("a").should == "a" + ["a"].map { _1 }.should == ["a"] + end + + 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 + + ruby_version_is '2.7'...'3.0' do + it "can be overwritten with local variable" do + suppress_warning do + eval <<~CODE + _1 = 0 + proc { _1 }.call("a").should == 0 + CODE + end + end + + it "warns when numbered parameter is overwritten with local variable" do + -> { + eval("_1 = 0") + }.should complain(/warning: `_1' is reserved for numbered parameter; consider another name/) + end + end + + ruby_version_is '3.0' do + it "cannot be overwritten with local variable" do + -> { + eval <<~CODE + _1 = 0 + proc { _1 }.call("a").should == 0 + CODE + }.should raise_error(SyntaxError, /_1 is reserved for numbered parameter/) + end + + it "errors when numbered parameter is overwritten with local variable" do + -> { + eval("_1 = 0") + }.should raise_error(SyntaxError, /_1 is reserved for numbered parameter/) + end + end + + it "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 + ruby_version_is '2.7'...'3.0' do + it "warns" do + -> { eval("proc { _1 = 0 }") }.should complain(/warning: `_1' is reserved for numbered parameter; consider another name/) + end + end + + ruby_version_is '3.0' do + it "raises SyntaxError" do + -> { eval("proc { _1 = 0 }") }.should raise_error(SyntaxError, /_1 is reserved for numbered parameter/) + end + 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 "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 +end diff --git a/spec/ruby/language/numbers_spec.rb b/spec/ruby/language/numbers_spec.rb new file mode 100644 index 0000000000..2d8e19c40a --- /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 an 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..217dcab74b --- /dev/null +++ b/spec/ruby/language/optional_assignments_spec.rb @@ -0,0 +1,458 @@ +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 a 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 '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 'works when writer is private' do + klass = Class.new do + def t + self.b = false + (self.b ||= 10).should == 10 + (self.b ||= 20).should == 10 + end + + def b + @b + end + + def b=(x) + @b = x + :v + end + + private :b= + end + + klass.new.t + 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 a single variable' 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 + 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] ||= 12).should == 12 + end + + it 'returns the assigned value, not the result of the []= method with +=' do + @b[:k] = 17 + (@b[:k] += 12).should == 29 + 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 (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 + end + end + + describe "with &&=" do + it "re-assigns a scoped constant if already true" do + module ConstantSpecs + OpAssignTrue = true + end + suppress_warning do + ConstantSpecs::OpAssignTrue &&= 1 + end + ConstantSpecs::OpAssignTrue.should == 1 + ConstantSpecs.send :remove_const, :OpAssignTrue + end + + it "leaves scoped constant if not true" do + module ConstantSpecs + OpAssignFalse = false + end + ConstantSpecs::OpAssignFalse &&= 1 + ConstantSpecs::OpAssignFalse.should == false + ConstantSpecs.send :remove_const, :OpAssignFalse + end + end +end 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..e4abae9412 --- /dev/null +++ b/spec/ruby/language/pattern_matching_spec.rb @@ -0,0 +1,1302 @@ +require_relative '../spec_helper' + +ruby_version_is "2.7" do + describe "Pattern matching" do + # TODO: Remove excessive eval calls when support of previous version + # Ruby 2.6 will be dropped + + before :each do + ScratchPad.record [] + end + + ruby_version_is "3.0" do + it "can be standalone assoc operator that deconstructs value" do + suppress_warning do + eval(<<-RUBY).should == [0, 1] + [0, 1] => [a, b] + [a, b] + RUBY + end + end + + describe "find pattern" do + it "captures preceding elements to the pattern" do + eval(<<~RUBY).should == [0, 1] + case [0, 1, 2, 3] + in [*pre, 2, 3] + pre + else + false + end + RUBY + end + + it "captures following elements to the pattern" do + eval(<<~RUBY).should == [2, 3] + case [0, 1, 2, 3] + in [0, 1, *post] + post + else + false + end + RUBY + end + + it "captures both preceding and following elements to the pattern" do + eval(<<~RUBY).should == [[0, 1], [3, 4]] + case [0, 1, 2, 3, 4] + in [*pre, 2, *post] + [pre, post] + else + false + end + RUBY + end + + it "can capture the entirety of the pattern" do + eval(<<~RUBY).should == [0, 1, 2, 3, 4] + case [0, 1, 2, 3, 4] + in [*everything] + everything + else + false + end + RUBY + end + + it "will match an empty Array-like structure" do + eval(<<~RUBY).should == [] + case [] + in [*everything] + everything + else + false + end + RUBY + end + end + end + + it "extends case expression with case/in construction" do + eval(<<~RUBY).should == :bar + case [0, 1] + in [0] + :foo + in [0, 1] + :bar + end + RUBY + end + + it "allows using then operator" do + eval(<<~RUBY).should == :bar + case [0, 1] + in [0] then :foo + in [0, 1] then :bar + end + RUBY + end + + describe "warning" do + before :each do + @experimental, Warning[:experimental] = Warning[:experimental], true + end + + after :each do + Warning[:experimental] = @experimental + end + + context 'when regular form' do + before :each do + @src = 'case [0, 1]; in [a, b]; end' + end + + ruby_version_is ""..."3.0" do + it "warns about pattern matching is experimental feature" do + -> { eval @src }.should complain(/pattern matching is experimental, and the behavior may change in future versions of Ruby!/i) + end + end + + ruby_version_is "3.0" do + it "does not warn about pattern matching is experimental feature" do + -> { eval @src }.should_not complain + end + end + end + + context 'when one-line form' do + ruby_version_is '3.0' do + before :each do + @src = '[0, 1] => [a, b]' + end + + ruby_version_is ""..."3.1" do + it "warns about pattern matching is experimental feature" do + -> { eval @src }.should complain(/pattern matching is experimental, and the behavior may change in future versions of Ruby!/i) + end + end + + ruby_version_is "3.1" do + it "does not warn about pattern matching is experimental feature" do + -> { eval @src }.should_not complain + end + end + end + end + end + + it "binds variables" do + eval(<<~RUBY).should == 1 + case [0, 1] + in [0, a] + a + end + RUBY + end + + it "cannot mix in and when operators" do + -> { + eval <<~RUBY + case [] + when 1 == 1 + in [] + end + RUBY + }.should raise_error(SyntaxError, /syntax error, unexpected `in'/) + + -> { + eval <<~RUBY + case [] + in [] + when 1 == 1 + end + RUBY + }.should raise_error(SyntaxError, /syntax error, unexpected `when'/) + end + + it "checks patterns until the first matching" do + eval(<<~RUBY).should == :bar + case [0, 1] + in [0] + :foo + in [0, 1] + :bar + in [0, 1] + :baz + end + RUBY + end + + it "executes else clause if no pattern matches" do + eval(<<~RUBY).should == false + case [0, 1] + in [0] + true + else + false + end + RUBY + end + + it "raises NoMatchingPatternError if no pattern matches and no else clause" do + -> { + eval <<~RUBY + case [0, 1] + in [0] + end + RUBY + }.should raise_error(NoMatchingPatternError, /\[0, 1\]/) + end + + it "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/) + end + + it "evaluates the case expression once for multiple patterns, caching the result" do + eval(<<~RUBY).should == true + case (ScratchPad << :foo; 1) + in 0 + false + in 1 + true + end + RUBY + + ScratchPad.recorded.should == [:foo] + end + + describe "guards" do + it "supports if guard" do + eval(<<~RUBY).should == false + case 0 + in 0 if false + true + else + false + end + RUBY + + eval(<<~RUBY).should == true + case 0 + in 0 if true + true + else + false + end + RUBY + end + + it "supports unless guard" do + eval(<<~RUBY).should == false + case 0 + in 0 unless true + true + else + false + end + RUBY + + eval(<<~RUBY).should == true + case 0 + in 0 unless false + true + else + false + end + RUBY + end + + it "makes bound variables visible in guard" do + eval(<<~RUBY).should == true + case [0, 1] + in [a, 1] if a >= 0 + true + end + RUBY + end + + it "does not evaluate guard if pattern does not match" do + eval <<~RUBY + case 0 + in 1 if (ScratchPad << :foo) || true + else + end + RUBY + + ScratchPad.recorded.should == [] + end + + it "takes guards into account when there are several matching patterns" do + eval(<<~RUBY).should == :bar + case 0 + in 0 if false + :foo + in 0 if true + :bar + end + RUBY + end + + it "executes else clause if no guarded pattern matches" do + eval(<<~RUBY).should == false + case 0 + in 0 if false + true + else + false + end + RUBY + end + + it "raises NoMatchingPatternError if no guarded pattern matches and no else clause" do + -> { + eval <<~RUBY + case [0, 1] + in [0, 1] if false + end + RUBY + }.should raise_error(NoMatchingPatternError, /\[0, 1\]/) + end + end + + describe "value pattern" do + it "matches an object such that pattern === object" do + eval(<<~RUBY).should == true + case 0 + in 0 + true + end + RUBY + + eval(<<~RUBY).should == true + case 0 + in (-1..1) + true + end + RUBY + + eval(<<~RUBY).should == true + case 0 + in Integer + true + end + RUBY + + eval(<<~RUBY).should == true + case "0" + in /0/ + true + end + RUBY + + eval(<<~RUBY).should == true + case "0" + in ->(s) { s == "0" } + true + end + RUBY + end + + it "allows string literal with interpolation" do + x = "x" + + eval(<<~RUBY).should == true + case "x" + in "#{x + ""}" + true + end + RUBY + end + end + + describe "variable pattern" do + it "matches a value and binds variable name to this value" do + eval(<<~RUBY).should == 0 + case 0 + in a + a + end + RUBY + end + + it "makes bounded variable visible outside a case statement scope" do + eval(<<~RUBY).should == 0 + case 0 + in a + end + + a + RUBY + end + + it "create local variables even if a pattern doesn't match" do + eval(<<~RUBY).should == [0, nil, nil] + case 0 + in a + in b + in c + end + + [a, b, c] + RUBY + end + + it "allow using _ name to drop values" do + eval(<<~RUBY).should == 0 + case [0, 1] + in [a, _] + a + end + RUBY + end + + it "supports using _ in a pattern several times" do + eval(<<~RUBY).should == true + case [0, 1, 2] + in [0, _, _] + true + end + RUBY + end + + it "supports using any name with _ at the beginning in a pattern several times" do + eval(<<~RUBY).should == true + case [0, 1, 2] + in [0, _x, _x] + true + end + RUBY + + eval(<<~RUBY).should == true + case {a: 0, b: 1, c: 2} + in {a: 0, b: _x, c: _x} + true + end + RUBY + end + + it "does not support using variable name (except _) several times" do + -> { + eval <<~RUBY + case [0] + in [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 + + eval(<<~RUBY).should == true + case 0 + in ^a + true + end + RUBY + end + + it "allows applying ^ operator to bound variables" do + eval(<<~RUBY).should == 1 + case [1, 1] + in [n, ^n] + n + end + RUBY + + eval(<<~RUBY).should == false + case [1, 2] + in [n, ^n] + true + else + false + end + RUBY + end + + it "requires bound variable to be specified in a pattern before ^ operator when it relies on a bound variable" do + -> { + eval <<~RUBY + case [1, 2] + in [^n, n] + true + else + false + end + RUBY + }.should raise_error(SyntaxError, /n: no such local variable/) + end + end + + describe "alternative pattern" do + it "matches if any of patterns matches" do + eval(<<~RUBY).should == true + case 0 + in 0 | 1 | 2 + true + end + RUBY + end + + it "does not support variable binding" do + -> { + eval <<~RUBY + case [0, 1] + in [0, 0] | [0, a] + end + RUBY + }.should raise_error(SyntaxError, /illegal variable in alternative pattern/) + end + + it "support underscore prefixed variables in alternation" do + eval(<<~RUBY).should == true + case [0, 1] + in [1, _] + false + in [0, 0] | [0, _a] + true + end + RUBY + end + + it "can be used as a nested pattern" do + eval(<<~RUBY).should == true + case [[1], ["2"]] + in [[0] | nil, _] + false + in [[1], [1]] + false + in [[1], [2 | "2"]] + true + end + RUBY + + eval(<<~RUBY).should == true + case [1, 2] + in [0, _] | {a: 0} + false + in {a: 1, b: 2} | [1, 2] + true + end + RUBY + end + end + + describe "AS pattern" do + it "binds a variable to a value if pattern matches" do + eval(<<~RUBY).should == 0 + case 0 + in Integer => n + n + end + RUBY + end + + it "can be used as a nested pattern" do + eval(<<~RUBY).should == [2, 3] + case [1, [2, 3]] + in [1, Array => ary] + ary + end + RUBY + end + end + + describe "Array pattern" do + it "supports form Constant(pat, pat, ...)" do + eval(<<~RUBY).should == true + case [0, 1, 2] + in Array(0, 1, 2) + true + end + RUBY + end + + it "supports form Constant[pat, pat, ...]" do + eval(<<~RUBY).should == true + case [0, 1, 2] + in Array[0, 1, 2] + true + end + RUBY + end + + it "supports form [pat, pat, ...]" do + eval(<<~RUBY).should == true + case [0, 1, 2] + in [0, 1, 2] + true + end + RUBY + end + + it "supports form pat, pat, ..." do + eval(<<~RUBY).should == true + case [0, 1, 2] + in 0, 1, 2 + true + end + RUBY + + eval(<<~RUBY).should == 1 + case [0, 1, 2] + in 0, a, 2 + a + end + RUBY + + eval(<<~RUBY).should == [1, 2] + case [0, 1, 2] + in 0, *rest + rest + end + RUBY + end + + it "matches an object with #deconstruct method which returns an array and each element in array matches element in pattern" do + obj = Object.new + def obj.deconstruct; [0, 1] end + + eval(<<~RUBY).should == true + case obj + in [Integer, Integer] + true + end + RUBY + end + + ruby_version_is "3.0" do + it "calls #deconstruct once for multiple patterns, caching the result" do + obj = Object.new + + def obj.deconstruct + ScratchPad << :deconstruct + [0, 1] + end + + eval(<<~RUBY).should == true + case obj + in [1, 2] + false + in [0, 1] + true + end + RUBY + + ScratchPad.recorded.should == [:deconstruct] + end + end + + it "calls #deconstruct even on objects that are already an array" do + obj = [1, 2] + def obj.deconstruct + ScratchPad << :deconstruct + [3, 4] + end + + eval(<<~RUBY).should == true + case obj + in [3, 4] + true + else + false + end + RUBY + + ScratchPad.recorded.should == [:deconstruct] + end + + it "does not match object if Constant === object returns false" do + eval(<<~RUBY).should == false + case [0, 1, 2] + in String[0, 1, 2] + true + else + false + end + RUBY + end + + it "does not match object without #deconstruct method" do + obj = Object.new + obj.should_receive(:respond_to?).with(:deconstruct) + + eval(<<~RUBY).should == false + case obj + in Object[] + true + else + false + end + RUBY + end + + it "raises TypeError if #deconstruct method does not return array" do + obj = Object.new + def obj.deconstruct; "" end + + -> { + eval <<~RUBY + case obj + in Object[] + else + end + RUBY + }.should raise_error(TypeError, /deconstruct must return Array/) + end + + it "accepts a subclass of Array from #deconstruct" do + obj = Object.new + def obj.deconstruct + subarray = Class.new(Array).new(2) + def subarray.[](n) + n + end + subarray + end + + eval(<<~RUBY).should == true + case obj + in [1, 2] + false + in [0, 1] + true + end + RUBY + end + + it "does not match object if elements of array returned by #deconstruct method does not match elements in pattern" do + obj = Object.new + def obj.deconstruct; [1] end + + eval(<<~RUBY).should == false + case obj + in Object[0] + true + else + false + end + RUBY + end + + it "binds variables" do + eval(<<~RUBY).should == [0, 1, 2] + case [0, 1, 2] + in [a, b, c] + [a, b, c] + end + RUBY + end + + it "supports splat operator *rest" do + eval(<<~RUBY).should == [1, 2] + case [0, 1, 2] + in [0, *rest] + rest + end + RUBY + end + + it "does not match partially by default" do + eval(<<~RUBY).should == false + case [0, 1, 2, 3] + in [1, 2] + true + else + false + end + RUBY + end + + it "does match partially from the array beginning if list + , syntax used" do + eval(<<~RUBY).should == true + case [0, 1, 2, 3] + in [0, 1,] + true + end + RUBY + + eval(<<~RUBY).should == true + case [0, 1, 2, 3] + in 0, 1,; + true + end + RUBY + end + + it "matches [] with []" do + eval(<<~RUBY).should == true + case [] + in [] + true + end + RUBY + end + + it "matches anything with *" do + eval(<<~RUBY).should == true + case [0, 1] + in *; + true + end + RUBY + end + + it "can be used as a nested pattern" do + eval(<<~RUBY).should == true + case [[1], ["2"]] + in [[0] | nil, _] + false + in [[1], [1]] + false + in [[1], [2 | "2"]] + true + end + RUBY + + eval(<<~RUBY).should == true + case [1, 2] + in [0, _] | {a: 0} + false + in {a: 1, b: 2} | [1, 2] + true + end + RUBY + end + end + + describe "Hash pattern" do + it "supports form Constant(id: pat, id: pat, ...)" do + eval(<<~RUBY).should == true + case {a: 0, b: 1} + in Hash(a: 0, b: 1) + true + end + RUBY + end + + it "supports form Constant[id: pat, id: pat, ...]" do + eval(<<~RUBY).should == true + case {a: 0, b: 1} + in Hash[a: 0, b: 1] + true + end + RUBY + end + + it "supports form {id: pat, id: pat, ...}" do + eval(<<~RUBY).should == true + case {a: 0, b: 1} + in {a: 0, b: 1} + true + end + RUBY + end + + it "supports form id: pat, id: pat, ..." do + eval(<<~RUBY).should == true + case {a: 0, b: 1} + in a: 0, b: 1 + true + end + RUBY + + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in a: a, b: b + [a, b] + end + RUBY + + eval(<<~RUBY).should == { b: 1, c: 2 } + case {a: 0, b: 1, c: 2} + in a: 0, **rest + rest + end + RUBY + end + + it "supports a: which means a: a" do + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in Hash(a:, b:) + [a, b] + end + RUBY + + a = b = nil + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in Hash[a:, b:] + [a, b] + end + RUBY + + a = b = nil + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in {a:, b:} + [a, b] + end + RUBY + + a = nil + eval(<<~RUBY).should == [0, {b: 1, c: 2}] + case {a: 0, b: 1, c: 2} + in {a:, **rest} + [a, rest] + end + RUBY + + a = b = nil + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in a:, b: + [a, b] + end + RUBY + end + + it "can mix key (a:) and key-value (a: b) declarations" do + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in Hash(a:, b: x) + [a, x] + end + RUBY + end + + it "supports 'string': key literal" do + eval(<<~RUBY).should == true + case {a: 0} + in {"a": 0} + true + end + RUBY + end + + it "does not support non-symbol keys" do + -> { + eval <<~RUBY + case {a: 1} + in {"a" => 1} + end + RUBY + }.should raise_error(SyntaxError, /unexpected/) + end + + it "does not support string interpolation in keys" do + x = "a" + + -> { + eval <<~'RUBY' + case {a: 1} + in {"#{x}": 1} + end + RUBY + }.should raise_error(SyntaxError, /symbol literal with interpolation is not allowed/) + 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 + + eval(<<~RUBY).should == true + case obj + in {a: 1} + true + end + RUBY + end + + it "calls #deconstruct_keys per pattern" do + obj = Object.new + + def obj.deconstruct_keys(*) + ScratchPad << :deconstruct_keys + {a: 1} + end + + eval(<<~RUBY).should == true + case obj + in {b: 1} + false + in {a: 1} + true + end + RUBY + + ScratchPad.recorded.should == [:deconstruct_keys, :deconstruct_keys] + end + + it "does not match object if Constant === object returns false" do + eval(<<~RUBY).should == false + case {a: 1} + in String[a: 1] + true + else + false + end + RUBY + end + + it "does not match object without #deconstruct_keys method" do + obj = Object.new + obj.should_receive(:respond_to?).with(:deconstruct_keys) + + eval(<<~RUBY).should == false + case obj + in Object[a: 1] + true + else + false + end + RUBY + end + + it "does not match object if #deconstruct_keys method does not return Hash" do + obj = Object.new + def obj.deconstruct_keys(*); "" end + + -> { + eval <<~RUBY + case obj + in Object[a: 1] + end + RUBY + }.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 + + eval(<<~RUBY).should == false + case obj + in Object[a: 1] + true + else + false + end + RUBY + end + + it "does not match object if elements of Hash returned by #deconstruct_keys method does not match values in pattern" do + obj = Object.new + def obj.deconstruct_keys(*); {a: 1} end + + eval(<<~RUBY).should == false + case obj + in Object[a: 2] + true + else + false + end + RUBY + 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 + + eval <<~RUBY + case obj + in Object[a: 1, b: 2, c: 3] + end + RUBY + + ScratchPad.recorded.sort.should == [[[:a, :b, :c]]] + end + + it "passes keys specified in pattern to #deconstruct_keys method if pattern contains double splat operator **" do + obj = Object.new + + def obj.deconstruct_keys(*args) + ScratchPad << args + {a: 1, b: 2, c: 3} + end + + eval <<~RUBY + case obj + in Object[a: 1, b: 2, **] + end + RUBY + + ScratchPad.recorded.sort.should == [[[:a, :b]]] + end + + it "passes nil to #deconstruct_keys method if pattern contains double splat operator **rest" do + obj = Object.new + + def obj.deconstruct_keys(*args) + ScratchPad << args + {a: 1, b: 2} + end + + eval <<~RUBY + case obj + in Object[a: 1, **rest] + end + RUBY + + ScratchPad.recorded.should == [[nil]] + end + + it "binds variables" do + eval(<<~RUBY).should == [0, 1, 2] + case {a: 0, b: 1, c: 2} + in {a: x, b: y, c: z} + [x, y, z] + end + RUBY + end + + it "supports double splat operator **rest" do + eval(<<~RUBY).should == {b: 1, c: 2} + case {a: 0, b: 1, c: 2} + in {a: 0, **rest} + rest + end + RUBY + end + + it "treats **nil like there should not be any other keys in a matched Hash" do + eval(<<~RUBY).should == true + case {a: 1, b: 2} + in {a: 1, b: 2, **nil} + true + end + RUBY + + eval(<<~RUBY).should == false + case {a: 1, b: 2} + in {a: 1, **nil} + true + else + false + end + RUBY + end + + it "can match partially" do + eval(<<~RUBY).should == true + case {a: 1, b: 2} + in {a: 1} + true + end + RUBY + end + + it "matches {} with {}" do + eval(<<~RUBY).should == true + case {} + in {} + true + end + RUBY + end + + it "matches anything with **" do + eval(<<~RUBY).should == true + case {a: 1} + in **; + true + end + RUBY + end + + it "can be used as a nested pattern" do + eval(<<~RUBY).should == true + case {a: {a: 1, b: 1}, b: {a: 1, b: 2}} + in {a: {a: 0}} + false + in {a: {a: 1}, b: {b: 1}} + false + in {a: {a: 1}, b: {b: 2}} + true + end + RUBY + + eval(<<~RUBY).should == true + case [{a: 1, b: [1]}, {a: 1, c: ["2"]}] + in [{a:, c:},] + false + in [{a: 1, b:}, {a: 1, c: [Integer]}] + false + in [_, {a: 1, c: [String]}] + true + end + RUBY + end + end + + describe "refinements" do + it "are used for #deconstruct" do + refinery = Module.new do + refine Array do + def deconstruct + [0] + end + end + end + + result = nil + Module.new do + using refinery + + result = eval(<<~RUBY) + case [] + in [0] + true + end + RUBY + end + + result.should == true + end + + it "are used for #deconstruct_keys" do + refinery = Module.new do + refine Hash do + def deconstruct_keys(_) + {a: 0} + end + end + end + + result = nil + Module.new do + using refinery + + result = eval(<<~RUBY) + case {} + in a: 0 + true + end + RUBY + 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 = eval(<<~RUBY) + case {} + in Array + true + end + RUBY + end + + result.should == true + end + end + + ruby_version_is "3.1" do + it "can omit parentheses in one line pattern matching" do + eval(<<~RUBY).should == [1, 2] + [1, 2] => a, b + [a, b] + RUBY + + eval(<<~RUBY).should == 1 + {a: 1} => a: + a + RUBY + end + 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..5a3c2861ce --- /dev/null +++ b/spec/ruby/language/precedence_spec.rb @@ -0,0 +1,447 @@ +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). + +=begin +Excerpted from 'Programming Ruby: The Pragmatic Programmer's Guide' +Second Edition by Dave Thomas, Chad Fowler, and Andy Hunt, page 324 + +Table 22.4. Ruby operators (high to low precedence) +Method Operator Description +----------------------------------------------------------------------- + :: . + x* [ ] [ ]= Element reference, element set + x ** Exponentiation + x ! ~ + - Not, complement, unary plus and minus + (method names for the last two are +@ and -@) + x * / % Multiply, divide, and modulo + x + - Plus and minus + x >> << Right and left shift + x & “And” (bitwise for integers) + x ^ | Exclusive “or” and regular “or” (bitwise for integers) + x <= < > >= Comparison operators + x <=> == === != =~ !~ Equality and pattern match operators (!= + and !~ may not be defined as methods) + && Logical “and” + || Logical “or” + .. ... Range (inclusive and exclusive) + ? : Ternary if-then-else + = %= /= -= += |= &= Assignment + >>= <<= *= &&= ||= **= + defined? Check if symbol defined + not Logical negation + or and Logical composition + if unless while until Expression modifiers + begin/end Block expression +----------------------------------------------------------------------- + +* Operators marked with 'x' in the Method column are implemented as methods +and can be overridden (except != and !~ as noted). (But see the specs +below for implementations that define != and !~ as methods.) + +** These are not included in the excerpted table but are shown here for +completeness. +=end + +# ----------------------------------------------------------------------- +# 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..d311750200 --- /dev/null +++ b/spec/ruby/language/predefined_spec.rb @@ -0,0 +1,1335 @@ +require_relative '../spec_helper' +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. + +=begin +Exception Information +--------------------------------------------------------------------------------------------------- + +$! Exception The exception object passed to raise. [thread] +$@ Array The stack backtrace generated by the last exception. [thread] +=end + +=begin +Pattern Matching Variables +--------------------------------------------------------------------------------------------------- + +These variables are set to nil after an unsuccessful pattern match. + +$& String The string matched (following a successful pattern match). This variable is + local to the current scope. [r/o, thread] +$+ String The contents of the highest-numbered group matched following a successful + pattern match. Thus, in "cat" =~/(c|a)(t|z)/, $+ will be set to “t”. This + variable is local to the current scope. [r/o, thread] +$` String The string preceding the match in a successful pattern match. This variable + is local to the current scope. [r/o, thread] +$' String The string following the match in a successful pattern match. This variable + is local to the current scope. [r/o, thread] +$1 to $<N> String The contents of successive groups matched in a successful pattern match. In + "cat" =~/(c|a)(t|z)/, $1 will be set to “a” and $2 to “t”. This variable + is local to the current scope. [r/o, thread] +$~ MatchData An object that encapsulates the results of a successful pattern match. The + variables $&, $`, $', and $1 to $<N> are all derived from $~. Assigning to $~ + changes the values of these derived variables. This variable is local to the + current scope. [thread] +=end + + +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 + + eval '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) + -> { $~ = 1 }.should raise_error(TypeError) + 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".force_encoding(Encoding::EUC_JP) =~ /b/ + $&.encoding.should equal(Encoding::EUC_JP) + 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".force_encoding(Encoding::EUC_JP) =~ /b/ + $`.encoding.should equal(Encoding::EUC_JP) + end + + it "sets an empty result to the encoding of the source String" do + "abc".force_encoding(Encoding::ISO_8859_1) =~ /a/ + $`.encoding.should equal(Encoding::ISO_8859_1) + end +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".force_encoding(Encoding::EUC_JP) =~ /b/ + $'.encoding.should equal(Encoding::EUC_JP) + end + + it "sets an empty result to the encoding of the source String" do + "abc".force_encoding(Encoding::ISO_8859_1) =~ /c/ + $'.encoding.should equal(Encoding::ISO_8859_1) + end +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".force_encoding(Encoding::EUC_JP) =~ /(b)/ + $+.encoding.should equal(Encoding::EUC_JP) + 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".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) + 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 + # 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 + +=begin +Input/Output Variables +--------------------------------------------------------------------------------------------------- + +$/ String The input record separator (newline by default). This is the value that rou- + tines such as Kernel#gets use to determine record boundaries. If set to + nil, gets will read the entire file. +$-0 String Synonym for $/. +$\ String The string appended to the output of every call to methods such as + Kernel#print and IO#write. The default value is nil. +$, String The separator string output between the parameters to methods such as + Kernel#print and Array#join. Defaults to nil, which adds no text. +$. Integer The number of the last line read from the current input file. +$; String The default separator pattern used by String#split. May be set from the + command line using the -F flag. +$< Object An object that provides access to the concatenation of the contents of all + the files given as command-line arguments or $stdin (in the case where + there are no arguments). $< supports methods similar to a File object: + binmode, close, closed?, each, each_byte, each_line, eof, eof?, + file, filename, fileno, getc, gets, lineno, lineno=, path, pos, pos=, + read, readchar, readline, readlines, rewind, seek, skip, tell, to_a, + to_i, to_io, to_s, along with the methods in Enumerable. The method + file returns a File object for the file currently being read. This may change + as $< reads through the files on the command line. [r/o] +$> IO The destination of output for Kernel#print and Kernel#printf. The + default value is $stdout. +$_ String The last line read by Kernel#gets or Kernel#readline. Many string- + related functions in the Kernel module operate on $_ by default. The vari- + able is local to the current scope. [thread] +$-F String Synonym for $;. +$stderr IO The current standard error output. +$stdin IO The current standard input. +$stdout IO The current standard output. Assignment to $stdout is deprecated: use + $stdout.reopen instead. +=end + +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 + + 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 "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) + end + + it "raises a TypeError if assigned an Integer" do + -> { $/ = 1 }.should raise_error(TypeError) + end + + it "raises a TypeError if assigned a boolean" do + -> { $/ = true }.should raise_error(TypeError) + 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 + + it "can be assigned a String" do + str = "abc" + $-0 = str + $-0.should equal(str) + 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) + end + + it "raises a TypeError if assigned an Integer" do + -> { $-0 = 1 }.should raise_error(TypeError) + end + + it "raises a TypeError if assigned a boolean" do + -> { $-0 = true }.should raise_error(TypeError) + 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) + end + + ruby_version_is "2.7" do + it "warns if assigned non-nil" do + -> { $, = "_" }.should complain(/warning: `\$,' is deprecated/) + end + 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 + + ruby_version_is "2.7" do + it "warns if assigned non-nil" do + -> { $; = "_" }.should complain(/warning: `\$;' is deprecated/) + end + 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 + + eval '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 + +=begin +Execution Environment Variables +--------------------------------------------------------------------------------------------------- + +$0 String The name of the top-level Ruby program being executed. Typically this will + be the program’s filename. On some operating systems, assigning to this + variable will change the name of the process reported (for example) by the + ps(1) command. +$* Array An array of strings containing the command-line options from the invoca- + tion of the program. Options used by the Ruby interpreter will have been + removed. [r/o] +$" Array An array containing the filenames of modules loaded by require. [r/o] +$$ Integer The process number of the program being executed. [r/o] +$? Process::Status The exit status of the last child process to terminate. [r/o, thread] +$: Array An array of strings, where each string specifies a directory to be searched for + Ruby scripts and binary extensions used by the load and require methods. + The initial value is the value of the arguments passed via the -I command- + line option, followed by an installation-defined standard library location, fol- + lowed by the current directory (“.”). This variable may be set from within a + program to alter the default search path; typically, programs use $: << dir + to append dir to the path. [r/o] +$-a Object True if the -a option is specified on the command line. [r/o] +$-d Object Synonym for $DEBUG. +$DEBUG Object Set to true if the -d command-line option is specified. +__FILE__ String The name of the current source file. [r/o] +$F Array The array that receives the split input line if the -a command-line option is + used. +$FILENAME String The name of the current input file. Equivalent to $<.filename. [r/o] +$-i String If in-place edit mode is enabled (perhaps using the -i command-line + option), $-i holds the extension used when creating the backup file. If you + set a value into $-i, enables in-place edit mode. +$-I Array Synonym for $:. [r/o] +$-K String Sets the multibyte coding system for strings and regular expressions. Equiv- + alent to the -K command-line option. +$-l Object Set to true if the -l option (which enables line-end processing) is present + on the command line. [r/o] +__LINE__ String The current line number in the source file. [r/o] +$LOAD_PATH Array A synonym for $:. [r/o] +$-p Object Set to true if the -p option (which puts an implicit while gets . . . end + loop around your program) is present on the command line. [r/o] +$VERBOSE Object Set to true if the -v, --version, -W, or -w option is specified on the com- + mand line. Set to false if no option, or -W1 is given. Set to nil if -W0 + was specified. Setting this option to true causes the interpreter and some + library routines to report additional information. Setting to nil suppresses + all warnings (including the output of Kernel.warn). +$-v Object Synonym for $VERBOSE. +$-w Object Synonym for $VERBOSE. +=end +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") + end + + it "is read-only" do + -> { + $: = [] + }.should raise_error(NameError) + + -> { + $LOAD_PATH = [] + }.should raise_error(NameError) + + -> { + $-I = [] + }.should raise_error(NameError) + 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) + + -> { + $LOADED_FEATURES = [] + }.should raise_error(NameError) + end +end + +describe "Global variable $<" do + it "is read-only" do + -> { + $< = nil + }.should raise_error(NameError) + end +end + +describe "Global variable $FILENAME" do + it "is read-only" do + -> { + $FILENAME = "-" + }.should raise_error(NameError) + end +end + +describe "Global variable $?" do + it "is read-only" do + -> { + $? = nil + }.should raise_error(NameError) + 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) + end +end + +describe "Global variable $-l" do + it "is read-only" do + -> { $-l = true }.should raise_error(NameError) + end +end + +describe "Global variable $-p" do + it "is read-only" do + -> { $-p = true }.should raise_error(NameError) + 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 "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(File.dirname(__FILE__)) 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 + +=begin +Standard Objects +--------------------------------------------------------------------------------------------------- + +ARGF Object A synonym for $<. +ARGV Array A synonym for $*. +ENV Object A hash-like object containing the program’s environment variables. An + instance of class Object, ENV implements the full set of Hash methods. Used + to query and set the value of an environment variable, as in ENV["PATH"] + and ENV["term"]="ansi". +false FalseClass Singleton instance of class FalseClass. [r/o] +nil NilClass The singleton instance of class NilClass. The value of uninitialized + instance and global variables. [r/o] +self Object The receiver (object) of the current method. [r/o] +true TrueClass Singleton instance of class TrueClass. [r/o] +=end + +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) + 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) + 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) + end +end + +describe "The self pseudo-variable" do + it "raises a SyntaxError if assigned to" do + -> { eval("self = 1") }.should raise_error(SyntaxError) + end +end + +=begin +Global Constants +--------------------------------------------------------------------------------------------------- + +The following constants are defined by the Ruby interpreter. + +DATA IO If the main program file contains the directive __END__, then + the constant DATA will be initialized so that reading from it will + return lines following __END__ from the source file. +FALSE FalseClass Synonym for false (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). +=end + +describe "The predefined global constants" do + describe "TRUE" do + ruby_version_is "3.0" do + it "is no longer defined" do + Object.const_defined?(:TRUE).should == false + end + end + + ruby_version_is ""..."3.0" do + it "includes TRUE" do + Object.const_defined?(:TRUE).should == true + -> { TRUE }.should complain(/constant ::TRUE is deprecated/) + end + end + end + + describe "FALSE" do + ruby_version_is "3.0" do + it "is no longer defined" do + Object.const_defined?(:FALSE).should == false + end + end + + ruby_version_is ""..."3.0" do + it "includes FALSE" do + Object.const_defined?(:FALSE).should == true + -> { FALSE }.should complain(/constant ::FALSE is deprecated/) + end + end + end + + describe "NIL" do + ruby_version_is "3.0" do + it "is no longer defined" do + Object.const_defined?(:NIL).should == false + end + end + + ruby_version_is ""..."3.0" do + it "includes NIL" do + Object.const_defined?(:NIL).should == true + -> { NIL }.should complain(/constant ::NIL is deprecated/) + end + 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 + +ruby_version_is "2.7" do + describe "$LOAD_PATH.resolve_feature_path" do + it "returns what will be loaded without actual loading, .rb file" do + extension, path = $LOAD_PATH.resolve_feature_path('set') + extension.should == :rb + path.should.end_with?('/set.rb') + end + + it "returns what will be loaded without actual loading, .so file" do + require 'rbconfig' + + extension, path = $LOAD_PATH.resolve_feature_path('etc') + extension.should == :so + path.should.end_with?("/etc.#{RbConfig::CONFIG['DLEXT']}") + end + + ruby_version_is "2.7"..."3.1" do + it "raises LoadError if feature cannot be found" do + -> { $LOAD_PATH.resolve_feature_path('noop') }.should raise_error(LoadError) + end + end + + ruby_version_is "3.1" do + it "return nil if feature cannot be found" do + $LOAD_PATH.resolve_feature_path('noop').should be_nil + end + end + end +end diff --git a/spec/ruby/language/private_spec.rb b/spec/ruby/language/private_spec.rb new file mode 100644 index 0000000000..ddf185e6d2 --- /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..ef4a43bed6 --- /dev/null +++ b/spec/ruby/language/proc_spec.rb @@ -0,0 +1,246 @@ +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, | 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 + + ruby_version_is ""..."2.7" do + it 'autosplats keyword arguments' do + @p.call([1, {a: 1}]).should == [[1], {a: 1}] + end + end + + ruby_version_is "2.7"..."3.0" do + it 'autosplats keyword arguments and warns' do + -> { + @p.call([1, {a: 1}]).should == [[1], {a: 1}] + }.should complain(/warning: Using the last argument as keyword parameters is deprecated; maybe \*\* should be added to the call/) + end + end + + ruby_version_is "3.0" do + it 'does not autosplat keyword arguments' do + @p.call([1, {a: 1}]).should == [[[1, {a: 1}]], {}] + end + 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..4cde7e9488 --- /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 endless ranges" do + (1..).should == Range.new(1, nil) + (1...).should == Range.new(1, nil, true) + end + + ruby_version_is "3.0" do + it "is frozen" do + (42..).should.frozen? + end + end + + ruby_version_is "2.7" do + it "creates beginless ranges" do + eval("(..1)").should == Range.new(nil, 1) + eval("(...1)").should == Range.new(nil, 1, true) + end + 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..0129e255da --- /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..26750c20c5 --- /dev/null +++ b/spec/ruby/language/regexp/back-references_spec.rb @@ -0,0 +1,140 @@ +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 "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..0cf1e9b6f4 --- /dev/null +++ b/spec/ruby/language/regexp/character_classes_spec.rb @@ -0,0 +1,637 @@ +# 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{36F}".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{36F}".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 + + 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 + + ruby_bug "#17340", ''...'3.0' do + it "raises a RegexpError for an unterminated unicode property" do + -> { Regexp.new('\p{') }.should raise_error(RegexpError) + end + 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..febc3fdb37 --- /dev/null +++ b/spec/ruby/language/regexp/encoding_spec.rb @@ -0,0 +1,148 @@ +# -*- encoding: binary -*- +require_relative '../../spec_helper' +require_relative '../fixtures/classes' + +describe "Regexps with encoding modifiers" do + it "supports /e (EUC encoding)" do + match = /./e.match("\303\251".force_encoding(Encoding::EUC_JP)) + match.to_a.should == ["\303\251".force_encoding(Encoding::EUC_JP)] + end + + it "supports /e (EUC encoding) with interpolation" do + match = /#{/./}/e.match("\303\251".force_encoding(Encoding::EUC_JP)) + match.to_a.should == ["\303\251".force_encoding(Encoding::EUC_JP)] + end + + it "supports /e (EUC encoding) with interpolation /o" do + match = /#{/./}/e.match("\303\251".force_encoding(Encoding::EUC_JP)) + match.to_a.should == ["\303\251".force_encoding(Encoding::EUC_JP)] + 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 + -> { /./n.match("\303\251".force_encoding('utf-8')) }.should complain(%r{historical binary regexp match /.../n against UTF-8 string}) + end + + it 'uses US-ASCII as /n encoding if all chars are 7-bit' do + /./n.encoding.should == Encoding::US_ASCII + end + + 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".force_encoding(Encoding::Windows_31J)) + match.to_a.should == ["\303".force_encoding(Encoding::Windows_31J)] + end + + it "supports /s (Windows_31J encoding) with interpolation" do + match = /#{/./}/s.match("\303\251".force_encoding(Encoding::Windows_31J)) + match.to_a.should == ["\303".force_encoding(Encoding::Windows_31J)] + end + + it "supports /s (Windows_31J encoding) with interpolation and /o" do + match = /#{/./}/s.match("\303\251".force_encoding(Encoding::Windows_31J)) + match.to_a.should == ["\303".force_encoding(Encoding::Windows_31J)] + 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".force_encoding('utf-8')).to_a.should == ["\u{e9}"] + end + + it "supports /u (UTF8 encoding) with interpolation" do + /#{/./}/u.match("\303\251".force_encoding('utf-8')).to_a.should == ["\u{e9}"] + end + + it "supports /u (UTF8 encoding) with interpolation and /o" do + /#{/./}/u.match("\303\251".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("".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("".force_encoding("US-ASCII"), Regexp::FIXEDENCODING) =~ "\303\251".force_encoding('UTF-8') }.should raise_error(Encoding::CompatibilityError) + end + + it "raises ArgumentError when trying to match a broken String" do + s = "\x80".force_encoding('UTF-8') + -> { s =~ /./ }.should raise_error(ArgumentError, "invalid byte sequence in UTF-8") + end + + it "computes the Regexp Encoding for each interpolated Regexp instance" do + make_regexp = -> str { /#{str}/ } + + r = make_regexp.call("été".force_encoding(Encoding::UTF_8)) + r.should.fixed_encoding? + r.encoding.should == Encoding::UTF_8 + + r = make_regexp.call("abc".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..2e5fe5ad2e --- /dev/null +++ b/spec/ruby/language/regexp/escapes_spec.rb @@ -0,0 +1,97 @@ +# -*- encoding: binary -*- +require_relative '../../spec_helper' +require_relative '../fixtures/classes' + +describe "Regexps with escape characters" do + it "they're supported" 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 "support quoting meta-characters via escape sequence" do + /\\/.match("\\").to_a.should == ["\\"] + /\//.match("/").to_a.should == ["/"] + # 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 == ["}"] + # 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 "allows any character to be escaped" do + /\y/.match("y").to_a.should == ["y"] + end + + it "support \\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 "support \\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..2f04a04018 --- /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..9a191d74e2 --- /dev/null +++ b/spec/ruby/language/regexp/repetition_spec.rb @@ -0,0 +1,142 @@ +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" + + ruby_bug '#17341', ''...'3.0' do + /a+?*/.match("aa")[0].should == "aa" + end + /(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" + + ruby_bug '#17341', ''...'3.0' do + /a+?+/.match("aa")[0].should == "aa" + end + /(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..f607fa6010 --- /dev/null +++ b/spec/ruby/language/regexp_spec.rb @@ -0,0 +1,190 @@ +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 + + ruby_version_is "3.0" do + it "is frozen" do + /Hello/.should.frozen? + end + 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 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", ""..."3.2" 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 + + escapable_terminators = ['!', '"', '#', '%', '&', "'", ',', '-', ':', ';', '@', '_', '`'] + + it "supports escaping characters when used as a terminator" do + escapable_terminators.each do |c| + ref = "(?-mix:#{c})" + pattern = eval("%r" + c + "\\" + c + c) + pattern.to_s.should == ref + end + end + + it "treats an escaped non-escapable character normally when used as a terminator" do + all_terminators = [*("!".."/"), *(":".."@"), *("[".."`"), *("{".."~")] + special_cases = ['(', '{', '[', '<', '\\', '=', '~'] + (all_terminators - special_cases - escapable_terminators).each do |c| + ref = "(?-mix:\\#{c})" + pattern = eval("%r" + c + "\\" + c + c) + pattern.to_s.should == ref + end + end + + it "support handling unicode 9.0 characters with POSIX bracket expressions" do + char_lowercase = "\u{104D8}" # OSAGE SMALL LETTER A + /[[:lower:]]/.match(char_lowercase).to_s.should == char_lowercase + 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..4d164b38c6 --- /dev/null +++ b/spec/ruby/language/rescue_spec.rb @@ -0,0 +1,505 @@ +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 setter method' do + RescueSpecs::SetterCaptor.should_capture_exception + end + + it 'using a square brackets setter' do + RescueSpecs::SquareBracketsCaptor.should_capture_exception + 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 "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 include ":in `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 + + 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 + + ruby_version_is "2.7" do + it "rescues with multiple assignment" do + + a, b = raise rescue [1, 2] + + a.should == 1 + b.should == 2 + 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..ee5377946f --- /dev/null +++ b/spec/ruby/language/retry_spec.rb @@ -0,0 +1,52 @@ +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 begin statement" do + -> { eval 'retry' }.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..d8506834c8 --- /dev/null +++ b/spec/ruby/language/return_spec.rb @@ -0,0 +1,489 @@ +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 + ruby_version_is "2.7" do + it "is not allowed" do + File.write(@filename, <<-END_OF_CODE) + class ReturnSpecs::A + ScratchPad << "before return" + 1.times { return } + ScratchPad << "after return" + end + END_OF_CODE + + -> { load @filename }.should raise_error(LocalJumpError) + end + 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 + ruby_version_is ""..."2.7" do + it "does not affect exit status" do + ruby_exe(<<-END_OF_CODE).should == "" + return 10 + END_OF_CODE + + $?.exitstatus.should == 0 + end + end + + ruby_version_is "2.7" do + it "warns but does not affect exit status" do + err = ruby_exe(<<-END_OF_CODE, args: "2>&1") + return 10 + END_OF_CODE + $?.exitstatus.should == 0 + + err.should =~ /warning: argument of top-level return is ignored/ + end + 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..c3aecff2dd --- /dev/null +++ b/spec/ruby/language/safe_navigator_spec.rb @@ -0,0 +1,99 @@ +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 + eval("nil&.unknown").should == nil + eval("[][10]&.unknown").should == nil + end + + it "can be chained" do + eval("nil&.one&.two&.three").should == nil + end + + it "doesn't evaluate arguments" do + obj = Object.new + obj.should_not_receive(:m) + eval("nil&.unknown(obj.m) { obj.m }") + end + end + + context "when context is false" do + it "calls the method" do + eval("false&.to_s").should == "false" + + -> { eval("false&.unknown") }.should raise_error(NoMethodError) + end + end + + context "when context is truthy" do + it "calls the method" do + eval("1&.to_s").should == "1" + + -> { eval("1&.unknown") }.should raise_error(NoMethodError) + end + end + + it "takes a list of arguments" do + eval("[1,2,3]&.first(2)").should == [1,2] + end + + it "takes a block" do + eval("[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 + + eval("obj&.foo = 3").should == 3 + obj.foo.should == 3 + + obj = nil + eval("obj&.foo = 3").should == nil + end + + it "allows assignment operators" do + klass = Class.new do + attr_accessor :m + + def initialize + @m = 0 + end + end + + obj = klass.new + + eval("obj&.m += 3") + obj.m.should == 3 + + obj = nil + eval("obj&.m += 3").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 + + -> { + eval("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..89830a2069 --- /dev/null +++ b/spec/ruby/language/safe_spec.rb @@ -0,0 +1,119 @@ +require_relative '../spec_helper' + +describe "The $SAFE variable" do + ruby_version_is ""..."2.7" do + after :each do + $SAFE = 0 + end + + it "is 0 by default" do + $SAFE.should == 0 + proc { + $SAFE.should == 0 + }.call + end + + it "can be set to 0" do + proc { + $SAFE = 0 + $SAFE.should == 0 + }.call + end + + it "can be set to 1" do + proc { + $SAFE = 1 + $SAFE.should == 1 + }.call + end + + [2, 3, 4].each do |n| + it "cannot be set to #{n}" do + -> { + proc { + $SAFE = n + }.call + }.should raise_error(ArgumentError, /\$SAFE=2 to 4 are obsolete/) + end + end + + it "raises ArgumentError when set to values below 0" do + -> { + proc { + $SAFE = -100 + }.call + }.should raise_error(ArgumentError, "$SAFE should be >= 0") + end + + it "cannot be set to values above 4" do + -> { + proc { + $SAFE = 100 + }.call + }.should raise_error(ArgumentError, /\$SAFE=2 to 4 are obsolete/) + end + + it "can be manually lowered" do + $SAFE = 1 + $SAFE = 0 + $SAFE.should == 0 + end + + it "is not Proc local" do + $SAFE.should == 0 + proc { + $SAFE = 1 + }.call + $SAFE.should == 1 + end + + it "is not lambda local" do + $SAFE.should == 0 + -> { + $SAFE = 1 + }.call + $SAFE.should == 1 + end + + it "is global like regular global variables" do + Thread.new { $SAFE }.value.should == 0 + $SAFE = 1 + Thread.new { $SAFE }.value.should == 1 + end + + it "can be read when default from Thread#safe_level" do + Thread.current.safe_level.should == 0 + end + + it "can be read when modified from Thread#safe_level" do + proc { + $SAFE = 1 + Thread.current.safe_level.should == 1 + }.call + end + end + + ruby_version_is "2.7"..."3.0" do + it "warn when access" do + -> { + $SAFE + }.should complain(/\$SAFE will become a normal global variable in Ruby 3.0/) + end + + it "warn when set" do + -> { + $SAFE = 1 + }.should complain(/\$SAFE will become a normal global variable in Ruby 3.0/) + end + end + + ruby_version_is "3.0" do + it "$SAFE is a regular global variable" do + $SAFE.should == nil + $SAFE = 42 + $SAFE.should == 42 + ensure + $SAFE = nil + end + end +end diff --git a/spec/ruby/language/send_spec.rb b/spec/ruby/language/send_spec.rb new file mode 100644 index 0000000000..e57e2c65dc --- /dev/null +++ b/spec/ruby/language/send_spec.rb @@ -0,0 +1,580 @@ +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 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 + + 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 + ruby_version_is ""..."2.7" do + it "does not permit self as a receiver" do + receiver = LangSendSpecs::PrivateGetter.new + -> { receiver.call_self_foo }.should raise_error(NoMethodError) + -> { receiver.call_self_foo_or_equals(6) }.should raise_error(NoMethodError) + end + end + + ruby_version_is "2.7" do + it "permits self as a receiver" do + receiver = LangSendSpecs::PrivateGetter.new + receiver.call_self_foo_or_equals(6) + receiver.call_self_foo.should == 6 + end + 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 + + ruby_version_is ""..."3.0" do + it "expands the Array elements from the splat after executing the arguments and block if no other arguments follow the splat" do + def self.m(*args, &block) + [args, block] + end + + args = [1, nil] + m(*args, &args.pop).should == [[1], nil] + + args = [1, nil] + order = [] + m(*(order << :args; args), &(order << :block; args.pop)).should == [[1], nil] + order.should == [:args, :block] + end + end + + ruby_version_is "3.0" do + it "expands the Array elements from the splat before applying block argument operations" do + def self.m(*args, &block) + [args, block] + end + + args = [1, nil] + m(*args, &args.pop).should == [[1, nil], nil] + + args = [1, nil] + order = [] + m(*(order << :args; args), &(order << :block; args.pop)).should == [[1, nil], nil] + order.should == [:args, :block] + end + 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..c1fb682ea0 --- /dev/null +++ b/spec/ruby/language/singleton_class_spec.rb @@ -0,0 +1,293 @@ +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".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 diff --git a/spec/ruby/language/source_encoding_spec.rb b/spec/ruby/language/source_encoding_spec.rb new file mode 100644 index 0000000000..19364fc676 --- /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 because empty as 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..ce4941569e --- /dev/null +++ b/spec/ruby/language/string_spec.rb @@ -0,0 +1,321 @@ +# -*- encoding: binary -*- + +require_relative '../spec_helper' + +# 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 "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 + + ruby_version_is ''...'2.7' do + it "taints the result of interpolation when an interpolated value is tainted" do + "#{"".taint}".tainted?.should be_true + + @ip.taint + "#@ip".tainted?.should be_true + + $ip.taint + "#$ip".tainted?.should be_true + end + + it "untrusts the result of interpolation when an interpolated value is untrusted" do + "#{"".untrust}".untrusted?.should be_true + + @ip.untrust + "#@ip".untrusted?.should be_true + + $ip.untrust + "#$ip".untrusted?.should be_true + end + end + + it "allows using non-alnum characters as string delimiters" do + %(hey #{@ip}).should == "hey xxx" + %[hey #{@ip}].should == "hey xxx" + %{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 + + it "produce different objects for literals with the same content in different files if the other file doesn't have the comment" do + ruby_exe(fixture(__FILE__, "freeze_magic_comment_across_files_no_comment.rb")).chomp.should == "true" + 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"'.force_encoding("us-ascii")).encoding.should == Encoding::US_ASCII + eval("# coding: US-ASCII \n 'a#{"b"}c'").encoding.should == Encoding::US_ASCII + end + + it "returns a string with the source encoding, even if the components have another encoding" do + a = "abc".force_encoding("euc-jp") + "#{a}".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".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 + + ruby_version_is "3.0" do + it "creates a non-frozen String when # frozen-string-literal: true is used" do + code = <<~'RUBY' + # frozen-string-literal: true + "a#{6*7}c" + RUBY + eval(code).should_not.frozen? + end + end + + ruby_version_is ""..."3.0" do + it "creates a frozen String when # frozen-string-literal: true is used" do + code = <<~'RUBY' + # frozen-string-literal: true + "a#{6*7}c" + RUBY + eval(code).should.frozen? + end + end +end diff --git a/spec/ruby/language/super_spec.rb b/spec/ruby/language/super_spec.rb new file mode 100644 index 0000000000..1ac5c5e1be --- /dev/null +++ b/spec/ruby/language/super_spec.rb @@ -0,0 +1,434 @@ +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 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 + + # 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] + end + + it "without explicit arguments that are '_' including any modifications" do + SuperSpecs::ZSuperWithUnderscores::B.new.m_modified(1, 2).should == [14, 2] + 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..d6a41d3059 --- /dev/null +++ b/spec/ruby/language/symbol_spec.rb @@ -0,0 +1,106 @@ +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 + + it "with invalid bytes raises an EncodingError at parse time" do + ScratchPad.record [] + -> { + eval 'ScratchPad << 1; :"\xC3"' + }.should raise_error(EncodingError, /invalid/) + ScratchPad.recorded.should == [] + 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..4e473b803f --- /dev/null +++ b/spec/ruby/language/undef_spec.rb @@ -0,0 +1,72 @@ +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 a interpolated symbol" do + @undef_class.class_eval do + undef :"#{'meth'}" + 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..699187335c --- /dev/null +++ b/spec/ruby/language/variables_spec.rb @@ -0,0 +1,853 @@ +require_relative '../spec_helper' +require_relative 'fixtures/variables' + +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 + a.should == [nil, 1, nil] + end + + it "assigns constants" do + module VariableSpecs + SINGLE_RHS_1, SINGLE_RHS_2 = 1 + [SINGLE_RHS_1, SINGLE_RHS_2].should == [1, nil] + end + 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 + + 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 + + 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 + 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 + 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 + 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 + # new feature in 2.6 -- https://bugs.ruby-lang.org/issues/13770 + it 'does not allow non-ASCII upcased characters at the beginning' do + -> do + eval <<-CODE + def test + ἍBB = 1 + end + CODE + end.should raise_error(SyntaxError, /dynamic constant assignment/) + end + + 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 +end + +describe "Instance variables" do + context "when instance variable is uninitialized" do + ruby_version_is ""..."3.0" do + it "warns about accessing uninitialized instance variable" do + obj = Object.new + def obj.foobar; a = @a; end + + -> { obj.foobar }.should complain(/warning: instance variable @a not initialized/, verbose: true) + end + end + + ruby_version_is "3.0" 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 + 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 +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..3db6d353a9 --- /dev/null +++ b/spec/ruby/language/yield_spec.rb @@ -0,0 +1,215 @@ +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 + ruby_version_is "2.7"..."3.0" do + it 'emits a deprecation warning' do + code = <<~RUBY + def m + class << Object.new + yield + end + end + m { :ok } + RUBY + + -> { eval(code) }.should complain(/warning: `yield' in class syntax will not be supported from Ruby 3.0/) + end + end + + ruby_version_is "3.0" do + it 'raises a SyntaxError' do + code = <<~RUBY + class << Object.new + yield + end + RUBY + + -> { eval(code) }.should raise_error(SyntaxError, /Invalid yield/) + end + end +end |
