require_relative '../spec_helper' describe "Pattern matching" do # TODO: Remove excessive eval calls when Ruby 3 is the minimum version. # It is best to keep the eval's longer if other Ruby impls cannot parse pattern matching yet. 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 it "can be nested" do eval(<<~RUBY).should == [[0, [2, 4, 6]], [[4, 16, 64]], 27] case [0, [2, 4, 6], [3, 9, 27], [4, 16, 64]] in [*pre, [*, 9, a], *post] [pre, post, a] else false end RUBY end it "can be nested with an array pattern" do eval(<<~RUBY).should == [[4, 16, 64]] case [0, [2, 4, 6], [3, 9, 27], [4, 16, 64]] in [_, _, [*, 9, *], *post] post else false end RUBY end it "can be nested within a hash pattern" do eval(<<~RUBY).should == [27] case {a: [3, 9, 27]} in {a: [*, 9, *post]} post else false end RUBY end it "can nest hash and array patterns" do eval(<<~RUBY).should == [42, 2] case [0, {a: 42, b: [0, 1]}, {a: 42, b: [1, 2]}] in [*, {a:, b: [1, c]}, *] [a, c] 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 it "supports pinning instance variables" do eval(<<~RUBY).should == true @a = /a/ case 'abc' in ^@a true end RUBY end it "supports pinning class variables" do result = nil Module.new do result = module_eval(<<~RUBY) @@a = 0..10 case 2 in ^@@a true end RUBY end result.should == true end it "supports pinning global variables" do eval(<<~RUBY).should == true $a = /a/ case 'abc' in ^$a true end RUBY end it "supports pinning expressions" do eval(<<~RUBY).should == true case 'abc' in ^(/a/) true end RUBY eval(<<~RUBY).should == true case {name: '2.6', released_at: Time.new(2018, 12, 25)} in {released_at: ^(Time.new(2010)..Time.new(2020))} true end RUBY eval(<<~RUBY).should == true case 0 in ^(0+0) true end RUBY end end end