From 0e9b664d85deb7db65dc8921abb5e18a67e59574 Mon Sep 17 00:00:00 2001 From: Markus Reiter Date: Fri, 13 Nov 2020 15:57:13 +0100 Subject: [PATCH] Update type checking documentation. --- docs/Typechecking.md | 160 +++++++++++++++++++++++++++---------------- 1 file changed, 102 insertions(+), 58 deletions(-) diff --git a/docs/Typechecking.md b/docs/Typechecking.md index 9947b3b76f..f93d126139 100644 --- a/docs/Typechecking.md +++ b/docs/Typechecking.md @@ -1,47 +1,97 @@ # Type Checking With Sorbet 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.
[Sorbet's Documentation](https://sorbet.org/docs/overview) is a -good place to get started if you want to dive deeper into Sorbet and it's abilities. +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. -## 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) -directory in `Library/Homebrew` consists of: +## Sorbet in the Homebrew Codebase -- The `rbi/` directory. It contains all Ruby Interface files, which help Sorbet to -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. +### Inline Type Annotations -- The `config` file. It is actually 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 (if it exists), followed by arguments provided on the -command line. We use it ignore the `Library/Homebrew/vendor` directory, which -contains gem definitions which we do not wish to type check. +To add type annotations to a class or module, we need to first extend it with +the `T::Sig` module (read this as `Type::Signature`). This adds the `sig` +method which is used to annotate method signatures. Here's a simple example: -- Every Ruby file in the codebase is divided into three strictness levels: false, -true and strict. The `false` files only -report errors related to the syntax, constant resolution and correctness of the -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` -and start reporting type errors on those files as well. If you are making changes -that require adding a new ruby file, we would urge you to add it to `true` and work -out the resulting type errors. Read more about Sorbet's strictness levels -[here](https://sorbet.org/docs/static#file-level-granularity-strictness-levels). +```ruby +class MyClass + extend T::Sig + + sig { params(name: String).returns(String) } + def my_method(name) + "Hello, #{name}!" + end +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: ` comment at the + top, where `` 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` 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 -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 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 @@ -51,35 +101,29 @@ passing them with `srb tc`. 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 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 -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. +- 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 + 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. -* 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. -Read more about RBI files [here](https://sorbet.org/docs/rbi). +- 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). -* 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. -A simple workaround for this would be to add an extra `include Kernel` line in the -respective RBI file. +- 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. + A simple workaround for this would be to add an extra `include Kernel` line in the + respective RBI file. -* 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). +- 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). -## Method Signatures - -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. +[Sorbet Documentation]: https://sorbet.org/docs/overview