rubysorbet

How can I resolve sorbet error: "Constants must have type annotations with T.let when specifying # typed: strict"?


This is similar to my question in How can I resolve sorbet error: "Use of undeclared variable"?, but for constants.

I am experimenting with adding sorbet type information to my gem, pdf-reader. I don't want sorbet to be a runtime dependency for the gem, so all type annotations are in an external file in the rbi/ directory. I also can't extend T::Sig in my classes, and I can't use T.let in my code.

I'd like to enable typed: strict in some files, but doing so flags that constants don't have type annotations:

$ be srb tc
./lib/pdf/reader/buffer.rb:41: Constants must have type annotations with T.let when specifying # typed: strict https://srb.help/7027
    41 |    TOKEN_WHITESPACE=[0x00, 0x09, 0x0A, 0x0C, 0x0D, 0x20]
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  Autocorrect: Use `-a` to autocorrect
    ./lib/pdf/reader/buffer.rb:41: Replace with T.let([0x00, 0x09, 0x0A, 0x0C, 0x0D, 0x20], T::Array[Integer])
    41 |    TOKEN_WHITESPACE=[0x00, 0x09, 0x0A, 0x0C, 0x0D, 0x20]
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

./lib/pdf/reader/buffer.rb:42: Constants must have type annotations with T.let when specifying # typed: strict https://srb.help/7027
    42 |    TOKEN_DELIMITER=[0x25, 0x3C, 0x3E, 0x28, 0x5B, 0x7B, 0x29, 0x5D, 0x7D, 0x2F]
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  Autocorrect: Use `-a` to autocorrect
    ./lib/pdf/reader/buffer.rb:42: Replace with T.let([0x25, 0x3C, 0x3E, 0x28, 0x5B, 0x7B, 0x29, 0x5D, 0x7D, 0x2F], T::Array[Integer])
    42 |    TOKEN_DELIMITER=[0x25, 0x3C, 0x3E, 0x28, 0x5B, 0x7B, 0x29, 0x5D, 0x7D, 0x2F]
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

./lib/pdf/reader/buffer.rb:55: Constants must have type annotations with T.let when specifying # typed: strict https://srb.help/7027
    55 |    WHITE_SPACE = [LF, CR, ' ']
                          ^^^^^^^^^^^^^
  Autocorrect: Use `-a` to autocorrect
    ./lib/pdf/reader/buffer.rb:55: Replace with T.let([LF, CR, ' '], T::Array[String])
    55 |    WHITE_SPACE = [LF, CR, ' ']
                          ^^^^^^^^^^^^^
Errors: 3

The proposed fix is to use T.let(). However I can't do that because it requires a runtime dependency on sorbet.

I tried using T.let() in my RBI file, similar to how we solved the instance variable issue in the linked question. However, that seems to have no effect for this error:

diff --git a/rbi/pdf-reader.rbi b/rbi/pdf-reader.rbi
index 113f183..f392b0a 100644
--- a/rbi/pdf-reader.rbi
+++ b/rbi/pdf-reader.rbi
@@ -81,7 +81,7 @@ module PDF
       CR = "\r"
       LF = "\n"
       CRLF = "\r\n"
-      WHITE_SPACE = [LF, CR, ' ']
+      WHITE_SPACE = T.let(T.unsafe(nil), T::Array[String])
       TRAILING_BYTECOUNT = 5000
 
       sig { returns(Integer) }

Extra Research

Interestingly, if I change the T.let() in the RBI file to something obviously wrong like:

diff --git a/rbi/pdf-reader.rbi b/rbi/pdf-reader.rbi
index 113f183..251d80d 100644
--- a/rbi/pdf-reader.rbi
+++ b/rbi/pdf-reader.rbi
@@ -81,7 +81,7 @@ module PDF
       CR = "\r"
       LF = "\n"
       CRLF = "\r\n"
-      WHITE_SPACE = [LF, CR, ' ']
+      WHITE_SPACE = T.let(T.unsafe(nil), T::Array[Integer])
       TRAILING_BYTECOUNT = 5000
 
       sig { returns(Integer) }

Then I get a type error:

$ srb tc
./lib/pdf/reader/buffer.rb:55: Expected T::Array[Integer] but found T::Array[String] for field https://srb.help/7013
    55 |    WHITE_SPACE = [LF, CR, " "]
                          ^^^^^^^^^^^^^
  Expected T::Array[Integer] for field defined here:
    ./lib/pdf/reader/buffer.rb:55:
    55 |    WHITE_SPACE = [LF, CR, " "]
            ^^^^^^^^^^^
  Got T::Array[String] originating from:
    ./lib/pdf/reader/buffer.rb:55:
    55 |    WHITE_SPACE = [LF, CR, " "]
                          ^^^^^^^^^^^^^

