A fast, powerful type checker designed for Ruby
# Sorbet This repository contains Sorbet, a fast, powerful type checker designed for Ruby. It aims to be easy to add to existing codebases with gradual types, and fast to respond with errors and suggestions. This README contains documentation specifically for contributing to Sorbet. You might also want to: - Read the [public Sorbet docs](https://sorbet.org/docs/overview) - Or even [edit the docs](#writing-docs) - Watch the [talks we've given](https://sorbet.org/en/community#talks) about Sorbet - Try the [Sorbet playground](https://sorbet.run) online If you are at Stripe, you might also want to see for docs about Stripe-specific development workflows and historical Stripe context. ## Table of Contents - [Sorbet user-facing design principles](#sorbet-user-facing-design-principles) - [Quickstart](#quickstart) - [Learning how Sorbet works](#learning-how-sorbet-works) - [Building Sorbet](#building-sorbet) - [Common Compilation Errors](#common-compilation-errors) - [Running Sorbet](#running-sorbet) - [Running the tests](#running-the-tests) - [Testing Sorbet against pay-server](#testing-sorbet-against-pay-server) - [Writing tests](#writing-tests) - [test_corpus tests](#test_corpus-tests) - [Expectation tests](#expectation-tests) - [CLI tests](#cli-tests) - [LSP tests](#lsp-tests) - [Testing "Find Definition" and "Find All References"](#testing-find-definition-and-find-all-references) - [Testing "Go to Type Definition"](#testing-go-to-type-definition) - [Testing hover](#testing-hover) - [Testing completion](#testing-completion) - [Testing workspace symbols (symbol search)](#testing-workspace-symbols-symbol-search) - [Testing "Go to Implementation"](#testing-go-to-implementation) - [Testing rename constant](#testing-rename-constant) - [Testing incremental type checking](#testing-incremental-type-checking) - [LSP recorded tests](#lsp-recorded-tests) - [Updating tests](#updating-tests) - [Debugging](#debugging) - [Writing docs](#writing-docs) - [Editor and environment](#editor-and-environment) - [Bazel](#bazel) - [Shell](#shell) - [Formatting files](#formatting-files) - [Editor setup for C++](#editor-setup-for-c) - [Editor setup for test/testdata](#editor-setup-for-testtestdata) - [@jez setup](#jez-setup) - [others](#others) ## Sorbet user-facing design principles Early in our project, we've defined some guidelines for how working with Sorbet should feel like. 1. **Explicit** We're willing to write annotations, and in fact see them as beneficial; they make code more readable and predictable. We're here to help readers as much as writers. 2. **Feel useful, not burdensome** While it is explicit, we are putting effort into making it concise. This shows in multiple ways: - error messages should be clear - verbosity should be compensated with more safety 3. **As simple as possible, but powerful enough** Overall, we are not strong believers in super-complex type systems. They have their place, and we need a fair amount of expressive power to model (enough) real Ruby code, but all else being equal we want to be simpler. We believe that such a system scales better, and—most importantly—is easier for our users to learn & understand. 4. **Compatible with Ruby** In particular, we don't want a new syntax. Existing Ruby syntax means we can leverage most of our existing tooling (editors, etc). Also, the point of Sorbet is to gradually improve an existing Ruby codebase. No new syntax makes it easier to be compatible with existing tools. 5. **Scales** On all axes: execution speed, number of collaborators, lines of code, codebase age. We work in large Ruby codebases, and they will only get larger. 6. **Can be adopted gradually** In order to make adoption possible at scale, we cannot require every team or project to adopt Sorbet all at once. Sorbet needs to support teams adopting it at different paces. ## Quickstart 1. Install the dependencies - `brew install bazel autoconf coreutils parallel` 2. Clone this repository - `git clone https://github.com/sorbet/sorbet.git` - `cd sorbet` 3. Build Sorbet - `./bazel build //main:sorbet --config=dbg` 4. Run Sorbet! - `bazel-bin/main/sorbet -e "42 + 'hello'"` ## Learning how Sorbet works We've documented the [internals of Sorbet](docs/internals.md) in a separate doc. Cross-reference between that doc and here to learn how Sorbet works and how to change it! [→ internals.md](docs/internals.md) There is also a talk online that describes Sorbet's high-level architecture and the reasons why it's fast: [→ Fast type checking for Ruby](https://sorbet.org/docs/talks/jvm-ls-2019) ## Building Sorbet There are multiple ways to build `sorbet`. This one is the most common: ``` ./bazel build //main:sorbet --config=dbg ``` This will build an executable in `bazel-bin/main/sorbet` (see "Running Sorbet" below). There are many options you can pass when building `sorbet`: - `--config=dbg` - Most common build config for development. - Good stack traces, runs all ENFORCEs. - `--config=sanitize` - Link in extra sanitizers, in particular: UBSan and ASan. - Catches most memory and undefined-behavior errors. - Substantially larger and slower binary. - `--config=debugsymbols` - (Included by `--config=dbg`) debugging symbols, and nothing else. - `--config=forcedebug` - Use more memory, but report even more sanity checks. - `--config=static-libs` - Forcibly use static linking (Sorbet defaults to dynamic linking for faster build times). - Sorbet already uses this option in release builds (see below). - `--config=release-mac` and `--config=release-linux` - Exact release configuration that we ship to our users. Independently of providing or omitting any of the above flags, you can turn on optimizations for any build: - `-c opt` - Enables `clang` optimizations (i.e., `-O2`) These args are not mutually exclusive. For example, a common pairing when debugging is ``` --config=dbg --config=sanitize ``` In `.bazelrc` you can find out what all these options (and others) mean. ### Common Compilation Errors **(Mac) `Xcode version must be specified to use an Apple CROSSTOOL`** This error typically occurs after an Xcode upgrade. Developer tools must be installed, the Xcode license must be accepted, and your active Xcode command line tools directory must point to an installed version of Xcode. The following commands should do the trick: ```shell # Install command line tools xcode-select --install # Ensure that the system finds command line tools in an active Xcode directory sudo xcode-select -s /Applications/Xcode.app/Contents/Developer # Accept the Xcode license. sudo xcodebuild -license # Clear bazel's cache, which may contain files generated from a previous # version of Xcode command line tools. bazel clean --expunge ``` **(Mac) `fatal error: 'math.h' file not found`** (or some other system header) This error can happen on Macs when the `/usr/include` folder is missing. The solution is to install macOS headers via the following package: macOS Mojave: ```shell open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg ``` macOS Catalina: ```shell sudo ln -s /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/* /usr/local/include/ ``` ## Running Sorbet Run Sorbet on an expression: ``` bazel-bin/main/sorbet -e "1 + false" ``` Run Sorbet on a file: ``` bazel-bin/main/sorbet foo.rb ``` Running `bazel-bin/main/sorbet --help` will show lots of options. These are the common ones for contributors: - `-p ` - Asks sorbet to print out any given intermediate representation. - See `--help` for available values of ``. - `--stop-after ` - Useful when there's a bug in a later phase, and you want to quit early to debug. - `-v`, `-vv`, `-vvv` - Show `logger` output (increasing verbosity) - `--max-threads=1` - Useful for determining if you're dealing with a concurrency bug or not. - `--wait-for-dbg` - Will freeze Sorbet on startup and wait for a debugger to attach - This is useful when you don't have control over launching the process (LSP) ## Running the tests To run all the tests: ``` bazel test //... --config=dbg ``` (The `//...` literally means "all targets".) To run a subset of the tests curated for faster iteration and development speed, run: ``` bazel test test --config=dbg ``` Note that in bazel terms, the second test is an alias for `//test:test`, so we're being a bit cute here. By default, all test output goes into files. To also print it to the screen: ``` bazel test //... --config=dbg --test_output=errors ``` If any test failed, you will see two pieces of information printed: ``` 1. //test:test_testdata/resolver/optional_constant 2. /private/var/tmp/.../test/test_testdata/resolver/optional_constant/test.log ``` 1. the test's target (in case you want to run just this test again with `bazel test `) 2. a (runnable) file containing the test's output To see the failing output, either: - Re-run `bazel test` with the `--test_output=errors` flag - Copy/paste the `*.log` file and run it (the output will open in `less`) ## Testing Sorbet against pay-server > This is specific to contributing to Sorbet at Stripe. If you are at Stripe and want to test your branch against pay-server, see . ## Writing tests We write tests by adding files to subfolders of the `test/` directory. Individual subfolders are "magic"; each contains specific types of tests. We aspire to have our tests be fully reproducible. > **C++ note**: In C++, hash functions are only required to produce the same > result for the same input within a single execution of a program. > > Thus, we expect all user-visible outputs to be explicitly sorted using a > key stable from one run to the next. There are many ways to test Sorbet, some "better" than others. We've ordered them below in order from most preferable to least preferable. And we always prefer some tests to no tests! ### test_corpus tests The first kind of test can be called either [test_corpus] tests or [testdata] tests, based on the name of the test harness or the folder containing these tests, respectively. [test_corpus]: test/pipeline_test_runner.cc [testdata]: test/testdata/ To create a test_corpus test, add any file `.rb` to `test/testdata`, in any folder depth. The file must either: - type check entirely, or - throw errors **only** on lines marked with a comment (see below). To mark that a line should have errors, append `# error: ` (the `` must match the raised error message). In case there are multiple errors on this line, add an `# error: ` on its own line just below. Error checks can optionally point to a range of characters rather than a line: ```ruby 1 + '' # error: `String` doesn't match `Integer` rescue Foo, Bar => baz # ^^^ error: Unable to resolve constant `Foo` # ^^^ error: Unable to resolve constant `Bar` ``` You can run this test with: ``` bazel test //test:test_PosTests/testdata/path/to/ ``` ### Expectation tests Each test_corpus test can be turned into an expectation test by optionally creating any number of `.rb..exp` files (where `` matches the name of the ruby file for this test). These files contain pretty printed representations of internal data structures, according to what would be printed by `-p `. The snapshot must exactly match the output generated by running `sorbet -p .rb` for the test to pass. You can run this test with: ``` bazel test //test:test_PosTests/testdata/path/to/ ``` Files that begin with a prefix and `__` will be run together. For example, `foo__1.rb` and `foo__2.rb` will be run together a