From a93f45be3d6f0ec1c1a3aa57ad7a51fd71d3e799 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 7 Jan 2025 16:14:00 +0900 Subject: [PATCH 1/3] Make tuple subtyping strict --- lib/steep/subtyping/check.rb | 2 +- sig/test/subtyping_test.rbs | 2 ++ sig/test/type_check_test.rbs | 2 ++ test/subtyping_test.rb | 11 +++++++++++ test/type_check_test.rb | 30 ++++++++++++++++++++++++++++++ 5 files changed, 46 insertions(+), 1 deletion(-) diff --git a/lib/steep/subtyping/check.rb b/lib/steep/subtyping/check.rb index 9817da92..f2e783cf 100644 --- a/lib/steep/subtyping/check.rb +++ b/lib/steep/subtyping/check.rb @@ -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| diff --git a/sig/test/subtyping_test.rbs b/sig/test/subtyping_test.rbs index a8b44196..2efb8e39 100644 --- a/sig/test/subtyping_test.rbs +++ b/sig/test/subtyping_test.rbs @@ -67,6 +67,8 @@ class SubtypingTest < Minitest::Test def test_void: () -> untyped + def test_tuple: () -> 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 diff --git a/sig/test/type_check_test.rbs b/sig/test/type_check_test.rbs index 57ef3d8a..65e74c2a 100644 --- a/sig/test/type_check_test.rbs +++ b/sig/test/type_check_test.rbs @@ -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 diff --git a/test/subtyping_test.rb b/test/subtyping_test.rb index 19c39e77..e518c436 100644 --- a/test/subtyping_test.rb +++ b/test/subtyping_test.rb @@ -431,6 +431,17 @@ 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 print_result(result, output, prefix: " ") mark = result.success? ? "👍" : "🤦" diff --git a/test/type_check_test.rb b/test/type_check_test.rb index 761b4181..61df331f 100644 --- a/test/type_check_test.rb +++ b/test/type_check_test.rb @@ -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 From 62bae7643cad72ed697b4e9c702a279e1d23e196 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 7 Jan 2025 16:30:31 +0900 Subject: [PATCH 2/3] Strict record subtyping --- lib/steep/subtyping/check.rb | 25 ++++++++++++++++--------- sig/test/subtyping_test.rbs | 2 ++ test/subtyping_test.rb | 14 +++++++++++++- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/lib/steep/subtyping/check.rb b/lib/steep/subtyping/check.rb index f2e783cf..fd8a56d2 100644 --- a/lib/steep/subtyping/check.rb +++ b/lib/steep/subtyping/check.rb @@ -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 diff --git a/sig/test/subtyping_test.rbs b/sig/test/subtyping_test.rbs index 2efb8e39..9c6b0807 100644 --- a/sig/test/subtyping_test.rbs +++ b/sig/test/subtyping_test.rbs @@ -69,6 +69,8 @@ class SubtypingTest < Minitest::Test 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 diff --git a/test/subtyping_test.rb b/test/subtyping_test.rb index e518c436..b5145bd6 100644 --- a/test/subtyping_test.rb +++ b/test/subtyping_test.rb @@ -442,6 +442,19 @@ def test_tuple 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? ? "👍" : "🤦" @@ -743,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? }" From 309f25b635e03ed868513655f11c8e184cb38036 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 7 Jan 2025 16:39:04 +0900 Subject: [PATCH 3/3] Update test --- smoke/hash/d.rb | 7 +++++-- smoke/hash/test_expectations.yml | 21 +++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/smoke/hash/d.rb b/smoke/hash/d.rb index 2a3c2f08..15a6d341 100644 --- a/smoke/hash/d.rb +++ b/smoke/hash/d.rb @@ -1,5 +1,8 @@ # @type var params: { name: String, id: Integer } -params = { id: 30, name: "Matz" } +params = { id: 30, name: "Matz", email: "matz@example.com" } +params = { id: 30, name: "foo" } -params = { id: "30", name: "foo", email: "matsumoto@soutaro.com" } +# @type var params2: { name: String, id: Integer, email: String? } +params2 = { id: 30, name: "Matz", email: "matz@example.com" } +params2 = { id: 30, name: "foo" } diff --git a/smoke/hash/test_expectations.yml b/smoke/hash/test_expectations.yml index 08d3be19..78b354f4 100644 --- a/smoke/hash/test_expectations.yml +++ b/smoke/hash/test_expectations.yml @@ -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