diff --git a/lib/steep/type_construction.rb b/lib/steep/type_construction.rb index 5d6dee06c..592c18fd9 100644 --- a/lib/steep/type_construction.rb +++ b/lib/steep/type_construction.rb @@ -1761,6 +1761,19 @@ def synthesize(node, hint: nil, condition: false) else if hint tuples = select_flatten_types(hint) {|type| type.is_a?(AST::Types::Tuple) } #: Array[AST::Types::Tuple] + if tuples.empty? + # An interface hint like `Hash::_Pair[K, V]` (which rbs 4.1 uses for the blocks of `Array#to_h` + # and `Hash#to_h` and for the array-of-pairs argument of `Hash.[]`) is not a tuple type itself, + # but declares the tuple it accepts through its `#to_ary` method. + # Deconstruct such hints so the array literal can be inferred as a tuple. + candidates = select_flatten_types(hint) do |type| + type.is_a?(AST::Types::Name::Interface) || type.is_a?(AST::Types::Intersection) + end + tuples = candidates.filter_map do |type| + converted = try_convert(type, :to_ary) + converted if converted.is_a?(AST::Types::Tuple) + end #: Array[AST::Types::Tuple] + end unless tuples.empty? fallback_pair = nil #: Pair? tuples.each do |tuple| diff --git a/test/type_check_test.rb b/test/type_check_test.rb index 5d70a1d28..f0f2d5b1c 100644 --- a/test/type_check_test.rb +++ b/test/type_check_test.rb @@ -4580,4 +4580,133 @@ class OrAssignBox[T] YAML ) end + + def test_array_literal_with_pair_interface_hint + run_type_check_test( + signatures: { + "a.rbs" => <<~RBS + interface _KeyValue[K, V] + def to_ary: () -> [K, V] + end + + class PairMaker + def make: [K, V] () { (String) -> _KeyValue[K, V] } -> Hash[K, V] + end + RBS + }, + code: { + "a.rb" => <<~RUBY + maker = PairMaker.new + hash = maker.make {|string| [string, string.size] } + hash.each_key {|key| key.upcase } + hash.each_value {|value| value + 1 } + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: [] + YAML + ) + end + + def test_array_literal_with_pair_interface_intersection_hint + run_type_check_test( + signatures: { + "a.rbs" => <<~RBS + interface _KeyValue[K, V] + def to_ary: () -> [K, V] + end + + class PairMaker + def make_pair: () { (String) -> ([Symbol, Integer] & _KeyValue[Symbol, Integer]) } -> Hash[Symbol, Integer] + end + RBS + }, + code: { + "a.rb" => <<~RUBY + maker = PairMaker.new + maker.make_pair {|string| [string.to_sym, string.size] } + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: [] + YAML + ) + end + + def test_array_literal_with_pair_interface_hint_two_params_block + run_type_check_test( + signatures: { + "a.rbs" => <<~RBS + interface _KeyValue[K, V] + def to_ary: () -> [K, V] + end + + class PairMaker + def make_from_pairs: [K, V] () { (Symbol, Integer) -> _KeyValue[K, V] } -> Hash[K, V] + end + RBS + }, + code: { + "a.rb" => <<~RUBY + maker = PairMaker.new + hash = maker.make_from_pairs {|key, value| [key.to_s, value.to_f] } + hash.each_key {|key| key.upcase } + hash.each_value {|value| value.floor } + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: [] + YAML + ) + end + + def test_array_literal_with_pair_interface_hint_mismatch + run_type_check_test( + signatures: { + "a.rbs" => <<~RBS + interface _KeyValue[K, V] + def to_ary: () -> [K, V] + end + + class PairMaker + def make_exact: () { (String) -> _KeyValue[Symbol, Integer] } -> Hash[Symbol, Integer] + end + RBS + }, + code: { + "a.rb" => <<~RUBY + maker = PairMaker.new + maker.make_exact {|string| [string, 1] } + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: + - range: + start: + line: 2 + character: 17 + end: + line: 2 + character: 40 + severity: ERROR + message: |- + Cannot allow block body have type `[::String, ::Integer]` because declared as type `::_KeyValue[::Symbol, ::Integer]` + [::String, ::Integer] <: ::_KeyValue[::Symbol, ::Integer] + () -> [::String, ::Integer] <: () -> [::Symbol, ::Integer] + [::String, ::Integer] <: [::Symbol, ::Integer] + ::String <: ::Symbol + ::Object <: ::Symbol + ::BasicObject <: ::Symbol + code: Ruby::BlockBodyTypeMismatch + YAML + ) + end end