1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/test/ruby/test_pattern_matching.rb
Jeremy Evans fa87f72e1e Add pattern matching pin support for instance/class/global variables
Pin matching for local variables and constants is already supported,
and it is fairly simple to add support for these variable types.

Note that pin matching for method calls is still not supported
without wrapping in parentheses (pin expressions).  I think that's
for the best as method calls are far more complex (arguments/blocks).

Implements [Feature #17724]
2021-07-15 09:56:02 -07:00

1553 lines
24 KiB
Ruby

# frozen_string_literal: true
require 'test/unit'
experimental, Warning[:experimental] = Warning[:experimental], false # suppress "warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!"
eval "\n#{<<~'END_of_GUARD'}", binding, __FILE__, __LINE__
class TestPatternMatching < Test::Unit::TestCase
class C
class << self
attr_accessor :keys
end
def initialize(obj)
@obj = obj
end
def deconstruct
@obj
end
def deconstruct_keys(keys)
C.keys = keys
@obj
end
end
def test_basic
assert_block do
case 0
in 0
true
else
false
end
end
assert_block do
case 0
in 1
false
else
true
end
end
assert_raise(NoMatchingPatternError) do
case 0
in 1
false
end
end
begin
o = [0]
case o
in 1
false
end
rescue => e
assert_match o.inspect, e.message
end
assert_block do
begin
true
ensure
case 0
in 0
false
end
end
end
assert_block do
begin
true
ensure
case 0
in 1
else
false
end
end
end
assert_raise(NoMatchingPatternError) do
begin
ensure
case 0
in 1
end
end
end
assert_block do
# suppress "warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!"
experimental, Warning[:experimental] = Warning[:experimental], false
eval(%q{
case true
in a
a
end
})
ensure
Warning[:experimental] = experimental
end
assert_block do
tap do |a|
tap do
case true
in a
a
end
end
end
end
assert_raise(NoMatchingPatternError) do
o = BasicObject.new
def o.match
case 0
in 1
end
end
o.match
end
end
def test_modifier
assert_block do
case 0
in a if a == 0
true
end
end
assert_block do
case 0
in a if a != 0
else
true
end
end
assert_block do
case 0
in a unless a != 0
true
end
end
assert_block do
case 0
in a unless a == 0
else
true
end
end
end
def test_as_pattern
assert_block do
case 0
in 0 => a
a == 0
end
end
end
def test_alternative_pattern
assert_block do
[0, 1].all? do |i|
case i
in 0 | 1
true
end
end
end
assert_block do
case 0
in _ | _a
true
end
end
assert_syntax_error(%q{
case 0
in a | 0
end
}, /illegal variable in alternative pattern/)
end
def test_var_pattern
# NODE_DASGN_CURR
assert_block do
case 0
in a
a == 0
end
end
# NODE_DASGN
b = 0
assert_block do
case 1
in b
b == 1
end
end
# NODE_LASGN
case 0
in c
assert_equal(0, c)
else
flunk
end
assert_syntax_error(%q{
case 0
in ^a
end
}, /no such local variable/)
assert_syntax_error(%q{
case 0
in a, a
end
}, /duplicated variable name/)
assert_block do
case [0, 1, 2, 3]
in _, _, _a, _a
true
end
end
assert_syntax_error(%q{
case 0
in a, {a:}
end
}, /duplicated variable name/)
assert_syntax_error(%q{
case 0
in a, {"a":}
end
}, /duplicated variable name/)
assert_block do
case [0, "1"]
in a, "#{case 1; in a; a; end}"
true
end
end
assert_syntax_error(%q{
case [0, "1"]
in a, "#{case 1; in a; a; end}", a
end
}, /duplicated variable name/)
assert_block do
case 0
in a
assert_equal(0, a)
true
in a
flunk
end
end
assert_syntax_error(%q{
0 => [a, a]
}, /duplicated variable name/)
end
def test_literal_value_pattern
assert_block do
case [nil, self, true, false]
in [nil, self, true, false]
true
end
end
assert_block do
case [0d170, 0D170, 0xaa, 0xAa, 0xAA, 0Xaa, 0XAa, 0XaA, 0252, 0o252, 0O252]
in [0d170, 0D170, 0xaa, 0xAa, 0xAA, 0Xaa, 0XAa, 0XaA, 0252, 0o252, 0O252]
true
end
case [0b10101010, 0B10101010, 12r, 12.3r, 1i, 12.3ri]
in [0b10101010, 0B10101010, 12r, 12.3r, 1i, 12.3ri]
true
end
end
assert_block do
x = 'x'
case ['a', 'a', x]
in ['a', "a", "#{x}"]
true
end
end
assert_block do
case ["a\n"]
in [<<END]
a
END
true
end
end
assert_block do
case [:a, :"a"]
in [:a, :"a"]
true
end
end
assert_block do
case [0, 1, 2, 3, 4, 5]
in [0..1, 0...2, 0.., 0..., (...5), (..5)]
true
end
end
assert_syntax_error(%q{
case 0
in a..b
end
}, /unexpected/)
assert_block do
case 'abc'
in /a/
true
end
end
assert_block do
case 0
in ->(i) { i == 0 }
true
end
end
assert_block do
case [%(a), %q(a), %Q(a), %w(a), %W(a), %i(a), %I(a), %s(a), %x(echo a), %(), %q(), %Q(), %w(), %W(), %i(), %I(), %s(), 'a']
in [%(a), %q(a), %Q(a), %w(a), %W(a), %i(a), %I(a), %s(a), %x(echo a), %(), %q(), %Q(), %w(), %W(), %i(), %I(), %s(), %r(a)]
true
end
end
assert_block do
case [__FILE__, __LINE__ + 1, __ENCODING__]
in [__FILE__, __LINE__, __ENCODING__]
true
end
end
end
def test_constant_value_pattern
assert_block do
case 0
in Integer
true
end
end
assert_block do
case 0
in Object::Integer
true
end
end
assert_block do
case 0
in ::Object::Integer
true
end
end
end
def test_pin_operator_value_pattern
assert_block do
a = /a/
case 'abc'
in ^a
true
end
end
assert_block do
case [0, 0]
in a, ^a
a == 0
end
end
assert_block do
@a = /a/
case 'abc'
in ^@a
true
end
end
assert_block do
@@TestPatternMatching = /a/
case 'abc'
in ^@@TestPatternMatching
true
end
end
assert_block do
$TestPatternMatching = /a/
case 'abc'
in ^$TestPatternMatching
true
end
end
end
def test_pin_operator_expr_pattern
assert_block do
case 'abc'
in ^(/a/)
true
end
end
assert_block do
case {name: '2.6', released_at: Time.new(2018, 12, 25)}
in {released_at: ^(Time.new(2010)..Time.new(2020))}
true
end
end
assert_block do
case 0
in ^(0+0)
true
end
end
end
def test_array_pattern
assert_block do
[[0], C.new([0])].all? do |i|
case i
in 0,;
true
end
end
end
assert_block do
[[0, 1], C.new([0, 1])].all? do |i|
case i
in 0,;
true
end
end
end
assert_block do
[[], C.new([])].all? do |i|
case i
in 0,;
else
true
end
end
end
assert_block do
[[0, 1], C.new([0, 1])].all? do |i|
case i
in 0, 1
true
end
end
end
assert_block do
[[0], C.new([0])].all? do |i|
case i
in 0, 1
else
true
end
end
end
assert_block do
[[], C.new([])].all? do |i|
case i
in *a
a == []
end
end
end
assert_block do
[[0], C.new([0])].all? do |i|
case i
in *a
a == [0]
end
end
end
assert_block do
[[0], C.new([0])].all? do |i|
case i
in *a, 0, 1
raise a # suppress "unused variable: a" warning
else
true
end
end
end
assert_block do
[[0, 1], C.new([0, 1])].all? do |i|
case i
in *a, 0, 1
a == []
end
end
end
assert_block do
[[0, 1, 2], C.new([0, 1, 2])].all? do |i|
case i
in *a, 1, 2
a == [0]
end
end
end
assert_block do
[[], C.new([])].all? do |i|
case i
in *;
true
end
end
end
assert_block do
[[0], C.new([0])].all? do |i|
case i
in *, 0, 1
else
true
end
end
end
assert_block do
[[0, 1], C.new([0, 1])].all? do |i|
case i
in *, 0, 1
true
end
end
end
assert_block do
[[0, 1, 2], C.new([0, 1, 2])].all? do |i|
case i
in *, 1, 2
true
end
end
end
assert_block do
case C.new([0])
in C(0)
true
end
end
assert_block do
case C.new([0])
in Array(0)
else
true
end
end
assert_block do
case C.new([])
in C()
true
end
end
assert_block do
case C.new([])
in Array()
else
true
end
end
assert_block do
case C.new([0])
in C[0]
true
end
end
assert_block do
case C.new([0])
in Array[0]
else
true
end
end
assert_block do
case C.new([])
in C[]
true
end
end
assert_block do
case C.new([])
in Array[]
else
true
end
end
assert_block do
case []
in []
true
end
end
assert_block do
case C.new([])
in []
true
end
end
assert_block do
case [0]
in [0]
true
end
end
assert_block do
case C.new([0])
in [0]
true
end
end
assert_block do
case [0]
in [0,]
true
end
end
assert_block do
case [0, 1]
in [0,]
true
end
end
assert_block do
case []
in [0, *a]
raise a # suppress "unused variable: a" warning
else
true
end
end
assert_block do
case [0]
in [0, *a]
a == []
end
end
assert_block do
case [0]
in [0, *a, 1]
raise a # suppress "unused variable: a" warning
else
true
end
end
assert_block do
case [0, 1]
in [0, *a, 1]
a == []
end
end
assert_block do
case [0, 1, 2]
in [0, *a, 2]
a == [1]
end
end
assert_block do
case []
in [0, *]
else
true
end
end
assert_block do
case [0]
in [0, *]
true
end
end
assert_block do
case [0, 1]
in [0, *]
true
end
end
assert_block do
case []
in [0, *a]
raise a # suppress "unused variable: a" warning
else
true
end
end
assert_block do
case [0]
in [0, *a]
a == []
end
end
assert_block do
case [0, 1]
in [0, *a]
a == [1]
end
end
assert_block do
case [0]
in [0, *, 1]
else
true
end
end
assert_block do
case [0, 1]
in [0, *, 1]
true
end
end
end
def test_find_pattern
[0, 1, 2] => [*, 1 => a, *]
assert_equal(1, a)
[0, 1, 2] => [*a, 1 => b, *c]
assert_equal([0], a)
assert_equal(1, b)
assert_equal([2], c)
assert_block do
case [0, 1, 2]
in [*, 9, *]
false
else
true
end
end
assert_block do
case [0, 1, 2]
in [*, Integer, String, *]
false
else
true
end
end
[0, 1, 2] => [*a, 1 => b, 2 => c, *d]
assert_equal([0], a)
assert_equal(1, b)
assert_equal(2, c)
assert_equal([], d)
case [0, 1, 2]
in *, 1 => a, *;
assert_equal(1, a)
end
assert_block do
case [0, 1, 2]
in String(*, 1, *)
false
in Array(*, 1, *)
true
end
end
assert_block do
case [0, 1, 2]
in String[*, 1, *]
false
in Array[*, 1, *]
true
end
end
# https://bugs.ruby-lang.org/issues/17534
assert_block do
case [0, 1, 2]
in x
x = x # avoid a warning "assigned but unused variable - x"
true
in [*, 2, *]
false
end
end
end
def test_hash_pattern
assert_block do
[{}, C.new({})].all? do |i|
case i
in a: 0
else
true
end
end
end
assert_block do
[{a: 0}, C.new({a: 0})].all? do |i|
case i
in a: 0
true
end
end
end
assert_block do
[{a: 0, b: 1}, C.new({a: 0, b: 1})].all? do |i|
case i
in a: 0
true
end
end
end
assert_block do
[{a: 0}, C.new({a: 0})].all? do |i|
case i
in a: 0, b: 1
else
true
end
end
end
assert_block do
[{a: 0, b: 1}, C.new({a: 0, b: 1})].all? do |i|
case i
in a: 0, b: 1
true
end
end
end
assert_block do
[{a: 0, b: 1, c: 2}, C.new({a: 0, b: 1, c: 2})].all? do |i|
case i
in a: 0, b: 1
true
end
end
end
assert_block do
[{}, C.new({})].all? do |i|
case i
in a:
raise a # suppress "unused variable: a" warning
else
true
end
end
end
assert_block do
[{a: 0}, C.new({a: 0})].all? do |i|
case i
in a:
a == 0
end
end
end
assert_block do
[{a: 0, b: 1}, C.new({a: 0, b: 1})].all? do |i|
case i
in a:
a == 0
end
end
end
assert_block do
[{a: 0}, C.new({a: 0})].all? do |i|
case i
in "a": 0
true
end
end
end
assert_block do
[{a: 0}, C.new({a: 0})].all? do |i|
case i
in "a":;
a == 0
end
end
end
assert_block do
[{}, C.new({})].all? do |i|
case i
in **a
a == {}
end
end
end
assert_block do
[{a: 0}, C.new({a: 0})].all? do |i|
case i
in **a
a == {a: 0}
end
end
end
assert_block do
[{}, C.new({})].all? do |i|
case i
in **;
true
end
end
end
assert_block do
[{a: 0}, C.new({a: 0})].all? do |i|
case i
in **;
true
end
end
end
assert_block do
[{}, C.new({})].all? do |i|
case i
in a:, **b
raise a # suppress "unused variable: a" warning
raise b # suppress "unused variable: b" warning
else
true
end
end
end
assert_block do
[{a: 0}, C.new({a: 0})].all? do |i|
case i
in a:, **b
a == 0 && b == {}
end
end
end
assert_block do
[{a: 0, b: 1}, C.new({a: 0, b: 1})].all? do |i|
case i
in a:, **b
a == 0 && b == {b: 1}
end
end
end
assert_block do
[{}, C.new({})].all? do |i|
case i
in **nil
true
end
end
end
assert_block do
[{a: 0}, C.new({a: 0})].all? do |i|
case i
in **nil
else
true
end
end
end
assert_block do
[{a: 0}, C.new({a: 0})].all? do |i|
case i
in a:, **nil
assert_equal(0, a)
true
end
end
end
assert_block do
[{a: 0, b: 1}, C.new({a: 0, b: 1})].all? do |i|
case i
in a:, **nil
assert_equal(0, a)
else
true
end
end
end
assert_block do
case C.new({a: 0})
in C(a: 0)
true
end
end
assert_block do
case {a: 0}
in C(a: 0)
else
true
end
end
assert_block do
case C.new({a: 0})
in C[a: 0]
true
end
end
assert_block do
case {a: 0}
in C[a: 0]
else
true
end
end
assert_block do
[{}, C.new({})].all? do |i|
case i
in {a: 0}
else
true
end
end
end
assert_block do
[{a: 0}, C.new({a: 0})].all? do |i|
case i
in {a: 0}
true
end
end
end
assert_block do
[{a: 0, b: 1}, C.new({a: 0, b: 1})].all? do |i|
case i
in {a: 0}
true
end
end
end
assert_block do
[{}, C.new({})].all? do |i|
case i
in {}
true
end
end
end
assert_block do
[{a: 0}, C.new({a: 0})].all? do |i|
case i
in {}
else
true
end
end
end
assert_syntax_error(%q{
case _
in a:, a:
end
}, /duplicated key name/)
assert_syntax_error(%q{
case _
in a?:
end
}, /key must be valid as local variables/)
assert_block do
case {a?: true}
in a?: true
true
end
end
assert_block do
case {a: 0, b: 1}
in {a: 1,}
false
in {a:,}
_a = a
true
end
end
assert_block do
case {a: 0}
in {a: 1
}
false
in {a:
2}
false
in a: {b:}, c:
_b = b
p c
in {a:
}
_a = a
true
end
end
assert_syntax_error(%q{
case _
in "a-b":
end
}, /key must be valid as local variables/)
assert_block do
case {"a-b": true}
in "a-b": true
true
end
end
assert_syntax_error(%q{
case _
in "#{a}": a
end
}, /symbol literal with interpolation is not allowed/)
assert_syntax_error(%q{
case _
in "#{a}":
end
}, /symbol literal with interpolation is not allowed/)
end
def test_paren
assert_block do
case 0
in (0)
true
end
end
end
def test_nomatchingpatternerror
assert_equal(StandardError, NoMatchingPatternError.superclass)
end
def test_invalid_syntax
assert_syntax_error(%q{
case 0
in a, b:
end
}, /unexpected/)
assert_syntax_error(%q{
case 0
in [a:]
end
}, /unexpected/)
assert_syntax_error(%q{
case 0
in {a}
end
}, /unexpected/)
assert_syntax_error(%q{
case 0
in {0 => a}
end
}, /unexpected/)
end
################################################################
class CTypeError
def deconstruct
nil
end
def deconstruct_keys(keys)
nil
end
end
def test_deconstruct
assert_raise(TypeError) do
case CTypeError.new
in []
end
end
end
def test_deconstruct_keys
assert_raise(TypeError) do
case CTypeError.new
in {}
end
end
assert_block do
case {}
in {}
C.keys == nil
end
end
assert_block do
case C.new({a: 0, b: 0, c: 0})
in {a: 0, b:}
assert_equal(0, b)
C.keys == [:a, :b]
end
end
assert_block do
case C.new({a: 0, b: 0, c: 0})
in {a: 0, b:, **}
assert_equal(0, b)
C.keys == [:a, :b]
end
end
assert_block do
case C.new({a: 0, b: 0, c: 0})
in {a: 0, b:, **r}
assert_equal(0, b)
assert_equal({c: 0}, r)
C.keys == nil
end
end
assert_block do
case C.new({a: 0, b: 0, c: 0})
in {**}
C.keys == []
end
end
assert_block do
case C.new({a: 0, b: 0, c: 0})
in {**r}
assert_equal({a: 0, b: 0, c: 0}, r)
C.keys == nil
end
end
end
################################################################
class CDeconstructCache
def initialize(v)
@v = v
end
def deconstruct
@v.shift
end
end
def test_deconstruct_cache
assert_block do
case CDeconstructCache.new([[0]])
in [1]
in [0]
true
end
end
assert_block do
case CDeconstructCache.new([[0, 1]])
in [1,]
in [0,]
true
end
end
assert_block do
case CDeconstructCache.new([[[0]]])
in [[1]]
in [[*a]]
a == [0]
end
end
assert_block do
case CDeconstructCache.new([[0]])
in [x] if x > 0
in [0]
true
end
end
assert_block do
case CDeconstructCache.new([[0]])
in []
in [1] | [0]
true
end
end
assert_block do
case CDeconstructCache.new([[0]])
in [1] => _
in [0] => _
true
end
end
assert_block do
case CDeconstructCache.new([[0]])
in C[0]
in CDeconstructCache[0]
true
end
end
assert_block do
case [CDeconstructCache.new([[0], [1]])]
in [[1]]
false
in [[1]]
true
end
end
assert_block do
case CDeconstructCache.new([[0, :a, 1]])
in [*, String => x, *]
false
in [*, Symbol => x, *]
x == :a
end
end
end
################################################################
class TestPatternMatchingRefinements < Test::Unit::TestCase
class C1
def deconstruct
[:C1]
end
end
class C2
end
module M
refine Array do
def deconstruct
[0]
end
end
refine Hash do
def deconstruct_keys(_)
{a: 0}
end
end
refine C2.singleton_class do
def ===(obj)
obj.kind_of?(C1)
end
end
end
using M
def test_refinements
assert_block do
case []
in [0]
true
end
end
assert_block do
case {}
in {a: 0}
true
end
end
assert_block do
case C1.new
in C2(:C1)
true
end
end
end
end
################################################################
def test_struct
assert_block do
s = Struct.new(:a, :b)
case s[0, 1]
in 0, 1
true
end
end
s = Struct.new(:a, :b, keyword_init: true)
assert_block do
case s[a: 0, b: 1]
in **r
r == {a: 0, b: 1}
end
end
assert_block do
s = Struct.new(:a, :b, keyword_init: true)
case s[a: 0, b: 1]
in a:, b:
a == 0 && b == 1
end
end
assert_block do
s = Struct.new(:a, :b, keyword_init: true)
case s[a: 0, b: 1]
in a:, c:
raise a # suppress "unused variable: a" warning
raise c # suppress "unused variable: c" warning
flunk
in a:, b:, c:
flunk
in b:
b == 1
end
end
end
################################################################
def test_one_line
1 => a
assert_equal 1, a
assert_raise(NoMatchingPatternError) do
{a: 1} => {a: 0}
end
assert_syntax_error("if {} => {a:}; end", /void value expression/)
assert_syntax_error(%q{
1 => a, b
}, /unexpected/, '[ruby-core:95098]')
assert_syntax_error(%q{
1 => a:
}, /unexpected/, '[ruby-core:95098]')
assert_equal true, (1 in 1)
assert_equal false, (1 in 2)
end
def assert_experimental_warning(code)
w = Warning[:experimental]
Warning[:experimental] = false
assert_warn('') {eval(code)}
Warning[:experimental] = true
assert_warn(/is experimental/) {eval(code)}
ensure
Warning[:experimental] = w
end
def test_experimental_warning
assert_experimental_warning("case [0]; in [*, 0, *]; end")
assert_experimental_warning("0 => 0")
assert_experimental_warning("0 in a")
end
end
END_of_GUARD
Warning[:experimental] = experimental