diff --git a/language/pattern_matching_spec.rb b/language/pattern_matching_spec.rb index 62ecf07f9b..37985186f5 100644 --- a/language/pattern_matching_spec.rb +++ b/language/pattern_matching_spec.rb @@ -2,390 +2,477 @@ ruby_version_is "2.7" do describe "Pattern matching" do + # TODO: Remove excessive eval calls when support of previous version + # Ruby 2.6 will be dropped + before do ScratchPad.record [] end it "can be standalone in operator that deconstructs value" do - [0, 1] in [a, b] - - a.should == 0 - b.should == 1 + eval(<<-RUBY).should == [0, 1] + [0, 1] in [a, b] + [a, b] + RUBY end it "extends case expression with case/in construction" do - case [0, 1] - in [0] - :foo - in [0, 1] - :bar - end.should == :bar + eval(<<~RUBY).should == :bar + case [0, 1] + in [0] + :foo + in [0, 1] + :bar + end + RUBY end it "allows using then operator" do - case [0, 1] - in [0] then :foo - in [0, 1] then :bar - end.should == :bar + eval(<<~RUBY).should == :bar + case [0, 1] + in [0] then :foo + in [0, 1] then :bar + end + RUBY end it "warns about pattern matching is experimental feature" do -> { - eval <<~CODE + eval <<~RUBY case 0 in 0 end - CODE + RUBY }.should complain(/warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!/) end it "binds variables" do - case [0, 1] - in [0, a] - a - end.should == 1 + eval(<<~RUBY).should == 1 + case [0, 1] + in [0, a] + a + end + RUBY end it "cannot mix in and when operators" do -> { - eval <<~CODE + eval <<~RUBY case [] when 1 == 1 in [] end - CODE + RUBY }.should raise_error(SyntaxError, /syntax error, unexpected `in'/) -> { - eval <<~CODE + eval <<~RUBY case [] in [] when 1 == 1 end - CODE + RUBY }.should raise_error(SyntaxError, /syntax error, unexpected `when'/) end it "checks patterns until the first matching" do - case [0, 1] - in [0] - :foo - in [0, 1] - :bar - in [0, 1] - :baz - end.should == :bar + 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 - case [0, 1] - in [0] - true - else - false - end.should == false + 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 -> { - case [0, 1] - in [0] - end + eval <<~RUBY + case [0, 1] + in [0] + end + RUBY }.should raise_error(NoMatchingPatternError, /\[0, 1\]/) end describe "guards" do it "supports if guard" do - case 0 - in 0 if false - true - else - false - end.should == false + eval(<<~RUBY).should == false + case 0 + in 0 if false + true + else + false + end + RUBY - case 0 - in 0 if true - true - else - false - end.should == true + eval(<<~RUBY).should == true + case 0 + in 0 if true + true + else + false + end + RUBY end it "supports unless guard" do - case 0 - in 0 unless true - true - else - false - end.should == false + eval(<<~RUBY).should == false + case 0 + in 0 unless true + true + else + false + end + RUBY - case 0 - in 0 unless false - true - else - false - end.should == true + eval(<<~RUBY).should == true + case 0 + in 0 unless false + true + else + false + end + RUBY end it "makes bound variables visible in guard" do - case [0, 1] - in [a, 1] if a >= 0 - true - end.should == true + 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 - case 0 - in 1 if (ScratchPad << :foo) || true - else - end + 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 - case 0 - in 0 if false - :foo - in 0 if true - :bar - end.should == :bar + 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 - case 0 - in 0 if false - true - else - false - end.should == false + 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 -> { - case [0, 1] - in [0, 1] if false - end + 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 - case 0 - in 0 - true - end.should == true + eval(<<~RUBY).should == true + case 0 + in 0 + true + end + RUBY - case 0 - in (-1..1) - true - end.should == true + eval(<<~RUBY).should == true + case 0 + in (-1..1) + true + end + RUBY - case 0 - in Integer - true - end.should == true + eval(<<~RUBY).should == true + case 0 + in Integer + true + end + RUBY - case "0" - in /0/ - true - end.should == true + eval(<<~RUBY).should == true + case "0" + in /0/ + true + end + RUBY - case "0" - in ->(s) { s == "0" } - true - end.should == true + eval(<<~RUBY).should == true + case "0" + in ->(s) { s == "0" } + true + end + RUBY end it "allows string literal with interpolation" do x = "x" - case "x" - in "#{x + ""}" - true - end.should == true + 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 - case 0 - in a - a - end.should == 0 + eval(<<~RUBY).should == 0 + case 0 + in a + a + end + RUBY end it "makes bounded variable visible outside a case statement scope" do - case 0 - in a - end + eval(<<~RUBY).should == 0 + case 0 + in a + end - a.should == 0 + a + RUBY end it "allow using _ name to drop values" do - case [0, 1] - in [a, _] - a - end.should == 0 + eval(<<~RUBY).should == 0 + case [0, 1] + in [a, _] + a + end + RUBY end it "supports using _ in a pattern several times" do - case [0, 1, 2] - in [0, _, _] - _ - end.should == 2 + eval(<<~RUBY).should == 2 + case [0, 1, 2] + in [0, _, _] + _ + end + RUBY end it "supports using any name with _ at the beginning in a pattern several times" do - case [0, 1, 2] - in [0, _x, _x] - _x - end.should == 2 + eval(<<~RUBY).should == 2 + case [0, 1, 2] + in [0, _x, _x] + _x + end + RUBY - case {a: 0, b: 1, c: 2} - in {a: 0, b: _x, c: _x} - _x - end.should == 2 + eval(<<~RUBY).should == 2 + case {a: 0, b: 1, c: 2} + in {a: 0, b: _x, c: _x} + _x + end + RUBY end it "does not support using variable name (except _) several times" do -> { - eval <<~CODE + eval <<~RUBY case [0] in [a, a] end - CODE + RUBY }.should raise_error(SyntaxError, /duplicated variable name/) end it "supports existing variables in a pattern specified with ^ operator" do a = 0 - case 0 - in ^a - true - end.should == true + eval(<<~RUBY).should == true + case 0 + in ^a + true + end + RUBY end it "allows applying ^ operator to bound variables" do - case [1, 1] - in [n, ^n] - n - end.should == 1 + eval(<<~RUBY).should == 1 + case [1, 1] + in [n, ^n] + n + end + RUBY - case [1, 2] - in [n, ^n] - true - else - false - end.should == false + eval(<<~RUBY).should == false + case [1, 2] + in [n, ^n] + true + else + false + end + RUBY end end describe "alternative pattern" do it "matches if any of patterns matches" do - case 0 - in 0 | 1 | 2 - true - end.should == true + eval(<<~RUBY).should == true + case 0 + in 0 | 1 | 2 + true + end + RUBY end it "does not support variable binding" do -> { - eval <<~CODE + eval <<~RUBY case [0, 1] in [0, 0] | [0, a] end - CODE + RUBY }.should raise_error(SyntaxError, /illegal variable in alternative pattern/) end end describe "AS pattern" do it "binds a variable to a value if pattern matches" do - case 0 - in Integer => n - n - end.should == 0 + eval(<<~RUBY).should == 0 + case 0 + in Integer => n + n + end + RUBY end it "can be used as a nested pattern" do - case [1, [2, 3]] - in [1, Array => ary] - ary - end.should == [2, 3] + 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 - case [0, 1, 2] - in Array(0, 1, 2) - true - end.should == true + eval(<<~RUBY).should == true + case [0, 1, 2] + in Array(0, 1, 2) + true + end + RUBY end it "supports form Constant[pat, pat, ...]" do - case [0, 1, 2] - in Array[0, 1, 2] - true - end.should == true + eval(<<~RUBY).should == true + case [0, 1, 2] + in Array[0, 1, 2] + true + end + RUBY end it "supports form [pat, pat, ...]" do - case [0, 1, 2] - in [0, 1, 2] - true - end.should == true + eval(<<~RUBY).should == true + case [0, 1, 2] + in [0, 1, 2] + true + end + RUBY end it "supports form pat, pat, ..." do - case [0, 1, 2] - in 0, 1, 2 then - true - end.should == true + eval(<<~RUBY).should == true + case [0, 1, 2] + in 0, 1, 2 + true + end + RUBY - case [0, 1, 2] - in 0, a, 2 - a - end.should == 1 + eval(<<~RUBY).should == 1 + case [0, 1, 2] + in 0, a, 2 + a + end + RUBY - case [0, 1, 2] - in 0, *rest - rest - end.should == [1, 2] + 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 - case obj - in [Integer, Integer] - true - end.should == true + eval(<<~RUBY).should == true + case obj + in [Integer, Integer] + true + end + RUBY end it "does not match object if Constant === object returns false" do - case [0, 1, 2] - in String[0, 1, 2] - true - else - false - end.should == false + 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 - case obj - in Object[] - true - else - false - end.should == false + 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 @@ -393,10 +480,12 @@ def obj.deconstruct; [0, 1] end def obj.deconstruct; "" end -> { - case obj - in Object[] - else - end + eval <<~RUBY + case obj + in Object[] + else + end + RUBY }.should raise_error(TypeError, /deconstruct must return Array/) end @@ -404,152 +493,192 @@ def obj.deconstruct; "" end obj = Object.new def obj.deconstruct; [1] end - case obj - in Object[0] - true - else - false - end.should == false + eval(<<~RUBY).should == false + case obj + in Object[0] + true + else + false + end + RUBY end it "binds variables" do - case [0, 1, 2] - in [a, b, c] - [a, b, c] - end.should == [0, 1, 2] + eval(<<~RUBY).should == [0, 1, 2] + case [0, 1, 2] + in [a, b, c] + [a, b, c] + end + RUBY end it "binds variable even if patter matches only partially" do a = nil - case [0, 1, 2] - in [a, 1, 3] - else - end + eval(<<~RUBY).should == 0 + case [0, 1, 2] + in [a, 1, 3] + else + end - a.should == 0 + a + RUBY end it "supports splat operator *rest" do - case [0, 1, 2] - in [0, *rest] - rest - end.should == [1, 2] + eval(<<~RUBY).should == [1, 2] + case [0, 1, 2] + in [0, *rest] + rest + end + RUBY end it "does not match partially by default" do - case [0, 1, 2, 3] - in [1, 2] - true - else - false - end.should == false + 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 - case [0, 1, 2, 3] - in [0, 1,] - true - end.should == true + eval(<<~RUBY).should == true + case [0, 1, 2, 3] + in [0, 1,] + true + end + RUBY - case [0, 1, 2, 3] - in 0, 1,; - true - end.should == true + eval(<<~RUBY).should == true + case [0, 1, 2, 3] + in 0, 1,; + true + end + RUBY end it "matches [] with []" do - case [] - in [] - true - end.should == true + eval(<<~RUBY).should == true + case [] + in [] + true + end + RUBY end end describe "Hash pattern" do it "supports form Constant(id: pat, id: pat, ...)" do - case {a: 0, b: 1} - in Hash(a: 0, b: 1) - true - end.should == true + 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 - case {a: 0, b: 1} - in Hash[a: 0, b: 1] - true - end.should == true + 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 - case {a: 0, b: 1} - in {a: 0, b: 1} - true - end.should == true + 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 - case {a: 0, b: 1} - in a: 0, b: 1 - true - end.should == true + eval(<<~RUBY).should == true + case {a: 0, b: 1} + in a: 0, b: 1 + true + end + RUBY - case {a: 0, b: 1} - in a: a, b: b - [a, b] - end.should == [0, 1] + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in a: a, b: b + [a, b] + end + RUBY - case {a: 0, b: 1, c: 2} - in a: 0, **rest - rest - end.should == { b: 1, c: 2 } + 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 - case {a: 0, b: 1} - in Hash(a:, b:) - [a, b] - end.should == [0, 1] + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in Hash(a:, b:) + [a, b] + end + RUBY a = b = nil - case {a: 0, b: 1} - in Hash[a:, b:] - [a, b] - end.should == [0, 1] + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in Hash[a:, b:] + [a, b] + end + RUBY a = b = nil - case {a: 0, b: 1} - in {a:, b:} - [a, b] - end.should == [0, 1] + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in {a:, b:} + [a, b] + end + RUBY a = nil - case {a: 0, b: 1, c: 2} - in {a:, **rest} - [a, rest] - end.should == [0, {b: 1, c: 2}] + 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 - case {a: 0, b: 1} - in a:, b: - [a, b] - end.should == [0, 1] + eval(<<~RUBY).should == [0, 1] + case {a: 0, b: 1} + in a:, b: + [a, b] + end + RUBY end it 'supports "str": key literal' do - case {a: 0} - in {"a": 0} - true - end.should == true + eval(<<~RUBY).should == true + case {a: 0} + in {"a": 0} + true + end + RUBY end it "does not support non-symbol keys" do -> { - eval <<~CODE + eval <<~RUBY case {a: 1} in {"a" => 1} end - CODE + RUBY }.should raise_error(SyntaxError, /unexpected/) end @@ -557,21 +686,21 @@ def obj.deconstruct; [1] end x = "a" -> { - eval <<~'CODE' + eval <<~'RUBY' case {a: 1} in {"#{x}": 1} end - CODE + RUBY }.should raise_error(SyntaxError, /symbol literal with interpolation is not allowed/) end it "raise SyntaxError when keys duplicate in pattern" do -> { - eval <<~CODE + eval <<~RUBY case {a: 1} in {a: 1, b: 2, a: 3} end - CODE + RUBY }.should raise_error(SyntaxError, /duplicated key name/) end @@ -579,30 +708,36 @@ def obj.deconstruct; [1] end obj = Object.new def obj.deconstruct_keys(*); {a: 1} end - case obj - in {a: 1} - true - end.should == true + eval(<<~RUBY).should == true + case obj + in {a: 1} + true + end + RUBY end it "does not match object if Constant === object returns false" do - case {a: 1} - in String[a: 1] - true - else - false - end.should == false + 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 - case obj - in Object[a: 1] - true - else - false - end.should == false + 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 @@ -610,9 +745,11 @@ def obj.deconstruct_keys(*); {a: 1} end def obj.deconstruct_keys(*); "" end -> { - case obj - in Object[a: 1] - end + eval <<~RUBY + case obj + in Object[a: 1] + end + RUBY }.should raise_error(TypeError, /deconstruct_keys must return Hash/) end @@ -620,24 +757,28 @@ def obj.deconstruct_keys(*); "" end obj = Object.new def obj.deconstruct_keys(*); {"a" => 1} end - case obj - in Object[a: 1] - true - else - false - end.should == false + 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 - case obj - in Object[a: 2] - true - else - false - end.should == false + 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 @@ -648,9 +789,11 @@ def obj.deconstruct_keys(*args) {a: 1, b: 2, c: 3} end - case obj - in Object[a: 1, b: 2, c: 3] - end + eval <<~RUBY + case obj + in Object[a: 1, b: 2, c: 3] + end + RUBY ScratchPad.recorded.should == [[[:a, :b, :c]]] end @@ -663,9 +806,11 @@ def obj.deconstruct_keys(*args) {a: 1, b: 2, c: 3} end - case obj - in Object[a: 1, b: 2, **] - end + eval <<~RUBY + case obj + in Object[a: 1, b: 2, **] + end + RUBY ScratchPad.recorded.should == [[[:a, :b]]] end @@ -678,64 +823,80 @@ def obj.deconstruct_keys(*args) {a: 1, b: 2} end - case obj - in Object[a: 1, **rest] - end + eval <<~RUBY + case obj + in Object[a: 1, **rest] + end + RUBY ScratchPad.recorded.should == [[nil]] end it "binds variables" do - case {a: 0, b: 1, c: 2} - in {a: x, b: y, c: z} - [x, y, z] - end.should == [0, 1, 2] + 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 "binds variable even if patter matches only partially" do + it "binds variable even if pattern matches only partially" do x = nil - case {a: 0, b: 1} - in {a: x, b: 2} - else - end + eval(<<~RUBY).should == 0 + case {a: 0, b: 1} + in {a: x, b: 2} + else + end - x.should == 0 + x + RUBY end it "supports double splat operator **rest" do - case {a: 0, b: 1, c: 2} - in {a: 0, **rest} - rest - end.should == {b: 1, c: 2} + 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 - case {a: 1, b: 2} - in {a: 1, b: 2, **nil} - true - end.should == true + eval(<<~RUBY).should == true + case {a: 1, b: 2} + in {a: 1, b: 2, **nil} + true + end + RUBY - case {a: 1, b: 2} - in {a: 1, **nil} - true - else - false - end.should == false + eval(<<~RUBY).should == false + case {a: 1, b: 2} + in {a: 1, **nil} + true + else + false + end + RUBY end it "can match partially" do - case {a: 1, b: 2} - in {a: 1} - true - end.should == true + eval(<<~RUBY).should == true + case {a: 1, b: 2} + in {a: 1} + true + end + RUBY end it "matches {} with {}" do - case {} - in {} - true - end.should == true + eval(<<~RUBY).should == true + case {} + in {} + true + end + RUBY end end end