Repository: soutaro/steep (Ruby)

Issue:
lambda's parameter is marked as untyped with union
Lambda's parameter is marked as untyped unexpectedly when the lambda is assigned to a variable as a union type.


```ruby
# It is ok. `str` is `String`
# @type var x: ^(String) -> void
x = -> (str) { str + 'a' }

# It is not ok. `str` is `untyped`, but I expected `String`.
# @type var x: ^(String) -> void | nil
x = -> (str) { str + 'a' }
```

I've confirmed this problem with vscode and Steep LSP's hover feature.
![Screen Shot 2022-11-01 at 0 34 41](https://user-images.githubusercontent.com/4361134/199047326-3b5fa39a-4247-45f2-84d2-ed9b041e8166.png)

---

Actually I found this problem when I tried to enable checking "untyped call" on retryable gem's RBS.
https://github.com/ruby/gem_rbs_collection/blob/006177988c5ad5261efe67af814cc0f726b0eb8e/gems/retryable/3.0/configuration.rbs#L23
https://github.com/ruby/gem_rbs_collection/blob/006177988c5ad5261efe67af814cc0f726b0eb8e/gems/retryable/3.0/_test/test.rb#L68
In this case, `retries` is marked as `untyped` unexpectedly.



Fix (patch):
diff --git a/lib/steep/diagnostic/ruby.rb b/lib/steep/diagnostic/ruby.rb
index b9249a163..980c67a15 100644
--- a/lib/steep/diagnostic/ruby.rb
+++ b/lib/steep/diagnostic/ruby.rb
@@ -908,6 +908,19 @@ def header_line
         end
       end
 
+      class ProcHintIgnored < Base
+        attr_reader :hint_type, :block_node
+
+        def initialize(hint_type:, node:)
+          @hint_type = hint_type
+          super(node: node)
+        end
+
+        def header_line
+          "The type hint given to the block is ignored: `#{hint_type}`"
+        end
+      end
+
       ALL = ObjectSpace.each_object(Class).with_object([]) do |klass, array|
         if klass < Base
           array << klass
@@ -932,7 +945,8 @@ def self.default
             UnexpectedTypeArgument => :information,
             InsufficientTypeArgument => :information,
             UnexpectedTypeArgument => :information,
-            UnsupportedSyntax => nil
+            UnsupportedSyntax => nil,
+            ProcHintIgnored => :information
           }
         ).freeze
       end
@@ -946,7 +960,8 @@ def self.strict
             ElseOnExhaustiveCase => nil,
             UnknownConstant => nil,
             MethodDefinitionMissing => nil,
-            UnsupportedSyntax => nil
+            UnsupportedSyntax => nil,
+            ProcHintIgnored => :warning
           }
         ).freeze
       end
@@ -962,7 +977,8 @@ def self.lenient
             MethodDefinitionMissing => nil,
             UnexpectedJump => nil,
             FalseAssertion => :hint,
-            UnsupportedSyntax => nil
+            UnsupportedSyntax => nil,
+            ProcHintIgnored => :hint
           }
         ).freeze
       end
diff --git a/lib/steep/subtyping/variable_variance.rb b/lib/steep/subtyping/variable_variance.rb
index 0fb514471..1a277ffb5 100644
--- a/lib/steep/subtyping/variable_variance.rb
+++ b/lib/steep/subtyping/variable_variance.rb
@@ -63,6 +63,17 @@ def self.add_type(type, variance:, covariants:, contravariants:)
             covariants << type.name
             contravariants << type.name
           end
+        when AST::Types::Proc
+          type.type.params.each_type do |type|
+            add_type(type, variance: variance, covariants: contravariants, contravariants: covariants)
+          end
+          add_type(type.type.return_type, variance: variance, covariants: covariants, contravariants: contravariants)
+          if type.block
+            type.block.type.params.each_type do |type|
+              add_type(type, variance: variance, covariants: covariants, contravariants: contravariants)
+            end
+            add_type(type.type.return_type, variance: variance, covariants: contravariants, contravariants: covariants)
+          end
         when AST::Types::Union, AST::Types::Intersection, AST::Types::Tuple
           type.types.each do |ty|
             add_type(ty, variance: variance, covariants: covariants, contravariants: contravariants)
