Merge pull request #9129 from reitermarkus/tc-docs
Improve type checking documentation.
This commit is contained in:
commit
ced0da159f
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -3,6 +3,7 @@
|
|||||||
- [ ] Have you added an explanation of what your changes do and why you'd like us to include them?
|
- [ ] Have you added an explanation of what your changes do and why you'd like us to include them?
|
||||||
- [ ] Have you written new tests for your changes? [Here's an example](https://github.com/Homebrew/brew/blob/HEAD/Library/Homebrew/test/PATH_spec.rb).
|
- [ ] Have you written new tests for your changes? [Here's an example](https://github.com/Homebrew/brew/blob/HEAD/Library/Homebrew/test/PATH_spec.rb).
|
||||||
- [ ] Have you successfully run `brew style` with your changes locally?
|
- [ ] Have you successfully run `brew style` with your changes locally?
|
||||||
|
- [ ] Have you successfully run `brew typecheck` with your changes locally?
|
||||||
- [ ] Have you successfully run `brew tests` with your changes locally?
|
- [ ] Have you successfully run `brew tests` with your changes locally?
|
||||||
- [ ] Have you successfully run `brew man` locally and committed any changes?
|
- [ ] Have you successfully run `brew man` locally and committed any changes?
|
||||||
|
|
||||||
|
|||||||
@ -107,7 +107,12 @@ module Homebrew
|
|||||||
srb_exec += ["--file", "../#{args.file}"] if args.file
|
srb_exec += ["--file", "../#{args.file}"] if args.file
|
||||||
srb_exec += ["--dir", "../#{args.dir}"] if args.dir
|
srb_exec += ["--dir", "../#{args.dir}"] if args.dir
|
||||||
end
|
end
|
||||||
Homebrew.failed = !system(*srb_exec)
|
success = system(*srb_exec)
|
||||||
|
return if success
|
||||||
|
|
||||||
|
$stderr.puts "Check #{Formatter.url("https://docs.brew.sh/Typechecking")} for " \
|
||||||
|
"more information on how to resolve these errors."
|
||||||
|
Homebrew.failed = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,47 +1,97 @@
|
|||||||
# Type Checking With Sorbet
|
# Type Checking With Sorbet
|
||||||
|
|
||||||
The majority of the code in Homebrew is written in Ruby which is a dynamic
|
The majority of the code in Homebrew is written in Ruby which is a dynamic
|
||||||
language. To avail the benefits of static type checking, we have set up Sorbet in
|
language. To avail the benefits of static type checking, we have set up
|
||||||
our codebase which provides the benefits of static type checking to dynamic languages
|
Sorbet in our codebase which provides the benefits of static type checking
|
||||||
like Ruby. <br> [Sorbet's Documentation](https://sorbet.org/docs/overview) is a
|
to dynamic languages like Ruby.
|
||||||
good place to get started if you want to dive deeper into Sorbet and it's abilities.
|
|
||||||
|
|
||||||
## Sorbet elements in the Homebrew Codebase
|
The [Sorbet Documentation] is a good place
|
||||||
|
to get started if you want to dive deeper into Sorbet and it's abilities.
|
||||||
|
|
||||||
The [`sorbet/`](https://github.com/Homebrew/brew/tree/master/Library/Homebrew/sorbet)
|
## Sorbet in the Homebrew Codebase
|
||||||
directory in `Library/Homebrew` consists of:
|
|
||||||
|
|
||||||
- The `rbi/` directory. It contains all Ruby Interface files, which help Sorbet to
|
### Inline Type Annotations
|
||||||
learn about constants, ancestors, and methods defined in ways it doesn’t understand
|
|
||||||
natively. RBI files for all gems are auto-generated using
|
|
||||||
[Tapioca](https://github.com/Shopify/tapioca#tapioca). We can also create a RBI
|
|
||||||
file to help Sorbet understand dynamic definitions.
|
|
||||||
For example: Sorbet assumes that `Kernel` is not necessarily included in our modules
|
|
||||||
and classes, hence we use RBI files to explicitly include the Kernel Module. Here is an
|
|
||||||
[example](https://github.com/Homebrew/brew/blob/72419630b4658da31556a0f6ef1dfa633cf4fe4f/Library/Homebrew/sorbet/rbi/homebrew.rbi#L3-L5)
|
|
||||||
in our codebase.
|
|
||||||
|
|
||||||
- The `config` file. It is actually a newline-separated list of arguments to pass to
|
To add type annotations to a class or module, we need to first extend it with
|
||||||
`srb tc`, the same as if they’d been passed at the command line. Arguments in the config
|
the `T::Sig` module (read this as `Type::Signature`). This adds the `sig`
|
||||||
file are always passed first (if it exists), followed by arguments provided on the
|
method which is used to annotate method signatures. Here's a simple example:
|
||||||
command line. We use it ignore the `Library/Homebrew/vendor` directory, which
|
|
||||||
contains gem definitions which we do not wish to type check.
|
|
||||||
|
|
||||||
- Every Ruby file in the codebase is divided into three strictness levels: false,
|
```ruby
|
||||||
true and strict. The `false` files only
|
class MyClass
|
||||||
report errors related to the syntax, constant resolution and correctness of the
|
extend T::Sig
|
||||||
method signatures, and not type errors. We use this file to override strictness
|
|
||||||
on a file-by-file basis. Our longtime goal is to move all `false` files to `true`
|
sig { params(name: String).returns(String) }
|
||||||
and start reporting type errors on those files as well. If you are making changes
|
def my_method(name)
|
||||||
that require adding a new ruby file, we would urge you to add it to `true` and work
|
"Hello, #{name}!"
|
||||||
out the resulting type errors. Read more about Sorbet's strictness levels
|
end
|
||||||
[here](https://sorbet.org/docs/static#file-level-granularity-strictness-levels).
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
With `params`, we specify that we have a parameter `name` which must be a
|
||||||
|
`String` and with `returns`, we specify that this method always returns
|
||||||
|
a `String`.
|
||||||
|
|
||||||
|
For more information on how to express more complex types, refer to the
|
||||||
|
official documentation:
|
||||||
|
|
||||||
|
- [Method Signatures](https://sorbet.org/docs/sigs)
|
||||||
|
- [Class Types](https://sorbet.org/docs/class-types)
|
||||||
|
- [Nilable Types](https://sorbet.org/docs/nilable-types)
|
||||||
|
- [Union Types](https://sorbet.org/docs/union-types)
|
||||||
|
|
||||||
|
### Ruby Interface Files (`.rbi`)
|
||||||
|
|
||||||
|
RBI files help Sorbet learn about constants, ancestors and methods
|
||||||
|
defined in ways it doesn’t understand natively. We can also create a
|
||||||
|
RBI file to help Sorbet understand dynamic definitions.
|
||||||
|
|
||||||
|
Sometimes it is necessary to explicitly include the `Kernel` module in
|
||||||
|
order for Sorbet to know that methods such as `puts` are available in
|
||||||
|
a given context. This is mostly necessary for modules since they can
|
||||||
|
be used in both `BasicObject`s (which don't include `Kernel`) and
|
||||||
|
`Object`s (which include `Kernel` by default). In this case, it is
|
||||||
|
necessary to create an `.rbi` file ([example]) since re-including the
|
||||||
|
`Kernel` module in actual code can break things.
|
||||||
|
|
||||||
|
Read more about RBI files [here](https://sorbet.org/docs/rbi).
|
||||||
|
|
||||||
|
[example]: https://github.com/Homebrew/brew/blob/61b79318ed089b5010501e2cbf163fd8e48e2dfc/Library/Homebrew/global.rbi
|
||||||
|
|
||||||
|
### The [`Library/Homebrew/sorbet`] Directory
|
||||||
|
|
||||||
|
[`Library/Homebrew/sorbet`]: https://github.com/Homebrew/brew/tree/master/Library/Homebrew/sorbet
|
||||||
|
|
||||||
|
- The `rbi` directory contains all Ruby Interface (`.rbi`) files
|
||||||
|
auto-generated by running `brew typecheck --update`:
|
||||||
|
|
||||||
|
- RBI files for all gems are generated using
|
||||||
|
[Tapioca](https://github.com/Shopify/tapioca#tapioca).
|
||||||
|
- Definitions for dynamic code (i.e. meta-programming) are generated using
|
||||||
|
`srb rbi hidden-definitions`.
|
||||||
|
- Definitions for missing constants are generated using `srb rbi todo`.
|
||||||
|
|
||||||
|
- The `config` file is a newline-separated list of arguments to pass to
|
||||||
|
`srb tc`, the same as if they’d been passed at the command-line. Arguments
|
||||||
|
in the config file are always passed first, followed by arguments provided
|
||||||
|
on the command-line. We use it to ignore Gem directories which we do not
|
||||||
|
wish to type check.
|
||||||
|
|
||||||
|
- Every Ruby file in the codebase has a magic `# typed: <level>` comment at the
|
||||||
|
top, where `<level>` is one of [Sorbet's strictness levels], usually `false`,
|
||||||
|
`true` or `strict`. The `false` files only report errors related to the
|
||||||
|
syntax, constant resolution and correctness of the method signatures, but no
|
||||||
|
type errors. Our long-term goal is to move all `false` files to `true` and
|
||||||
|
start reporting type errors on those files as well. Therefore, when adding
|
||||||
|
new files, you should ideally mark it with `# typed: true` and work out any
|
||||||
|
resulting type errors.
|
||||||
|
|
||||||
|
[Sorbet's strictness levels]: https://sorbet.org/docs/static#file-level-granularity-strictness-levels
|
||||||
|
|
||||||
## Using `brew typecheck`
|
## Using `brew typecheck`
|
||||||
|
|
||||||
When run without any arguments, `brew typecheck`, will run considering the strictness levels
|
When run without any arguments, `brew typecheck`, will run considering the strictness levels
|
||||||
set in each of the individual Ruby files in the core Homebrew codebase. However, when
|
set in each of the individual Ruby files in the core Homebrew codebase. However, when
|
||||||
typecheck is run on a specific file or directory, more errors may show up since Sorbet
|
it is run on a specific file or directory, more errors may show up since Sorbet
|
||||||
cannot resolve constants defined outside the scope of the specified file. These
|
cannot resolve constants defined outside the scope of the specified file. These
|
||||||
problems can be solved with RBI files. Currently `brew typecheck` provides `--quiet`, `--file`,
|
problems can be solved with RBI files. Currently `brew typecheck` provides `--quiet`, `--file`,
|
||||||
`--dir` and `--ignore` options but you can explore more options with `srb tc --help` and
|
`--dir` and `--ignore` options but you can explore more options with `srb tc --help` and
|
||||||
@ -51,35 +101,29 @@ passing them with `srb tc`.
|
|||||||
|
|
||||||
Sorbet reports type errors along with an error reference code, which can be used
|
Sorbet reports type errors along with an error reference code, which can be used
|
||||||
to look up more information on how to debug the error, or what causes the error in
|
to look up more information on how to debug the error, or what causes the error in
|
||||||
the Sorbet documentation. Here is how we debug some common type errors:
|
the [Sorbet Documentation]. Here is how to debug some common type errors:
|
||||||
|
|
||||||
* Using `T.reveal_type`. In files which are `true` or higher, if we wrap a variable
|
- Using `T.reveal_type`. In files which are `true` or higher, if we wrap a variable
|
||||||
or method call in `T.reveal_type`, Sorbet will show us what type it thinks that
|
or method call in `T.reveal_type`, Sorbet will show us what type it thinks that
|
||||||
variable has in the output of `srb tc`. This is particularly useful when writing
|
variable has in the output of `srb tc`. This is particularly useful when writing
|
||||||
[method signatures](https://sorbet.org/docs/sigs) and debugging. Make sure to
|
[method signatures](https://sorbet.org/docs/sigs) and debugging. Make sure to
|
||||||
remove this line from your code before committing your changes, since this is
|
remove this line from your code before committing your changes, since this is
|
||||||
just a debugging tool.
|
just a debugging tool.
|
||||||
|
|
||||||
* One of the most frequent errors that we've encountered is: `7003: Method does not exist.`
|
- One of the most frequent errors that we've encountered is: `7003: Method does not exist.`
|
||||||
Since Ruby is a very dynamic language, methods can be defined in ways Sorbet cannot
|
Since Ruby is a very dynamic language, methods can be defined in ways Sorbet cannot
|
||||||
see statically. In such cases, check if the method exists at runtime, if not, then
|
see statically. In such cases, check if the method exists at runtime, if not, then
|
||||||
Sorbet has caught a future bug! But, it is also possible that even though a method
|
Sorbet has caught a future bug! But, it is also possible that even though a method
|
||||||
exists at runtime, Sorbet cannot see it. In such cases, we use `*.rbi` files.
|
exists at runtime, Sorbet cannot see it. In such cases, we use
|
||||||
Read more about RBI files [here](https://sorbet.org/docs/rbi).
|
[`.rbi` files](#ruby-interface-files-rbi).
|
||||||
|
|
||||||
* Since Sorbet does not automatically assume that Kernel is to be included in Modules,
|
- Since Sorbet does not automatically assume that Kernel is to be included in Modules,
|
||||||
we may encounter many errors while trying to use methods like `puts`, `ohai`, `odebug` et cetera.
|
we may encounter many errors while trying to use methods like `puts`, `ohai`, `odebug` et cetera.
|
||||||
A simple workaround for this would be to add an extra `include Kernel` line in the
|
A simple workaround for this would be to add an extra `include Kernel` line in the
|
||||||
respective RBI file.
|
respective RBI file.
|
||||||
|
|
||||||
* The tips above are very generic and apply to lots of cases. For some common gotchas
|
- The tips above are very generic and apply to lots of cases. For some common gotchas
|
||||||
when using Sorbet, refer to the [Sorbet Error Reference](https://sorbet.org/docs/error-reference)
|
when using Sorbet, refer to the [Sorbet Error Reference](https://sorbet.org/docs/error-reference)
|
||||||
and [FAQ](https://sorbet.org/docs/faq).
|
and [FAQ](https://sorbet.org/docs/faq).
|
||||||
|
|
||||||
## Method Signatures
|
[Sorbet Documentation]: https://sorbet.org/docs/overview
|
||||||
|
|
||||||
Detailed explanation about why we use Method Signatures and its syntax can be found
|
|
||||||
[here](https://sorbet.org/docs/sigs). The only extra thing to keep in mind is that
|
|
||||||
we add method signatures to RBI files instead of the actual method definition in
|
|
||||||
the code. This way we preserve the original code structure and everything related to
|
|
||||||
Sorbet is kept within the `Library/Homebrew/sorbet` directory.
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user