Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lib/steep/type_construction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
129 changes: 129 additions & 0 deletions test/type_check_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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