Repository: soutaro/steep (Ruby)

Issue:
Can't type check `$stdout`/`$stderr`
When I reference `$stdout` or `$stderr`, `steep check` tells me it can't detect the type:

```
elasticgraph-support/lib/elastic_graph/support/logger.rb:33:29: [error] Cannot detect the type of the expression
│ Diagnostic ID: Ruby::FallbackAny
│
└           when "stdout" then $stdout
                               ~~~~~~~

elasticgraph-support/lib/elastic_graph/support/logger.rb:34:29: [error] Cannot detect the type of the expression
│ Diagnostic ID: Ruby::FallbackAny
│
└           when "stderr" then $stderr
                               ~~~~~~~
```

Since `$stdout` and `$stderr` are built-in global variables it feels like steep should understand them automatically.  Furthermore, defining the types for these manually (e.g. via `$stdout: IO` in an rbs file) doesn't work; that changes the steep error to:

```
elasticgraph-support/sig/elastic_graph/support/logger.rbs:33:0: [error] Declaration of `$stderr` is duplicated
│ Diagnostic ID: RBS::DuplicatedDeclaration
│
└ $stderr: IO
  ~~~~~~~~~~~
```

or

```
elasticgraph-support/sig/elastic_graph/support/logger.rbs:32:0: [error] Declaration of `$stdout` is duplicated
│ Diagnostic ID: RBS::DuplicatedDeclaration
│
└ $stdout: IO
  ~~~~~~~~~~~
```

(Which of the two "is duplicated" errors I get depends on which global variable is defined first in my rbs file).

So it seems like steep already has a definition for `$stdout`/`$stderr` but doesn't use the definition when it encounters the expression.


Discussion:
While `$stdout` / `$stderr` are normally `IO` instances, you can actually set them to anything which has `.write` defined:
```ruby
class MyStdout
   def write(*) = nil # swallow
end
```
Since they're global variables, they can be assigned by any anyone—including code that steep can't monitor (eg external gems). While I'd like to see this feature, I don't really think it's possible to do if we want to typecheck valid ruby
> While `$stdout` / `$stderr` are normally `IO` instances, you can actually set them to anything which has `.write` defined:

Perhaps (are there any ruby docs that state that only a `write` method is required?) but according to the RBS files that ship with ruby, the type for these is `IO`:

https://github.com/ruby/rbs/blob/v2.4.0/core/global_variables.rbs#L174-L181

> While I'd like to see this feature, I don't really think it's possible to do if we want to typecheck valid ruby

To be clear, the only "feature" I'm asking for is for code that uses `$stdout`/`$stderr` to be able to type check with steep without using `__skip__`.  But actually I think this is just a bug--these globals have a type defined but steep reports `Cannot detect the type of the expression` on them.
Thanks @myronmarston, 

They are global variables so the type definitions should be included or we can add type definition. Something is wrong...
Two problems are uncovered in this issue.

1. The global variable type checking is broken in Steep
2. The type definition of `$stdout`/`$stderr` can be improved in RBS

We will update the type of the global variables in RBS, in next major release (3.0).



Patch:
diff --git a/lib/steep/type_construction.rb b/lib/steep/type_construction.rb
index 1613776da..29e545106 100644
--- a/lib/steep/type_construction.rb
+++ b/lib/steep/type_construction.rb
@@ -131,11 +131,13 @@ def check_relation(sub_type:, super_type:, constraints: Subtyping::Constraints.e
 
     def for_new_method(method_name, node, args:, self_type:, definition:)
       annots = source.annotations(block: node, factory: checker.factory, context: nesting)
-      type_env = TypeInference::TypeEnv.new(subtyping: checker, const_env: module_context&.const_env || self.type_env.const_env)
-
-      self.type_env.const_types.each do |name, type|
-        type_env.set(const: name, type: type)
-      end
+      type_env = TypeInference::TypeEnv.build(
+        annotations: annots,
+        subtyping: checker,
+        const_env: module_context&.const_env || self.type_env.const_env,
+        signatures: checker.factory.env
+      )
+      type_env.merge_const_types(self.type_env)
 
       definition_method_type = if definition
                                  definition.methods[method_name]&.yield_self do |method|
diff --git a/lib/steep/type_inference/type_env.rb b/lib/steep/type_inference/type_env.rb
index 41fda1118..4759986be 100644
--- a/lib/steep/type_inference/type_env.rb
+++ b/lib/steep/type_inference/type_env.rb
@@ -73,6 +73,12 @@ def get(const: nil, gvar: nil, ivar: nil)
         end
       end
 
+      def merge_const_types(env)
+        env.const_types.each do |name, type|
+          const_types[name] = type unless const_types.key?(name)
+        end
+      end
+
       def set(const: nil, gvar: nil, ivar: nil, type:)
         case
         when const


Test patch:
diff --git a/test/type_construction_test.rb b/test/type_construction_test.rb
index 3b4b1c131..e3688e0e7 100644
--- a/test/type_construction_test.rb
+++ b/test/type_construction_test.rb
@@ -9176,4 +9176,44 @@ def foo(x = 123, foo: true)
       end
     end
   end
+
+  def test_anonymouse_block_forwarding
+    with_checker(<<RBS) do |checker|
+$TEST: bool
+RBS
+      source = parse_ruby(<<RUBY)
+def foo
+  $TEST
+end
+RUBY
+
+      with_standard_construction(checker, source) do |construction, typing|
+        type, _ = construction.synthesize(source.node)
+
+        assert_no_error(typing)
+      end
+    end
+  end
+
+  def test_const_inline_annotation
+    with_checker(<<RBS) do |checker|
+module A end
+RBS
+      source = parse_ruby(<<RUBY)
+module A
+  # @type const X: String
+
+  def foo
+    X + ""
+  end
+end
+RUBY
+
+      with_standard_construction(checker, source) do |construction, typing|
+        type, _ = construction.synthesize(source.node)
+
+        assert_no_error(typing)
+      end
+    end
+  end
 end