Skip to content

Commit

Permalink
Merge pull request #1460 from soutaro/tuple-type
Browse files Browse the repository at this point in the history
Strict record and tuple subtyping
  • Loading branch information
soutaro authored Jan 7, 2025
2 parents 9d9c88e + 309f25b commit 1cbceaf
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 25 deletions.
27 changes: 17 additions & 10 deletions lib/steep/subtyping/check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ def check_type0(relation)
end

when relation.sub_type.is_a?(AST::Types::Tuple) && relation.super_type.is_a?(AST::Types::Tuple)
if relation.sub_type.types.size >= relation.super_type.types.size
if relation.sub_type.types.size == relation.super_type.types.size
pairs = relation.sub_type.types.take(relation.super_type.types.size).zip(relation.super_type.types)

All(relation) do |result|
Expand Down Expand Up @@ -541,21 +541,28 @@ def check_type0(relation)

when relation.sub_type.is_a?(AST::Types::Record) && relation.super_type.is_a?(AST::Types::Record)
All(relation) do |result|
unchecked_keys = Set.new(relation.sub_type.elements.each_key)

relation.super_type.elements.each_key do |key|
super_element_type = relation.super_type.elements[key]
super_element_type = relation.super_type.elements.fetch(key) #: AST::Types::t
sub_element_type = relation.sub_type.elements.fetch(key, nil) #: AST::Types::t?

if relation.sub_type.elements.key?(key)
sub_element_type = relation.sub_type.elements[key]
if relation.super_type.required?(key)
rel = Relation.new(sub_type: sub_element_type || AST::Builtin.nil_type, super_type: super_element_type)
result.add(rel) { check_type(rel) }
else
if relation.super_type.required?(key)
sub_element_type = AST::Builtin.nil_type
# If the key is optional, it's okay to not have the key
if sub_element_type
rel = Relation.new(sub_type: sub_element_type, super_type: super_element_type)
result.add(rel) { check_type(rel) }
end
end

if sub_element_type
rel = Relation.new(sub_type: sub_element_type, super_type: super_element_type)
result.add(rel) { check_type(rel) }
end
unchecked_keys.delete(key)
end

unless unchecked_keys.empty?
return Failure(relation, Result::Failure::UnknownPairError.new(relation: relation))
end
end

Expand Down
4 changes: 4 additions & 0 deletions sig/test/subtyping_test.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ class SubtypingTest < Minitest::Test

def test_void: () -> untyped

def test_tuple: () -> untyped

def test_record: () -> untyped

def print_result: (untyped result, untyped output, ?prefix: untyped) -> untyped

def assert_success_check: (untyped checker, untyped sub_type, untyped super_type, ?self_type: untyped, ?instance_type: untyped, ?class_type: untyped, ?constraints: untyped) -> untyped
Expand Down
2 changes: 2 additions & 0 deletions sig/test/type_check_test.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,6 @@ class TypeCheckTest < Minitest::Test
def test_when__assertion: () -> untyped

def test_when__type_annotation: () -> untyped

def test_tuple_type_assertion: () -> untyped
end
7 changes: 5 additions & 2 deletions smoke/hash/d.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# @type var params: { name: String, id: Integer }

params = { id: 30, name: "Matz" }
params = { id: 30, name: "Matz", email: "[email protected]" }
params = { id: 30, name: "foo" }

params = { id: "30", name: "foo", email: "[email protected]" }
# @type var params2: { name: String, id: Integer, email: String? }
params2 = { id: 30, name: "Matz", email: "[email protected]" }
params2 = { id: 30, name: "foo" }
21 changes: 9 additions & 12 deletions smoke/hash/test_expectations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,26 +54,23 @@
diagnostics:
- range:
start:
line: 5
line: 3
character: 0
end:
line: 5
character: 66
line: 3
character: 60
severity: ERROR
message: |-
Cannot assign a value of type `{ ?:email => ::String, :id => ::String, :name => ::String }` to a variable of type `{ :id => ::Integer, :name => ::String }`
{ ?:email => ::String, :id => ::String, :name => ::String } <: { :id => ::Integer, :name => ::String }
::String <: ::Integer
::Object <: ::Integer
::BasicObject <: ::Integer
Cannot assign a value of type `{ ?:email => ::String, :id => ::Integer, :name => ::String }` to a variable of type `{ :id => ::Integer, :name => ::String }`
{ ?:email => ::String, :id => ::Integer, :name => ::String } <: { :id => ::Integer, :name => ::String }
code: Ruby::IncompatibleAssignment
- range:
start:
line: 5
character: 34
line: 3
character: 33
end:
line: 5
character: 39
line: 3
character: 38
severity: ERROR
message: Unknown key `:email` is given to a record type
code: Ruby::UnknownRecordKey
Expand Down
25 changes: 24 additions & 1 deletion test/subtyping_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,30 @@ def test_void
end
end

def test_tuple
with_checker do |checker|
assert_success_check checker, "[String]", "[untyped]"
assert_success_check checker, "[String]", "[top]"
assert_success_check checker, "[123]", "[Integer]"

assert_fail_check checker, "[String]", "[String, Integer]"
assert_fail_check checker, "[String, Symbol]", "[String]"
end
end

def test_record
with_checker do |checker|
assert_success_check checker, "{ foo: String }", "{ foo: untyped }"
assert_success_check checker, "{ foo: String }", "{ foo: Object }"

assert_fail_check checker, "{ foo: String, bar: Integer }", "{ foo: String }"
assert_fail_check checker, "{ foo: String }", "{ foo: String, bar: Integer }"

assert_success_check checker, "{ foo: String }", "{ foo: String, ?bar: Integer }"
assert_success_check checker, "{ foo: String }", "{ foo: String, bar: Integer? }"
end
end

def print_result(result, output, prefix: " ")
mark = result.success? ? "👍" : "🤦"

Expand Down Expand Up @@ -732,7 +756,6 @@ def test_expand_alias

def test_hash
with_checker do |checker|
assert_success_check checker, "{ foo: ::Integer, bar: ::String }", "{ foo: ::Integer }"
assert_fail_check checker, "{ foo: ::String }", "{ foo: ::Integer }"
assert_success_check checker, "{ foo: ::String, bar: ::Integer }", "{ foo: ::String, bar: ::Integer? }"
assert_success_check checker, "{ foo: ::String, bar: nil }", "{ foo: ::String, bar: ::Integer? }"
Expand Down
30 changes: 30 additions & 0 deletions test/type_check_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2897,4 +2897,34 @@ def test_when__type_annotation
YAML
)
end

def test_tuple_type_assertion
run_type_check_test(
signatures: {
"a.rbs" => <<~RBS
RBS
},
code: {
"a.rb" => <<~RUBY
[1, ""] #: [1, "", bool]
RUBY
},
expectations: <<~YAML
---
- file: a.rb
diagnostics:
- range:
start:
line: 1
character: 0
end:
line: 1
character: 24
severity: ERROR
message: 'Assertion cannot hold: no relationship between inferred type (`[1, \"\"]`)
and asserted type (`[1, \"\", bool]`)'
code: Ruby::FalseAssertion
YAML
)
end
end

0 comments on commit 1cbceaf

Please sign in to comment.