Skip to content

Latest commit

 

History

History
520 lines (386 loc) · 22 KB

faq.md

File metadata and controls

520 lines (386 loc) · 22 KB
id title sidebar_label
faq
Frequently Asked Questions
FAQ

Why does Sorbet think this is nil? I just checked that it's not!

Sorbet implements a flow-sensitive type system, but there are some limitations. In particular, Sorbet does not assume a method called twice returns the same thing each time!

See Limitations of flow-sensitivity for a fully working example.

It looks like Sorbet's types for the stdlib are wrong.

Sorbet uses RBI files to annotate the types for the Ruby standard library. Every RBI file for the Ruby standard library is maintained by hand. This means they're able to have fine grained types, but it also means that sometimes they're incomplete or inaccurate.

The Sorbet team is usually too busy to respond to requests to fix individual bugs in these type annotations for the Ruby standard library. That means there are two options:

  1. Submit a pull request to fix the type annotations yourself.

    Every RBI file for the Ruby standard library lives here in the Sorbet repo. Find which RBI file you need to edit, and submit a pull request on GitHub with the changes.

    This is the preferred option, because then every Sorbet user will benefit.

  2. Use an escape hatch to opt out of static type checks.

    Use this option if you can't afford to wait for Sorbet to be fixed and published. (Sorbet publishes new versions to RubyGems nightly).

If you're having problems making a change to Sorbet, we're happy to help on Slack! See the Community page for an invite link.

What's the difference between T.let, T.cast, and T.unsafe?

→ Type Assertions

What's the type signature for a method with no return?

sig {void}

→ Method Signatures

How should I add types to methods defined with attr_reader?

Sorbet has special knowledge for attr_reader, attr_writer, and attr_accessor. To add types to methods defined with any of these helpers, put a method signature above the declaration, just like any other method:

# typed: true
class A
  extend T::Sig

  sig {returns(Integer)}
  attr_reader :reader

  sig {params(writer: Integer).returns(Integer)}
  attr_writer :writer

  # For attr_accessor, write the sig for the reader portion.
  # (Sorbet will use that to write the sig for the writer portion.)
  sig {returns(Integer)}
  attr_accessor :accessor

  sig {void}
  def initialize
    @reader = T.let(0, Integer)
    @writer = T.let(0, Integer)
    @accessor = T.let(0, Integer)
  end
end

→ View on sorbet.run

Because Sorbet knows what attr_* methods define what instance variables, in typed: strict there are some gotchas that apply (which are the same for all other uses of instance variables): in order to use an instance variable, it must be initialized in the constructor, or be marked nilable (T.nilable(...)).

(→ Full example on sorbet.run)

What's the difference between Array and T::Array[…]?

Array is the built-in Ruby class for arrays. On the other hand, T::Array[...] is a Sorbet generic type for arrays. The ... must always be filled in when using it.

While Sorbet implicitly treats Array the same as T::Array[T.untyped], Sorbet will error when trying to use T::Array as a standalone type.

→ Generics in the Standard Library

How do I accept a class object, and not an instance of a class?

→ T.class_of

How do I write a signature for initialize?

When defining initialize for a class, we strongly encourage that you use .void. This reminds people instantiating your class that they probably meant to call .new, which is defined on every Ruby class. Typing the result as .void means that it's not possible to do anything with the result of initialize.

→ Method Signatures

How do I override ==? What signature should I use?

Your method should accept BasicObject and return T::Boolean.

Unfortunately, not all BasicObjects have is_a?, so we have to do one extra step in our == function: check whether Object === other. (In Ruby, == and === are completely unrelated. The latter has to do with case subsumption). The idiomatic way to write Object === other in Ruby is to use case:

case other
when Object
  # ...
end

Here's a complete example that uses case to implement ==:

→ View on sorbet.run

I use T.must a lot with arrays and hashes. Is there a way around this?

Hash#[] and Array#[] return a nilable type because in Ruby, accessing a Hash or Array returns nil if the key does not exist or is out-of-bounds. If you would rather raise an exception than handle nil, use the #fetch method:

