| 
									
										
										
										
											2020-10-17 21:51:28 +01:00
										 |  |  |  | # Type Checking With Sorbet
 | 
					
						
							| 
									
										
										
										
											2020-08-18 19:21:29 +05:30
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | The majority of the code in Homebrew is written in Ruby which is a dynamic | 
					
						
							| 
									
										
										
										
											2020-11-13 15:57:13 +01:00
										 |  |  |  | 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. | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | The [Sorbet Documentation] is a good place | 
					
						
							|  |  |  |  | to get started if you want to dive deeper into Sorbet and it's abilities. | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | ## Sorbet in the Homebrew Codebase
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | ### Inline Type Annotations
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 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: | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | ```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: <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 | 
					
						
							| 
									
										
										
										
											2020-08-18 19:21:29 +05:30
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | ## Using `brew typecheck`
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | When run without any arguments, `brew typecheck`, will run considering the strictness levels | 
					
						
							| 
									
										
										
										
											2020-10-17 21:35:28 +01:00
										 |  |  |  | set in each of the individual Ruby files in the core Homebrew codebase. However, when | 
					
						
							| 
									
										
										
										
											2020-11-13 15:57:13 +01:00
										 |  |  |  | it is run on a specific file or directory, more errors may show up since Sorbet | 
					
						
							| 
									
										
										
										
											2020-10-17 21:35:28 +01:00
										 |  |  |  | cannot resolve constants defined outside the scope of the specified file. These | 
					
						
							| 
									
										
										
										
											2020-10-17 22:02:25 +01:00
										 |  |  |  | problems can be solved with RBI files. Currently `brew typecheck` provides `--quiet`, `--file`, | 
					
						
							| 
									
										
										
										
											2020-10-17 21:35:28 +01:00
										 |  |  |  | `--dir` and `--ignore` options but you can explore more options with `srb tc --help` and | 
					
						
							|  |  |  |  | passing them with `srb tc`. | 
					
						
							| 
									
										
										
										
											2020-08-18 19:21:29 +05:30
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | ## Resolving Type Errors
 | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | 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 | 
					
						
							| 
									
										
										
										
											2020-11-13 15:57:13 +01:00
										 |  |  |  | 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. | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | - 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. | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | - 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). | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | [Sorbet Documentation]: https://sorbet.org/docs/overview |