brew/docs/Typechecking.md

76 lines
6.2 KiB
Markdown
Raw Normal View History

# Type Checking With Sorbet
2022-04-19 21:28:17 -04:00
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 our codebase which provides the benefits of static type checking to dynamic languages like Ruby.
2020-11-13 15:57:13 +01:00
The [Sorbet Documentation] is a good place to get started if you want to dive deeper into Sorbet and its abilities.
2020-11-13 15:57:13 +01:00
## Sorbet in the Homebrew Codebase
2023-09-11 02:06:00 -04:00
### Inline type annotations
2020-11-13 15:57:13 +01:00
2023-04-24 19:13:25 -07:00
The `sig` method is used to annotate method signatures. Here's a simple example:
2020-11-13 15:57:13 +01:00
```ruby
class MyClass
sig { params(name: String).returns(String) }
def my_method(name)
"Hello, #{name}!"
end
end
```
With `params` we specify that there is a parameter `name` which must be a `String`, and with `returns` we specify that this method always returns a `String`.
2020-11-13 15:57:13 +01:00
2022-04-19 21:28:17 -04:00
For more information on how to express more complex types, refer to the official documentation:
2020-11-13 15:57:13 +01:00
2022-04-19 21:28:17 -04:00
- [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)
2020-11-13 15:57:13 +01:00
2023-09-11 02:06:00 -04:00
### Ruby interface files (`.rbi`)
2020-11-13 15:57:13 +01:00
[RBI files](https://sorbet.org/docs/rbi) help Sorbet learn about constants, ancestors and methods defined in ways it doesnt understand natively. We can also create an RBI file to help Sorbet understand dynamic definitions. Some of these files are autogenerated (see the next section) and some are manually written ([example]).
2020-11-13 15:57:13 +01:00
There are also a very small number of files that Homebrew loads before sorbet-runtime, such as `utils/gems.rb`. Those files cannot have type signatures alongside the code itself, so RBI files are used there instead to retain static type checking.
2020-11-13 15:57:13 +01:00
[example]: https://github.com/Homebrew/brew/blob/9d4000b15cb6ffa8c11f49372f7016d05aaa0851/Library/Homebrew/extend/ENV.rbi
2020-11-13 15:57:13 +01:00
2023-09-11 02:06:00 -04:00
### The [`Library/Homebrew/sorbet`] directory
2020-11-13 15:57:13 +01:00
[`Library/Homebrew/sorbet`]: https://github.com/Homebrew/brew/tree/master/Library/Homebrew/sorbet
2022-04-19 21:28:17 -04:00
- The `rbi` directory contains all Ruby Interface (`.rbi`) files auto-generated by running `brew typecheck --update`:
2020-11-13 15:57:13 +01:00
- `gems`: RBI files for all gems are generated using [Tapioca](https://github.com/Shopify/tapioca#tapioca).
- `dsl`: RBI files autogenerated by our [Tapioca compilers](https://github.com/Homebrew/brew/tree/master/Library/Homebrew/sorbet/tapioca/compilers).
- `parlour.rbi`: RBI files generated by Parlour that have not yet been migrated to Tapioca compilers
- `upstream.rbi`: This file is not auto-generated and is a manually written file that contains temporary workarounds for upstream Sorbet issues. This file is typically empty.
2020-11-13 15:57:13 +01:00
- The `config` file is a newline-separated list of arguments to pass to `srb tc`, the same as if theyd been passed on 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.
2020-11-13 15:57:13 +01:00
2022-04-19 21:28:17 -04:00
- 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.
2020-11-13 15:57:13 +01:00
2022-04-19 21:28:17 -04:00
[Sorbet's strictness levels]: https://sorbet.org/docs/static#file-level-granularity-strictness-levels
## Using `brew typecheck`
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 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 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 pass them with `srb tc`.
## Resolving Type Errors
2022-04-19 21:28:17 -04:00
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 the [Sorbet Documentation]. Here is how to debug some common type errors:
- Using `T.reveal_type`: in files which are `true` or higher, by wrapping a variable 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 [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 just a debugging tool.
2022-04-19 21:28:17 -04:00
- 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 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 exists at runtime, Sorbet cannot see it. In such cases, we use [`.rbi` files](#ruby-interface-files-rbi).
2022-04-19 21:28:17 -04:00
- 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` etc. There are generally two approaches to fixing this:
- If you are using `module_function` but never run `include ModuleName` anywhere, remove the `module_definition` and convert all methods to class methods (prepend the name with `self.`)
- If you do include the module elsewhere, add a `requires_ancestor` to the module defining what types of classes this module can be included in. This may be as simple as a `requires_ancestor { Kernel }`, which most classes are a descendant from.
2022-04-19 21:28:17 -04:00
- 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) and [FAQ](https://sorbet.org/docs/faq).
2020-11-13 15:57:13 +01:00
[Sorbet Documentation]: https://sorbet.org/docs/overview