[0, 1, 2].fetch(3) # IndexError: index 3 outside of array bounds

Sigs are vague for stdlib methods that accept keyword arguments & have multiple return types

You might notice this when calling Array#sample, Pathname#find, or other stdlib methods that accept a keyword argument and can have different return types based on arguments:

T.reveal_type([1, 2, 3].sample) # Revealed type: T.nilable(T.any(Integer, T::Array[Integer]))

The sig in Sorbet's stdlib is quite wide, since it has to cover every possible return type. Sorbet does not have good support for this for methods that accept keyword arguments. #37 is the original report of this. #2248 has an explanation of why this is hard to fix in Sorbet.

To work around this, you'll need to use T.cast.

item = T.cast([1, 2, 3].sample, Integer)
T.reveal_type(item) # Revealed type: Integer

In some cases - for example, with complex number conversion methods in Kernel - the Sorbet team has chosen to ship technically incorrect RBIs that are much more pragmatic. See #1144 for an example. You can do the same for other cases you find annoying, but you take on the risk of always need to call the method correctly based on your new sig.

For example, if you are confident you'll never call Array#sample on an empty array, use this RBI to not have to worry about nil returns.

class Array
  extend T::Sig

  sig do
    params(arg0: Integer, random: Random::Formatter)
    .returns(T.any(Elem, T::Array[Elem]))
  end
  def sample(arg0=T.unsafe(nil), random: T.unsafe(nil)); end
end

Or if you never call it with an argument (you always do [1,2,3].sample, never [1,2,3].sample(2)), use this RBI to always get an element (not an array) as your return type:

class Array
  extend T::Sig

  sig do
    params(arg0: Integer, random: Random::Formatter)
    .returns(Elem) # or T.nilable(Elem), to also support empty arrays
  end
  def sample(arg0=T.unsafe(nil), random: T.unsafe(nil)); end
end

Overriding stdlib RBIs can make type checking less safe, since Sorbet will now have an incorrect understanding of how the stdlib behaves.

Another alternative is to define new methods that are stricter about arguments, and use these in place of stdlib methods:

class Array
  extend T::Sig

  sig { returns(Elem) } # or T.nilable(Elem) unless you're confident this is never called on empty arrays
  def sample_one
    T.cast(sample, Elem)
  end

  sig { params(n: Integer).returns(T::Array[Elem]) }
  def sample_n(n)
    T.cast(sample(n), T::Array[Elem])
  end
end

Why is super untyped, even when the parent method has a sig?

Sorbet can't know what the "parent method" is 100% of the time. For example, when calling super from a method defined in a module, the super method will be determined only once we know which class or module this module has been mixed into. That's a lot of words, so here's an example:

→ View on sorbet.run

To typecheck this example, Sorbet would have to typecheck MyModule#foo multiple times, once for each place that method might be used from, or place restrictions on how and where this module can be included.

Sorbet might adopt a more sophisticated approach in the future, but for now it falls back to treating super as a method call that accepts anything and returns anything.

Does Sorbet work with Rake and Rakefiles?

Kind of, with some effort. Rake monkey patches the global main object (i.e., top-level code) to extend their DSL, which Sorbet cannot understand:

# -- from lib/rake/dsl_definition.rb --

...

# Extend the main object with the DSL commands. This allows top-level
# calls to task, etc. to work from a Rakefile without polluting the
# object inheritance tree.
self.extend Rake::DSL

lib/rake/dsl_definition.rb

Sorbet cannot model that a single instance of an object (in this case main) has a different inheritance hierarchy than that instance's class (in this case Object).

To get around this, factor out all tasks defined the Rakefile that should be typechecked into an explicit class in a separate file, something like this:

# -- my_rake_tasks.rb --

# (1) Make a proper class inside a file with a *.rb extension
class MyRakeTasks
  # (2) Explicitly extend Rake::DSL in this class
  extend Rake::DSL

  # (3) Define tasks like normal:
  task :test do
    puts 'Testing...'
  end

  # ... more tasks ...