It seems like T.let() for constants in an RBI file isn't ignored, but it's not enough to satisfy the strict requirement for the type of constants to be defined.


Solution

  • # TLDR
    
    # NOTE: temporary fix, because this looks like a sorbet bug/feature;
    #       if you're getting inconsistent results use --max-threads=1
    $ srb typecheck --stress-incremental-resolver
    

    This is the minimal setup to reproduce the issue:

    # Gemfile
    source "https://rubygems.org"
    gem 'sorbet'
    
    # lib/my_gem.rb
    module MyGem
      WHITE_SPACE = [' ']
    end
    
    # sorbet/rbi/my_gem.rbi
    module MyGem
      # NOTE: Based on sorbet docs, this should tell sorbet the type of this constant
      #       and should be equivalent to doing this in `lib/my_gem.rb`:
      #       WHITE_SPACE = T.let([' '], T::Array[String])
      WHITE_SPACE = T.let(T.unsafe(nil), T::Array[String])
    end
    
    # srb --version 
    # Sorbet typechecker 0.5.10010 git d2cd1e574d70b4485d961fdf1f457948e4d3988d debug_symbols=true clean=0
    # ruby --version
    # ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux]
    

    Running a strict typecheck fails:

    $ srb typecheck --typed=strict --dir .
    lib/my_gem.rb:2: Constants must have type annotations with T.let when specifying # typed: strict https://srb.help/7027
         2 |  WHITE_SPACE = [' ']
                            ^^^^^
    

    There is a resolver flag that changes something somewhere, total guess on my part, that fixes the error:

    $ srb typecheck --help
    ...
    --stress-incremental-resolver
        Force incremental updates to discover resolver & namer bugs
    ...
    
    $ srb typecheck --stress-incremental-resolver --typed=strict --dir .                                          
    No errors! Great job.
    

    To verify that it actually does the typecheck, we can change the .rbi file to something incorrect:

    # sorbet/rbi/my_gem.rbi
    module MyGem
      WHITE_SPACE = T.let(T.unsafe(nil), T::Array[Integer])
    end
    
    $ srb typecheck --stress-incremental-resolver --typed=strict --dir .     
    lib/my_gem.rb:42: Expected T::Array[Integer] but found [String(" ")] for field https://srb.help/7013
        42 |  WHITE_SPACE = [' ']
                            ^^^^^
    

    Seems to work and looks like it resolves the constant type correctly all by itself => [String(" ")] which doesn't match [Integer].

    Digging a little deeper shows that sorbet parses/rewrites/desugars our file differently with the resolver flag:

    $ srb typecheck --print=resolve-tree --typed=strict --dir . 
    begin
      class <emptyTree><<C <root>>> < (::<todo sym>)
        nil
      end
      <emptyTree>
    end
    begin
      class <emptyTree><<C <root>>> < (::<todo sym>)
        begin
          module ::MyGem<<C MyGem>> < ()
    
            #
            # NOTE: This `Magic` bit in particular is not present with --stress-incremental-resolver
            #
            ::MyGem::WHITE_SPACE = ::<Magic>.<suggest-type>([" "])
    
          end
          ::Sorbet::Private::Static.keep_for_ide(::MyGem)
          <emptyTree>
        end
      end
      <emptyTree>
    end
    begin
      class <emptyTree><<C <root>>> < (::<todo sym>)
        begin
          module ::MyGem<<C MyGem>> < ()
            ::MyGem::WHITE_SPACE = begin
              ::Sorbet::Private::Static.keep_for_typechecking(::T::Array.[](::String))
              T.let(::T.unsafe(nil), AppliedType {
                klass = <C <U Array>>
                targs = [
                  <C <U Elem>> = String
                ]
              })
            end
          end
          ::Sorbet::Private::Static.keep_for_ide(::MyGem)
          <emptyTree>
        end
      end
      <emptyTree>
    end
    

    The <Magic>.<suggest-type> maps to this method:

    https://github.com/sorbet/sorbet/blob/0.5.10010.20220513160354-d2cd1e574/core/types/calls.cc#L2642

    That method works fine with plain string ' ', but throws an error with arrays [' '] even though everything looks resolved, indicated by ... found [String(" ")] ... above. Is it a feature or a bug is TBD.

    Also, converting WHITE_SPACE from a constant to a method could be a solution.