id | title |
---|---|
static |
Enabling Static Checks |
This doc will cover how to enable and disable the static checks that srb
reports. Specifically, we'll look at how to toggle these checks...
- ...within an entire file.
- ...for a particular method.
- ...for a single argument of a method.
- ...at a specific call site.
Before we get into the mechanics of how to toggle Sorbet's static checks, let's slap on a quick warning label:
Warning: Think carefully before disabling static checks!
When run at the command line, srb
roughly works like this:
- Read, parse, and analyze every Ruby file in a project.
- Generate a list of errors within the project.
- Display all errors to the user.
However, in step (3), most kinds of errors are silenced by default, instead of
being reported. To opt into more checks, we use # typed:
sigils1.
A # typed:
sigil is a comment placed at the top of a Ruby file, indicating to
srb
which errors to report and which to silence. These are the available
sigils, each defining a strictness level:
All errors silenced | All errors reported | |||
---|---|---|---|---|
typed: ignore |
typed: false |
typed: true |
typed: strict |
typed: strong |
Each strictness level reports all errors at lower levels, plus new errors:
-
At
# typed: ignore
, the file is not even read by Sorbet, and so no errors at all are reported in that file. Note: ignoring a file can cause errors to appear in other files, because that other file references something defined in an ignored file. We recommend pushing the entire project to out ofignore
(at Stripe, 100% of non-test files are not ignored.) -
At
# typed: false
, only errors related to syntax, constant resolution and correctness ofsig
s are reported. Fixing these errors is the baseline for adopting Sorbet in a new codebase, and provides value even before adding type annotations.# typed: false
is the default for files without sigils.(Note: Sorbet will still parse and store method signatures even in
# typed: false
files, for use whenever it sees a call to that method in# typed: true
or higher files.# typed: false
only prevents that method's body from being typechecked, which means that signatures in# typed: false
files are simply assumed to hold.) -
At
# typed: true
, things that would normally be called "type errors" are reported. This includes calling a non-existent method, calling a method with mismatched argument counts, using variables inconsistently with their types, etc. -
At
# typed: strict
, Sorbet no longer implicitly marks things as being dynamically typed. At this level all methods must have sigs, and all constants and instance variables must have explicitly annotated types. This is analogous to TypeScript'snoImplicitAny
flag.
- At
# typed: strong
, Sorbet no longer allowsT.untyped
as the intermediate result of any method call. This effectively means that Sorbet knew the type statically for 100% of calls within a file. Currently, this sigil is rarely used—usually the only files that are# typed: strong
are RBI files and files with empty class definitions. Most Ruby files that actually do interesting things will have errors in# typed: strong
. Support fortyped: strong
files is minimal, as Sorbet changes regularly and new features often bring newT.untyped
intermediate values.
To recap: adding one of these comments to the top of a Ruby file controls which
errors srb
reports or silences in that file. The strictness level only affects
which errors are reported.
Note: Method signatures in
# typed: false
files are still parsed and used by Sorbet if that method is called in other files. Specifically, adding a signature in a# typed: false
file might introduce new type errors if it's called from a# typed: true
file.
After changing the sigil of an ignore
d file, you must run
srb rbi hidden-definitions
.
The last time hidden definitions were generated, the file was ignore
d, sorbet
didn't load it, so anything in it was treated as "hidden" and defined in
sorbet/rbi/hidden-definitions/hidden.rbi
. Now that sorbet is allowed to load
it, those definitions must be removed from hidden.rbi
.
If you don't regenerate hidden.rbi
, you are likely to encounter
error 4010 (two definitions of the same method).
After enabling # typed: true
in some files, we can opt individual methods into
even more checks by adding signatures (or sig
s) to them. For example srb
reports no errors in this file:
# typed: true
def log_env(env, key)
puts "LOG: #{key} => #{env[key]}"
end
log_env({timeout_len: 2000}, 'timeout_len')
It would be nice to be warned about our call to log_env
, because we passed a
Hash with Symbol
keys but tried to ask about a String
key. To opt into this
check, we can add a signature to log_env
:
# typed: true
# (1) add this to get access to sig method
extend T::Sig
# (2) add a signature
sig {params(env: T::Hash[Symbol, Integer], key: Symbol).void}
def log_env(env, key)
puts "LOG: #{key} => #{env[key]}"
end
log_env({timeout_len: 2000}, 'timeout_len') # => Expected `Symbol` but found `String("timeout_len")`
In this example, we add a line like sig {...}
above the def log_env
line.
This is a Sorbet method signature---it declares the parameter and return types
of a method. By adding the sig
to log_env
, we opted this method into
additional checks. Now srb
reports this:
Expected `Symbol` but found `String("timeout_len")` for argument `key`
In our previous example, the sig
we added was a bit too restrictive for the
env
parameter:
T::Hash[Symbol, Integer]
^^^^^^^
For example, it's possible that we don't care about what's stored in the env
,
only that we access things in the env
with Symbol
keys. Right now though, an
env
of {user: 'jez'}
is a type error. In this case, we may want to opt out
of some static checks on this specific argument, without opting out the method
entirely. In this case, we can use T.untyped
:
# typed: true
extend T::Sig
sig {params(env: T::Hash[Symbol, T.untyped], key: Symbol).void}
def log_env(env, key)
puts "LOG: #{key} => #{env[key]}"
end
log_env({timeout_len: 2000, user: 'jez'}, :user) # ok
T.untyped
is a type that effectively makes a region of code act like it was
written in a dynamically typed language with no static checks. By using
T.untyped
in specific arguments within a sig
, we can silence most (but not
all) errors relating to that argument.
Warning: Be careful about opting out of static checks with
T.untyped
! Usually, we can rewrite our code to avoid silencing errors. For example, we could have refactored this code to use Shape types,Struct
s, or best of all: Typed Structs.
Using sigils and method signatures are the primary ways we opt into static
checks, and using T.untyped
we can opt a specific argument out of static
checks.
One last way we can opt out of static checks is with T.unsafe
. T.unsafe
is a
method (not a type) that returns its input unchanged and marks the result as
T.untyped
. Like how T.untyped
in a signature lets us opt an argument out of
type checks, T.unsafe
lets us opt out a local variable or method call.
This is frequently necessary when using Ruby's various "metaprogramming" features:
class A
define_method(:foo) { puts 'In A#foo' }
end
a = A.new
a.foo # => Method `foo` does not exist on `A`
T.unsafe(a).foo # ok
The call to T.unsafe
marks a
as T.untyped
, which causes Sorbet to silence
the error about the method foo
as missing. Note: sometimes what looks like a
local variable is actually a method call on self
in Ruby:
define_singleton_method(:foo) { puts 'A.foo'; true }
if foo # => Method `foo` does not exist on `T.class_of(A)`
puts 'succeeded'
end
In this case the tendency is to want to wrap the call to foo
in T.unsafe
(for example: T.unsafe(foo)
) but in fact what we need to wrap is the method's
receiver (the thing the method is being called on). When there is no
explicit receiver, it's self
in Ruby:
define_singleton_method(:foo) { puts 'A.foo'; true }
if T.unsafe(self).foo # ok
puts 'succeeded'
end
The call to T.unsafe(self)
evaluates to self
, but forces Sorbet to think it
has type T.untyped
, which permits calling any method. Using T.unsafe
we can
limit untyped code to a specific call site, and make explicit where we're
relying on dynamic behaviors.
-
The runtime component of Sorbet supports the static component. Learn how it works and how to best take advantage of it.
-
If you haven't already read this, learn what makes Sorbet different from other static type systems.
Footnotes
-
Google defines sigil as, "an inscribed or painted symbol considered to have magical power," and we like to think of types as pretty magical 🙂 ↩