end

# -- Rakefile --

# (4) Require that file from the Rakefile
require_relative './my_rake_tasks'

For more information, see this StackOverflow question.

How do I upgrade Sorbet?

Sorbet has not reached version 1.0 yet. As such, it will make breaking changes constantly, without warning.

To upgrade a patch level (e.g., from 0.4.4314 to 0.4.4358):

bundle update sorbet sorbet-runtime
# also update plugins like sorbet-rails, if any

# For plugins like sorbet-rails, see their docs, eg.
https://github.com/chanzuckerberg/sorbet-rails#initial-setup

# Optional: Suggest new, stronger sigils (per-file strictness
# levels) when possible. Currently, the suggestion process is
# fallible, and may suggest downgrading when it's not necessary.
bundle exec srb rbi suggest-typed

What platforms does Sorbet support?

The sorbet-runtime gem is currently only tested on Ruby 2.6 and Ruby 2.7. It is known to not support Ruby 2.4. Feel free to report runtime issues for any current or future Ruby version.

The sorbet-static gem is known to support Ruby 2.4, Ruby 2.5, Ruby 2.6, and Ruby 2.7 to a minimum level (i.e., it can at least parse syntax introduced in those versions). Some language features are typed more strictly than others (generally, language features in newer Ruby versions have looser type support). This is not by design, just by convenience. Feel free to open feature requests that various (new or old) language features be typed more strictly.

Sorbet bundles RBI files for the standard library. In Ruby the standard library changes with the Ruby version being used, but Sorbet only ships one set of RBI definitions for the standard library. In particular, Sorbet's RBI files for the standard library might reflect classes, methods, or APIs that are only available in a version of Ruby newer than the one used to run a given project. You will have to rely on (runtime) test suites to verify that your project does not use new standard library APIs with an old Ruby version.

The sorbet-static gem is only tested on macOS 10.14 (Mojave) and Ubuntu 18 (Bionic Beaver). There is currently no Windows support. We expect sorbet-static to work as far back as macOS 10.10 (Yosemite), as far forward as macOS 11.0 Big Sur, and on most Linux distributions using glibc.

We do not test nor publish prebuilt binaries for macOS on Apple Silicon. We have reports that it doesn't work, but no one on the Sorbet team has access to Apple Silicon-based macOS machines, so we have been unable to diagnose nor fix any problems. If you are interested in working on this, feel free to reach out in the #internals channel on our Sorbet Slack.

The sorbet gem has runtime dependencies on git and bash.

Combined, these points mean that if you are using one of the official minimal Ruby Docker images (which are based on Apline Linux), you will need to install some support libraries:

FROM ruby:2.6-alpine

RUN apk add --no-cache --update \
    git \
    bash \
    ca-certificates \
    wget

ENV GLIBC_RELEASE_VERSION 2.30-r0
RUN wget -nv -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub && \
    wget -nv https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_RELEASE_VERSION}/glibc-${GLIBC_RELEASE_VERSION}.apk && \
    apk add glibc-${GLIBC_RELEASE_VERSION}.apk && \
    rm /etc/apk/keys/sgerrand.rsa.pub && \
    rm glibc-${GLIBC_RELEASE_VERSION}.apk

Does Sorbet support ActiveRecord (and Rails?)

Sorbet doesn't support Rails, but Tapioca can generate RBI files for it.

If you're not using Tapioca, you could use sorbet-rails, a community-maintained project which can help generate RBI files for certain Rails constructs.

Also see the Community page for more community-maintained projects!

Can I convert from YARD docs to Sorbet signatures?

Sord is a community-maintained project which can generate Sorbet RBI files from YARD docs.

Also see the Community page for more community-maintained projects!

When Ruby 3 gets types, what will the migration plan look like?

The Sorbet team is actively involved in the Ruby 3 working group for static typing. There are some things we know and something we don't know about Ruby 3.

