diff --git a/core/hash.rbs b/core/hash.rbs index c5741fd30..76152a112 100644 --- a/core/hash.rbs +++ b/core/hash.rbs @@ -499,6 +499,12 @@ class Hash[unchecked out K, unchecked out V] < Object include Enumerable[[ K, V ]] + interface _Key + def hash: () -> Integer + + def eql?: (untyped rhs) -> boolish + end + # + # Returns the member names of the Struct descendant as an array: + # + # Customer = Struct.new(:name, :address, :zip) + # Customer.members # => [:name, :address, :zip] + # + def self.members: () -> Array[Symbol] + + # + # Returns `true` if the class was initialized with `keyword_init: true`. + # Otherwise returns `nil` or `false`. + # + # Examples: + # Foo = Struct.new(:a) + # Foo.keyword_init? # => nil + # Bar = Struct.new(:a, keyword_init: true) + # Bar.keyword_init? # => true + # Baz = Struct.new(:a, keyword_init: false) + # Baz.keyword_init? # => false + # + def self.keyword_init?: () -> bool? + + # + # Returns `true` if and only if the following are true; otherwise returns + # `false`: + # + # * `other.class == self.class`. + # * For each member name `name`, `other.name == self.name`. + # + # + # Examples: + # + # Customer = Struct.new(:name, :address, :zip) + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe_jr = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe_jr == joe # => true + # joe_jr[:name] = 'Joe Smith, Jr.' + # # => "Joe Smith, Jr." + # joe_jr == joe # => false + # + def ==: (untyped other) -> bool + + # + # Returns `true` if and only if the following are true; otherwise returns + # `false`: + # + # * `other.class == self.class`. + # * For each member name `name`, `other.name.eql?(self.name)`. + # + # Customer = Struct.new(:name, :address, :zip) + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe_jr = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe_jr.eql?(joe) # => true + # joe_jr[:name] = 'Joe Smith, Jr.' + # joe_jr.eql?(joe) # => false + # + # + # Related: Object#==. + # + def eql?: (untyped other) -> bool + + # + # Returns the integer hash value for `self`. + # + # Two structs of the same class and with the same content will have the same + # hash code (and will compare using Struct#eql?): + # + # Customer = Struct.new(:name, :address, :zip) + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe_jr = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe.hash == joe_jr.hash # => true + # joe_jr[:name] = 'Joe Smith, Jr.' + # joe.hash == joe_jr.hash # => false + # + # Related: Object#hash. + # + def hash: () -> Integer + + # + # Returns a string representation of `self`: + # + # Customer = Struct.new(:name, :address, :zip) # => Customer + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe.inspect # => "#" + # + # Struct#to_s is an alias for Struct#inspect. + # + def inspect: () -> String + + # + # Returns a string representation of `self`: + # + # Customer = Struct.new(:name, :address, :zip) # => Customer + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe.inspect # => "#" + # + # Struct#to_s is an alias for Struct#inspect. + # + alias to_s inspect + + # + # Returns the values in `self` as an array: + # + # Customer = Struct.new(:name, :address, :zip) + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe.to_a # => ["Joe Smith", "123 Maple, Anytown NC", 12345] + # + # Struct#values and Struct#deconstruct are aliases for Struct#to_a. + # + # Related: #members. + # + def to_a: () -> Array[Elem] + + # + # Returns a hash containing the name and value for each member: + # + # Customer = Struct.new(:name, :address, :zip) + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # h = joe.to_h + # h # => {:name=>"Joe Smith", :address=>"123 Maple, Anytown NC", :zip=>12345} + # + # If a block is given, it is called with each name/value pair; the block should + # return a 2-element array whose elements will become a key/value pair in the + # returned hash: + # + # h = joe.to_h{|name, value| [name.upcase, value.to_s.upcase]} + # h # => {:NAME=>"JOE SMITH", :ADDRESS=>"123 MAPLE, ANYTOWN NC", :ZIP=>"12345"} + # + # Raises ArgumentError if the block returns an inappropriate value. + # + def to_h: () -> Hash[Symbol, Elem] + | [K, V] () { (Symbol key, Elem value) -> [K, V] } -> Hash[K, V] + + # + # Returns the values in `self` as an array: + # + # Customer = Struct.new(:name, :address, :zip) + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe.to_a # => ["Joe Smith", "123 Maple, Anytown NC", 12345] + # + # Struct#values and Struct#deconstruct are aliases for Struct#to_a. + # + # Related: #members. + # + alias values to_a + + # + # Returns the number of members. + # + # Customer = Struct.new(:name, :address, :zip) + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe.size #=> 3 + # + # Struct#length is an alias for Struct#size. + # + def size: () -> Integer + + # + # Returns the number of members. + # + # Customer = Struct.new(:name, :address, :zip) + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe.size #=> 3 + # + # Struct#length is an alias for Struct#size. + # + alias length size # - # Returns the member names of the Struct descendant as an array: + # Calls the given block with each member name/value pair; returns `self`: + # + # Customer = Struct.new(:name, :address, :zip) # => Customer + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe.each_pair {|(name, value)| p "#{name} => #{value}" } + # + # Output: + # + # "name => Joe Smith" + # "address => 123 Maple, Anytown NC" + # "zip => 12345" + # + # Returns an Enumerator if no block is given. + # + # Related: #each. + # + def each_pair: () -> Enumerator[[Symbol, Elem], self] + | () { ([Symbol, Elem] key_value) -> void } -> self + + # + # Returns a value from `self`. + # + # With symbol or string argument `name` given, returns the value for the named + # member: # # Customer = Struct.new(:name, :address, :zip) - # Customer.members # => [:name, :address, :zip] + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe[:zip] # => 12345 + # + # Raises NameError if `name` is not the name of a member. + # + # With integer argument `n` given, returns `self.values[n]` if `n` is in range; + # see Array@Array+Indexes: + # + # joe[2] # => 12345 + # joe[-2] # => "123 Maple, Anytown NC" + # + # Raises IndexError if `n` is out of range. # - def self.members: () -> ::Array[Symbol] + def []: (index name_or_position) -> Elem # - # Returns `true` if the class was initialized with `keyword_init: true`. - # Otherwise returns `nil` or `false`. + # Assigns a value to a member. + # + # With symbol or string argument `name` given, assigns the given `value` to the + # named member; returns `value`: + # + # Customer = Struct.new(:name, :address, :zip) + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe[:zip] = 54321 # => 54321 + # joe # => # + # + # Raises NameError if `name` is not the name of a member. + # + # With integer argument `n` given, assigns the given `value` to the `n`-th + # member if `n` is in range; see Array@Array+Indexes: + # + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe[2] = 54321 # => 54321 + # joe[-3] = 'Joseph Smith' # => "Joseph Smith" + # joe # => # + # + # Raises IndexError if `n` is out of range. + # + def []=: (index name_or_position, Elem value) -> Elem + + # + # With a block given, returns an array of values from `self` for which the block + # returns a truthy value: + # + # Customer = Struct.new(:name, :address, :zip) + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # a = joe.select {|value| value.is_a?(String) } + # a # => ["Joe Smith", "123 Maple, Anytown NC"] + # a = joe.select {|value| value.is_a?(Integer) } + # a # => [12345] + # + # With no block given, returns an Enumerator. + # + # Struct#filter is an alias for Struct#select. + # + def select: () -> Enumerator[Elem, Array[Elem]] + | () { (Elem value) -> boolish } -> Array[Elem] + + # + # With a block given, returns an array of values from `self` for which the block + # returns a truthy value: + # + # Customer = Struct.new(:name, :address, :zip) + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # a = joe.select {|value| value.is_a?(String) } + # a # => ["Joe Smith", "123 Maple, Anytown NC"] + # a = joe.select {|value| value.is_a?(Integer) } + # a # => [12345] + # + # With no block given, returns an Enumerator. + # + # Struct#filter is an alias for Struct#select. + # + alias filter select + + # + # Returns an array of values from `self`. + # + # With integer arguments `integers` given, returns an array containing each + # value given by one of `integers`: + # + # Customer = Struct.new(:name, :address, :zip) + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe.values_at(0, 2) # => ["Joe Smith", 12345] + # joe.values_at(2, 0) # => [12345, "Joe Smith"] + # joe.values_at(2, 1, 0) # => [12345, "123 Maple, Anytown NC", "Joe Smith"] + # joe.values_at(0, -3) # => ["Joe Smith", "Joe Smith"] + # + # Raises IndexError if any of `integers` is out of range; see + # Array@Array+Indexes. + # + # With integer range argument `integer_range` given, returns an array containing + # each value given by the elements of the range; fills with `nil` values for + # range elements larger than the structure: + # + # joe.values_at(0..2) + # # => ["Joe Smith", "123 Maple, Anytown NC", 12345] + # joe.values_at(-3..-1) + # # => ["Joe Smith", "123 Maple, Anytown NC", 12345] + # joe.values_at(1..4) # => ["123 Maple, Anytown NC", 12345, nil, nil] + # + # Raises RangeError if any element of the range is negative and out of range; + # see Array@Array+Indexes. + # + def values_at: (*int | range[int?] positions) -> Array[Elem] + + # + # Returns the member names from `self` as an array: + # + # Customer = Struct.new(:name, :address, :zip) + # Customer.new.members # => [:name, :address, :zip] + # + # Related: #to_a. + # + def members: () -> Array[Symbol] + + # + # Finds and returns an object among nested objects. The nested objects may be + # instances of various classes. See [Dig Methods](rdoc-ref:dig_methods.rdoc). + # + # Given symbol or string argument `name`, returns the object that is specified + # by `name` and `identifiers`: # - # Examples: # Foo = Struct.new(:a) - # Foo.keyword_init? # => nil - # Bar = Struct.new(:a, keyword_init: true) - # Bar.keyword_init? # => true - # Baz = Struct.new(:a, keyword_init: false) - # Baz.keyword_init? # => false + # f = Foo.new(Foo.new({b: [1, 2, 3]})) + # f.dig(:a) # => #[1, 2, 3]}> + # f.dig(:a, :a) # => {:b=>[1, 2, 3]} + # f.dig(:a, :a, :b) # => [1, 2, 3] + # f.dig(:a, :a, :b, 0) # => 1 + # f.dig(:b, 0) # => nil + # + # Given integer argument `n`, returns the object that is specified by `n` and + # `identifiers`: + # + # f.dig(0) # => #[1, 2, 3]}> + # f.dig(0, 0) # => {:b=>[1, 2, 3]} + # f.dig(0, 0, :b) # => [1, 2, 3] + # f.dig(0, 0, :b, 0) # => 1 + # f.dig(:b, 0) # => nil + # + def dig: (index name_or_position) -> Elem + | (index name_or_position, untyped, *untyped) -> untyped + + # + # Returns the values in `self` as an array: + # + # Customer = Struct.new(:name, :address, :zip) + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # joe.to_a # => ["Joe Smith", "123 Maple, Anytown NC", 12345] + # + # Struct#values and Struct#deconstruct are aliases for Struct#to_a. + # + # Related: #members. + # + alias deconstruct to_a + + # + # Returns a hash of the name/value pairs for the given member names. + # + # Customer = Struct.new(:name, :address, :zip) + # joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) + # h = joe.deconstruct_keys([:zip, :address]) + # h # => {:zip=>12345, :address=>"123 Maple, Anytown NC"} + # + # Returns all names and values if `array_of_names` is `nil`: + # + # h = joe.deconstruct_keys(nil) + # h # => {:name=>"Joseph Smith, Jr.", :address=>"123 Maple, Anytown NC", :zip=>12345} # - def self.keyword_init?: () -> (true | false | nil) + def deconstruct_keys: (Array[index & Hash::_Key]? indices) -> Hash[index & Hash::_Key, Elem] end diff --git a/test/stdlib/Struct_test.rb b/test/stdlib/Struct_test.rb index 103bc0647..3af71167c 100644 --- a/test/stdlib/Struct_test.rb +++ b/test/stdlib/Struct_test.rb @@ -1,16 +1,219 @@ -require_relative 'test_helper' +require_relative "test_helper" -class StructTest < Test::Unit::TestCase +class StructSingletonTest < Test::Unit::TestCase include TypeAssertions - testing '::Struct[::Integer]' + testing "singleton(::Struct)" + + def test_new + # Since we're redefining `TestNewStruct` constantly, we need to set `$VERBOSE` to nil to suppress + # the redeclaration warnings. + old_verbose = $VERBOSE + $VERBOSE = nil + + with_string 'TestNewStruct' do |classname| + with_interned :field1 do |field1| + with_interned :field2 do |field2| + assert_send_type "(::string?, *::interned) -> singleton(Struct)", + Struct, :new, classname, field1, field2 + assert_send_type "(::string?, *::interned) { (self) -> void } -> singleton(Struct)", + Struct, :new, classname, field1, field2 do end + + if Symbol === field1 # can't use `.is_a?` since `ToStr` doesn't define it. + assert_send_type "(Symbol, *::interned) -> singleton(Struct)", + Struct, :new, field1, field2 + assert_send_type "(Symbol, *::interned) { (self) -> void } -> singleton(Struct)", + Struct, :new, field1, field2 do end + end + + ['yes', false, nil].each do |kwinit| + assert_send_type "(::string?, *::interned, keyword_init: ::boolish?) ?{ (self) -> void } -> singleton(Struct)", + Struct, :new, classname, field1, field2, keyword_init: kwinit + assert_send_type "(::string?, *::interned, keyword_init: ::boolish?) ?{ (self) -> void } -> singleton(Struct)", + Struct, :new, classname, field1, field2, keyword_init: kwinit do end + + if Symbol === field1 # can't use `.is_a?` since `ToStr` doesn't define it. + assert_send_type "(Symbol, *::interned, keyword_init: ::boolish?) -> singleton(Struct)", + Struct, :new, field1, field2, keyword_init: kwinit + assert_send_type "(Symbol, *::interned, keyword_init: ::boolish?) { (self) -> void } -> singleton(Struct)", + Struct, :new, field1, field2, keyword_init: kwinit do end + end + end + end + end + end + ensure + $VERBOSE = old_verbose + end + + def test_members + assert_send_type "() -> Array[Symbol]", + Struct.new(:foo, :bar), :members + end + + def test_keyword_init? + assert_send_type "() -> bool?", + Struct.new(:foo), :keyword_init? + assert_send_type "() -> bool?", + Struct.new(:foo, keyword_init: true), :keyword_init? + assert_send_type "() -> bool?", + Struct.new(:foo, keyword_init: false), :keyword_init? + assert_send_type "() -> bool?", + Struct.new(:foo, keyword_init: nil), :keyword_init? + end +end + +class StructInstanceTest < Test::Unit::TestCase + include TypeAssertions + + testing "::Struct[Rational]" MyStruct = Struct.new(:foo, :bar) + Instance = MyStruct.new(1r, 2r) + + + def with_index(int=1, str=:bar, &block) + block.call str.to_s + block.call str.to_sym + with_int(int, &block) + end + + def test_equal + assert_send_type "(untyped) -> bool", + Instance, :==, 3 + end + + def test_eql? + assert_send_type "(untyped) -> bool", + Instance, :eql?, 3 + end + + def test_hash + assert_send_type "() -> Integer", + Instance, :hash + end + + def test_inspect + assert_send_type "() -> String", + Instance, :inspect + end + + def test_to_s + assert_send_type "() -> String", + Instance, :to_s + end + + def test_to_a + assert_send_type "() -> Array[Rational]", + Instance, :to_a + end + + def test_to_h + assert_send_type "() -> Hash[Symbol, Rational]", + Instance, :to_h + assert_send_type "[K, V] () { (Symbol, Rational) -> [K, V] } -> Hash[K, V]", + Instance, :to_h do [1, 2] end + end + + def test_values + assert_send_type "() -> Array[Rational]", + Instance, :values + end + + def test_size + assert_send_type "() -> Integer", + Instance, :size + end + + def test_length + assert_send_type "() -> Integer", + Instance, :length + end def test_each - assert_send_type '() { (::Integer?) -> void } -> self', - MyStruct.new(42), :each do end - assert_send_type '() -> ::Enumerator[::Integer?, self]', - MyStruct.new(42), :each + assert_send_type "() -> Enumerator[Rational, self]", + Instance, :each + assert_send_type "() { (Rational) -> void } -> self", + Instance, :each do end + end + + def test_each_pair + assert_send_type "() -> Enumerator[[Symbol, Rational], self]", + Instance, :each_pair + assert_send_type "() { ([Symbol, Rational]) -> void } -> self", + Instance, :each_pair do end + end + + def test_aref + with_index do |idx| + assert_send_type "(Struct::index) -> Rational", + Instance, :[], idx + end + end + + def test_aset + with_index do |idx| + assert_send_type "(Struct::index, Rational) -> Rational", + Instance, :[]=, idx, 1r + end + end + + def test_select + assert_send_type "() -> Enumerator[Rational, Array[Rational]]", + Instance, :select + assert_send_type "() { (Rational) -> ::boolish } -> Array[Rational]", + Instance, :select do end + end + + + def test_filter + assert_send_type "() -> Enumerator[Rational, Array[Rational]]", + Instance, :filter + assert_send_type "() { (Rational) -> ::boolish } -> Array[Rational]", + Instance, :filter do end + end + + def test_values_at + assert_send_type "() -> Array[Rational]", + Instance, :values_at + + with_int 1 do |idx| + assert_send_type "(*::int | ::range[::int?]) -> Array[Rational]", + Instance, :values_at, idx, idx..nil + end + end + + def test_members + assert_send_type "() -> Array[Symbol]", + Instance, :members + end + + def test_dig + array_instance = MyStruct.new([1]) + with_index do |idx| + assert_send_type "(Struct::index) -> Rational", + Instance, :dig, idx + assert_send_type "(Struct::index, untyped, *untyped) -> untyped", + array_instance, :dig, idx, 1 + end + end + + def test_deconstruct + assert_send_type "() -> Array[Rational]", + Instance, :deconstruct + end + + def test_deconstruct_keys + assert_send_type "(nil) -> Hash[Symbol, Rational]", + Instance, :deconstruct_keys, nil + + with_index do |idx| + # Ensure that the `ToInt` variants have `hash` and `eql?` defined. + def idx.hash = 0 unless defined? idx.hash + def idx.eql?(r) = false unless defined? idx.eql? + + assert_send_type "(Array[Struct::index & Hash::_Key]) -> Hash[Struct::index & Hash::_Key, Rational]", + Instance, :deconstruct_keys, [idx] + end end end