diff --git a/lib/steep/type_construction.rb b/lib/steep/type_construction.rb index 8ec8d9fd8..3fa549ec2 100644 --- a/lib/steep/type_construction.rb +++ b/lib/steep/type_construction.rb @@ -1398,8 +1398,11 @@ def synthesize(node, hint: nil, condition: false) when :true, :false ty = node.type == :true ? AST::Types::Literal.new(value: true) : AST::Types::Literal.new(value: false) - if hint && check_relation(sub_type: ty, super_type: hint).success? && !hint.is_a?(AST::Types::Any) && !hint.is_a?(AST::Types::Top) + case + when hint && check_relation(sub_type: ty, super_type: hint).success? && !hint.is_a?(AST::Types::Any) && !hint.is_a?(AST::Types::Top) add_typing(node, type: hint) + when condition + add_typing(node, type: ty) else add_typing(node, type: AST::Types::Boolean.new) end @@ -2016,9 +2019,6 @@ def synthesize(node, hint: nil, condition: false) branch_results = [] #: Array[Pair] condition_constr = constr - clause_constr = constr - - next_branch_reachable = true whens.each do |when_clause| when_clause_constr = condition_constr @@ -2029,7 +2029,6 @@ def synthesize(node, hint: nil, condition: false) *tests, body = when_clause.children branch_reachable = false - false_branch_reachable = false tests.each do |test| test_type, condition_constr = condition_constr.synthesize(test, condition: true) @@ -2040,12 +2039,9 @@ def synthesize(node, hint: nil, condition: false) condition_constr = condition_constr.update_type_env { falsy_env } body_envs << truthy_env - branch_reachable ||= next_branch_reachable && !truthy.unreachable - false_branch_reachable ||= !falsy.unreachable + branch_reachable ||= !truthy.unreachable end - next_branch_reachable &&= false_branch_reachable - if body branch_results << when_clause_constr @@ -4476,19 +4472,27 @@ def union_type(*types) end def union_type_unify(*types) - types.inject do |type1, type2| - next type1 if type1.is_a?(AST::Types::Any) - next type2 if type2.is_a?(AST::Types::Any) + types = types.reject {|t| t.is_a?(AST::Types::Bot) } - unless no_subtyping?(sub_type: type1, super_type: type2) - next type2 - end + if types.empty? + AST::Types::Bot.new + else + types.inject do |type1, type2| + next type2 if type1.is_a?(AST::Types::Any) + next type1 if type2.is_a?(AST::Types::Any) - unless no_subtyping?(sub_type: type2, super_type: type1) - next type1 - end + unless no_subtyping?(sub_type: type1, super_type: type2) + # type1 <: type2 + next type2 + end - union_type(type1, type2) + unless no_subtyping?(sub_type: type2, super_type: type1) + # type2 <: type1 + next type1 + end + + union_type(type1, type2) + end end end diff --git a/lib/steep/type_inference/logic_type_interpreter.rb b/lib/steep/type_inference/logic_type_interpreter.rb index 3166a5186..ea7939684 100644 --- a/lib/steep/type_inference/logic_type_interpreter.rb +++ b/lib/steep/type_inference/logic_type_interpreter.rb @@ -417,6 +417,10 @@ def literal_var_type_case_select(value_node, arg_type) end [truthy_types, falsy_types] + when AST::Types::Boolean + [[arg_type], [arg_type]] + when AST::Types::Top, AST::Types::Any + [[arg_type], [arg_type]] else types = [arg_type] diff --git a/test/type_check_test.rb b/test/type_check_test.rb index 0578d7c68..435312eb8 100644 --- a/test/type_check_test.rb +++ b/test/type_check_test.rb @@ -428,16 +428,6 @@ def test_case_unreachable_2 severity: ERROR message: Type `::String` does not have method `is_a_string` code: Ruby::NoMethod - - range: - start: - line: 7 - character: 0 - end: - line: 7 - character: 19 - severity: ERROR - message: The branch is unreachable - code: Ruby::UnreachableBranch YAML ) end @@ -887,6 +877,142 @@ def test_type_case__returns_nil_untyped_union a.is_untyped RUBY }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: + - range: + start: + line: 13 + character: 2 + end: + line: 13 + character: 12 + severity: ERROR + message: Type `nil` does not have method `is_untyped` + code: Ruby::NoMethod + YAML + ) + end + + def test_case_when__no_subject__reachability + run_type_check_test( + signatures: { + }, + code: { + "a.rb" => <<~RUBY + case + when false + :a + when nil + :b + when "".is_a?(NilClass) + :c + end + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: + - range: + start: + line: 3 + character: 2 + end: + line: 3 + character: 4 + severity: ERROR + message: The branch is unreachable + code: Ruby::UnreachableBranch + - range: + start: + line: 5 + character: 2 + end: + line: 5 + character: 4 + severity: ERROR + message: The branch is unreachable + code: Ruby::UnreachableBranch + - range: + start: + line: 7 + character: 2 + end: + line: 7 + character: 4 + severity: ERROR + message: The branch is unreachable + code: Ruby::UnreachableBranch + YAML + ) + end + + def test_case_when__no_subject__reachability_no_continue + run_type_check_test( + signatures: { + }, + code: { + "a.rb" => <<~RUBY + case + when true + :a + when 1 + :b + else + :c + end + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: [] + YAML + ) + end + + def test_case_when__untyped_value + run_type_check_test( + signatures: { + }, + code: { + "a.rb" => <<~RUBY + foo = true #: untyped + + case foo + when nil + 1 + when true + 2 + end + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: [] + YAML + ) + end + + def test_case_when__bool_value + run_type_check_test( + signatures: { + }, + code: { + "a.rb" => <<~RUBY + foo = true #: bool + + case foo + when false + 1 + when true + 2 + end + RUBY + }, expectations: <<~YAML --- - file: a.rb diff --git a/test/type_construction_test.rb b/test/type_construction_test.rb index d47ce1b2c..39dd1b959 100644 --- a/test/type_construction_test.rb +++ b/test/type_construction_test.rb @@ -3804,8 +3804,8 @@ def test_and_or assert_empty typing.errors - assert_equal parse_type("bool"), pair.context.type_env[:a] - assert_equal parse_type("bool"), pair.context.type_env[:b] + assert_equal parse_type("false"), pair.context.type_env[:a] + assert_equal parse_type("true"), pair.context.type_env[:b] end end end