Ruby 3 plans to ship type annotations for the standard library. These type annotations for the standard library will live in separate Ruby Signature (RBS) files, with the *.rbs extension. The exact syntax is not yet finalized. When the syntax is finalized, Sorbet intends to ingest both RBS and RBI formats, so that users can choose their favorite.

Ruby 3 has no plans to change Ruby's syntax. To have type annotations for methods live in the same place as the method definition, the only option will be to continue using Sorbet's method signatures. As such, the Sorbet docs will always use RBI syntax in examples, because the syntax is the same for signatures both within a Ruby file and in external RBI files.

Ruby 3 has no plans to ship a type checker for RBS annotations. Instead, Ruby 3 plans to ship a type profiler, which will attempt to guess signatures for code without signatures. The only way to get type checking will be to use third party tools, like Sorbet.

Ruby 3 plans to ship no specification for what the type annotations mean. Each third party type checker and the Ruby 3 type profiler will be allowed to ascribe their own meanings to individual type annotations. When there are ambiguities or constructs that one tool doesn't understand, it should fall back to T.untyped (or the equivalent in whatever RBS syntax decides to use for this construct).

Ruby 3 plans to seed the initial type annotations for the standard library from Sorbet's extensive existing type annotations for the standard library. Sorbet already has great type annotations for the standard library in the form of RBI files which are used to type check millions of lines of production Ruby code every day.

From all of this, we have every reason to believe that users of Sorbet will have a smooth transition to Ruby 3:

  • You will be able to either keep using Sorbet's RBI syntax or switch to using RBS syntax.
  • The type definitions for the standard library will mean the same (because they will have come from Sorbet!) but have a different syntax.
  • For inline type annotations with Ruby 3, you will have to continue using Sorbet's sig syntax, no different from today.

For more information, watch this section from Matz's RubyConf 2019 keynote, which talks about his plans for typing in Ruby 3.

Can I use Sorbet for duck typed code?

No. You can use an interface instead, or T.untyped if you do not control all of the code.

Duck typing (or, more formally, Structural typing) specifies types by their structure. For example, Rack middleware accepts any object that has a call method which takes one argument and returns a tuple representing an HTTP response.

Sorbet does not support duck typing either for static analysis or runtime checking.

How do I see errors from a single file, not the whole project?

Fundamentally, Sorbet typechecks a single project at a time. Unlike other languages and compilers, it does not process files in a codebase one file at a time. This is largely due to the fact that Ruby does not have any sort of file-level import mechanism—in Ruby, code can usually be defined anywhere and used from anywhere else—and this places constraints on how Sorbet must be implemented.

Because Sorbet typechecks a single project at a time, it does not allow filtering the list of errors to a single file. Sometimes though, the number of errors Sorbet reports can be overwhelming, especially when adopting Sorbet in a new codebase. In these cases, the best way to proceed is as follows:

  1. Put every file in the codebase at # typed: false (see Strictness Levels). Note that the default strictness level when there is no explicit # typed: comment in a file is # typed: false. The --typed=false command line flag can be used to forcibly override every file's strictness level to # typed: false, regardless of what's written in the file.

    Forcing every file to # typed: false will silence all but the most critical Sorbet errors throughout the project.

  2. Proceed to fix these errors. If there are still an overwhelming number of errors, tackle the errors that are reported earlier first. Sorbet's design means that errors discovered early in the early phases of typechecking can cause redundant errors in later phases.

  3. Once there are no errors in any files at # typed: false, proceed to upgrade individual files to # typed: true. Only new errors will be reported in these # typed: true files, and not in any other files. Repeatedly upgrade as many individual files as is preferred. Note that many Sorbet codebases start off with all files at # typed: false and gradually (usually organically) shrink the number of # typed: false files over time.

If for some reason it's still imperative to limit the Sorbet error output to only those errors coming from a single file and the steps above are not acceptable, we recommend post processing the errors reported by Sorbet with tools like grep.

There are also third-party tools that offer the ability to sort and filter Sorbet's errors, like spoom.