diff --git a/lib/steep/type_construction.rb b/lib/steep/type_construction.rb
index ab5bc7215..cdf0e7da0 100644
--- a/lib/steep/type_construction.rb
+++ b/lib/steep/type_construction.rb
@@ -2904,14 +2904,32 @@ def type_lambda(node, params_node:, body_node:, type_hint:)
       block_annotations = source.annotations(block: node, factory: checker.factory, context: nesting)
       params = TypeInference::BlockParams.from_node(params_node, annotations: block_annotations)
 
-      type_hint = deep_expand_alias(type_hint) if type_hint
+      if type_hint
+        original_hint = type_hint
 
-      case type_hint
-      when AST::Types::Proc
-        params_hint = type_hint.type.params
-        return_hint = type_hint.type.return_type
-        block_hint = type_hint.block
-        self_hint = type_hint.self_type
+        type_hint = deep_expand_alias(type_hint) || type_hint
+
+        procs = flatten_union(type_hint).select do |type|
+          check_relation(sub_type: type, super_type: AST::Builtin::Proc.instance_type).success?
+        end
+
+        proc_instances, proc_types = procs.partition {|type| AST::Builtin::Proc.instance_type?(type) }
+
+        case
+        when !proc_instances.empty? && proc_types.empty?
+          # `::Proc` is given as a hint
+        when proc_types.size == 1
+          # Proc type is given as a hint
+          hint_proc = proc_types[0]  #: AST::Types::Proc
+          params_hint = hint_proc.type.params
+          return_hint = hint_proc.type.return_type
+          block_hint = hint_proc.block
+          self_hint = hint_proc.self_type
+        else
+          typing.add_error(
+            Diagnostic::Ruby::ProcHintIgnored.new(hint_type: original_hint, node: node)
+          )
+        end
       end
 
       block_constr = for_block(
diff --git a/sig/steep/diagnostic/ruby.rbs b/sig/steep/diagnostic/ruby.rbs
index a80fe3764..4095ad589 100644
--- a/sig/steep/diagnostic/ruby.rbs
+++ b/sig/steep/diagnostic/ruby.rbs
@@ -530,6 +530,17 @@ module Steep
         def header_line: () -> String
       end
 
+      # Type hint is given to a proc/lambda but it was ignored
+      #
+      # 1. Because the hint is incompatible to `::Proc` type
+      # 2. More than one *proc type* is included in the hint
+      #
+      class ProcHintIgnored < Base
+        attr_reader hint_type: AST::Types::t
+
+        def initialize: (hint_type: AST::Types::t, node: Parser::AST::Node) -> void
+      end
+
       # Argument forwarding `...` cannot be done safely, because of
       #
       # 1. The arguments are incompatible, or
diff --git a/sig/steep/subtyping/variable_variance.rbs b/sig/steep/subtyping/variable_variance.rbs
index 8482726b1..460fecce3 100644
--- a/sig/steep/subtyping/variable_variance.rbs
+++ b/sig/steep/subtyping/variable_variance.rbs
@@ -1,25 +1,25 @@
 module Steep
   module Subtyping
     class VariableVariance
-      attr_reader covariants: untyped
+      attr_reader covariants: Set[Symbol]
 
-      attr_reader contravariants: untyped
+      attr_reader contravariants: Set[Symbol]
 
-      def initialize: (covariants: untyped, contravariants: untyped) -> void
+      def initialize: (covariants: Set[Symbol], contravariants: Set[Symbol]) -> void
 
-      def covariant?: (untyped var) -> untyped
+      def covariant?: (Symbol var) -> bool
 
-      def contravariant?: (untyped var) -> untyped
+      def contravariant?: (Symbol var) -> bool
 
-      def invariant?: (untyped var) -> untyped
+      def invariant?: (Symbol var) -> bool
 
       def self.from_type: (AST::Types::t) -> VariableVariance
 
-      def self.from_method_type: (untyped method_type) -> untyped
+      def self.from_method_type: (Interface::MethodType method_type) -> VariableVariance
 
-      def self.add_params: (untyped params, block: untyped, covariants: untyped, contravariants: untyped) -> untyped
+      def self.add_params: (Interface::Function::Params params, block: bool, covariants: Set[Symbol], contravariants: Set[Symbol]) -> void
 
-      def self.add_type: (untyped `type`, variance: untyped, covariants: untyped, contravariants: untyped) -> untyped
+      def self.add_type: (AST::Types::t `type`, variance: :covariant | :contravariant | :invariant, covariants: Set[Symbol], contravariants: Set[Symbol]) -> void
     end
   end
 end