diff --git a/README.md b/README.md index f7e7caefa..e931d7abb 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,48 @@ > :warning: **Note**: This software is currently under active development. The API and interface should be considered unstable until a v1.0.0 release. -# Tapioca +

+ Tapioca logo +

-![Build Status](https://github.com/Shopify/tapioca/workflows/CI/badge.svg) - -Tapioca is a library used to generate RBI (Ruby interface) files for use with [Sorbet](https://sorbet.org). RBI files provide the structure (classes, modules, methods, parameters) of the gem/library to Sorbet to assist with typechecking. - -As yet, no gem exports type information in a consumable format and it would be a huge effort to manually maintain such an interface file for all the gems that your codebase depends on. Thus, there is a need for an automated way to generate the appropriate RBI file for a given gem. The `tapioca` gem, developed at Shopify, is able to do exactly that to almost 99% accuracy. It can generate the definitions for all statically defined types and most of the runtime defined types exported from Ruby gems (non-Ruby gems are not handled yet). - -When you run `tapioca gem` in a project, `tapioca` loads all the gems that are in your dependency list from the Gemfile into memory. It then performs runtime introspection on the loaded types to understand their structure and generates an appropriate RBI file for each gem with a versioned filename. - -## Manual gem requires - -For gems that have a normal default `require` and load all of their constants through such a require, everything works seamlessly. However, for gems that are marked as `require: false` in the Gemfile, or for gems that export optionally loaded types via different requires, where a single require does not load the whole gem code into memory, `tapioca` will not be able to load some of the types into memory and, thus, won't be able to generate complete RBIs for them. For this reason, we need to keep a small external file named `sorbet/tapioca/require.rb` that is executed after all the gems in the Gemfile have been required and before generation of gem RBIs have started. This file is responsible for adding the requires for additional files from gems, which are not covered by the default require. - -For example, suppose you are using the class `BetterHtml::Parser` exported from the `better_html` gem. Just doing a `require "better_html"` (which is the default require) does not load that type: - -```shell -$ bundle exec pry -[1] pry(main)> require 'better_html' -=> true -[2] pry(main)> BetterHtml -=> BetterHtml -[3] pry(main)> BetterHtml::Parser -NameError: uninitialized constant BetterHtml::Parser -from (pry):3:in `__pry__` -[4] pry(main)> require 'better_html/parser' -=> true -[5] pry(main)> BetterHtml::Parser -=> BetterHtml::Parser -``` - -In order to make sure that `tapioca` can reflect on that type, we need to add the line `require "better_html/parser"` to the `sorbet/tapioca/require.rb` file. This will make sure `BetterHtml::Parser` is loaded into memory and a type annotation is generated for it in the `better_html.rbi` file. If this extra `require` line is not added to `sorbet/tapioca/require.rb` file, then the definition for that type will be missing from the RBI file. +# Tapioca - The swiss army knife of RBI generation -If you ever run into a case, where you add a gem or update the version of a gem and run `tapioca gem` but don't have some types you expect in the generated gem RBI files, you will need to make sure you have added the necessary requires to the `sorbet/tapioca/require.rb` file. - -You can use the command `tapioca require` to auto-populate the `sorbet/tapioca/require.rb` file with all the requires found -in your application. Once the file generated, you should review it, remove all unnecessary requires and commit it. - -## How does tapioca compare to "srb rbi gems" ? +![Build Status](https://github.com/Shopify/tapioca/workflows/CI/badge.svg) -[Please see the detailed answer on our wiki](https://github.com/Shopify/tapioca/wiki/How-does-tapioca-compare-to-%22srb-rbi-gems%22-%3F) +Tapioca makes it easy to work with [Sorbet](https://sorbet.org) in your codebase. It surfaces types and methods from many sources that Sorbet cannot otherwise see – such as gems, Rails and other DSLs – compiles them into [RBI files](https://sorbet.org/docs/rbi) and makes it easy for you to add gradual typing to your application. + +**Features**: + +* Easy installation and configuration +* Generation of RBI files for the gems used in your application + * Automatic generation from your application's Gemfile + * Importing of signatures from the source code of gems + * Importing of documentation from the source code of gems + * Synchronization validation for your CI +* Generation of RBI files for various DSL patterns that relies on meta-programming + * Automatic generation from your application's content + * Support many DSL patterns such as Rails, Google Protobuf, SmartProperties and more out of the box + * Extensible interface that allows you to write your own DSL compilers for other DSL patterns + * Automatic generation of signatures for methods from known DSLs + * Synchronization validation for your CI +* Management of shim RBI files + * Find useless definitions in shim RBI files from gems generated RBI files + * Find useless definitions in shim RBI files from DSL generated RBI files + * Find useless definitions in shim RBI files from Sorbet's embedded RBI for core and stdlib + * Synchronization validation for your CI ## Installation Add this line to your application's `Gemfile`: -```ruby +```rb group :development do gem 'tapioca', require: false end ``` -and do not forget to execute `tapioca` using `bundler`: +Run `bundle install` and make sure Tapioca is properly installed: -```shell +```sh $ bundle exec tapioca help Commands: tapioca --version, -v # show version @@ -72,13 +60,22 @@ Options: -V, [--verbose], [--no-verbose] # Verbose output for debugging purposes ``` -## Usage +## Getting started -### Initialize folder structure +Execute this command to get started: -Command: `tapioca init` +```sh +$ bundle exec tapioca init + create sorbet/config + create sorbet/tapioca/config.yml + create sorbet/tapioca/require.rb + create bin/tapioca +``` + +This will: -This will create the `sorbet/config` and `sorbet/tapioca/require.rb` files for you, if they don't exist. If any of the files already exist, they will not be changed. +* create the [configuration file for Sorbet](https://sorbet.org/docs/cli#config-file), the [configuration file for Tapioca](#Configuration) and the [require.rb file](#manually-requiring-parts-of-a-gem) +* install the [binstub](https://bundler.io/man/bundle-binstubs.1.html#DESCRIPTION) for Tapioca in your app's `bin/` folder, so that you can use `bin/tapioca` to run commands in your app ```shell @@ -96,11 +93,35 @@ initializes folder structure ``` -### Generate RBI files for gems +## Usage + +### Generating RBI files for gems + +Sorbet does not read the code in your gem dependencies, so it does not know the constants and methods declared inside gems. Tapioca is able to load your gem dependencies from your application's `Gemfile` and compile RBI files to represent their content. + +In order to generate the RBI files for the gems used in your application, run the following command: -Command: `tapioca gem [gems...]` +```sh +$ bin/tapioca gems [gems...] -This will generate RBIs for the specified gems and place them in the RBI directory. +Removing RBI files of gems that have been removed: + + Nothing to do. + +Generating RBI files of gems that are added or updated: + + Requiring all gems to prepare for compiling... Done + + Compiled ansi + create sorbet/rbi/gems/ansi@1.5.0.rbi + + ... + +All operations performed in working directory. +Please review changes and commit them. +``` + +This will load your application, find all the gems required by it and generate an RBI file for each gem under the `sorbet/rbi/gems` directory for each of those gems. This process will also import signatures that can be found inside each gem sources, and, optionally, any YARD documentation inside the gem. ```shell @@ -142,35 +163,135 @@ generate RBIs from gems ``` -### Generate the list of all unresolved constants +> Are you coming from `srb rbi`? [See how `tapioca gem` compares to `srb rbi`](https://github.com/Shopify/tapioca/wiki/How-does-tapioca-compare-to-%22srb-rbi-gems%22-%3F). -Command: `tapioca todo` +#### Manually requiring parts of a gem -This will generate the file `sorbet/rbi/todo.rbi` defining all unresolved constants as empty modules. +It may happen that the RBI file generated for a gem listed inside your `Gemfile.lock` is missing some definitions taht you would expect it to be exporting. + +For gems that have a normal default `require` and that load all of their constants through that, everything should work seamlessly. However, for gems that are marked as `require: false` in the `Gemfile`, or for gems that export constants optionally via different requires, where a single require does not load the whole gem code into memory, Tapioca will not be able to load some of the types into memory and, thus, won't be able to generate complete RBIs for them. For this reason, we need to keep a small external file named `sorbet/tapioca/require.rb` that is executed after all the gems in the `Gemfile` have been required and before generation of gem RBIs have started. This file is responsible for adding the requires for additional files from gems, which are not covered by the default require. + +For example, suppose you are using the class `BetterHtml::Parser` exported from the `better_html` gem. Just doing a `require "better_html"` (which is the default require) does not load that type: - ```shell -$ tapioca help todo +$ bundle exec pry -Usage: - tapioca todo +[1] pry(main)> require 'better_html' +=> true +[2] pry(main)> BetterHtml +=> BetterHtml +[3] pry(main)> BetterHtml::Parser +NameError: uninitialized constant BetterHtml::Parser +from (pry):3:in `__pry__` +[4] pry(main)> require 'better_html/parser' +=> true +[5] pry(main)> BetterHtml::Parser +=> BetterHtml::Parser +``` -Options: - [--todo-file=TODO_FILE] # Path to the generated todo RBI file - # Default: sorbet/rbi/todo.rbi - [--file-header], [--no-file-header] # Add a "This file is generated" header on top of each generated RBI file - # Default: true - -c, [--config=] # Path to the Tapioca configuration file - # Default: sorbet/tapioca/config.yml - -V, [--verbose], [--no-verbose] # Verbose output for debugging purposes +In order to make sure that `tapioca` can reflect on that type, we need to add the line `require "better_html/parser"` to the `sorbet/tapioca/require.rb` file. This will make sure `BetterHtml::Parser` is loaded into memory and a type annotation is generated for it in the `better_html.rbi` file. If this extra `require` line is not added to `sorbet/tapioca/require.rb` file, then Tapioca will be able to generate definitions for `BetterHtml` and other constants, but not for `BetterHtml::Parser`, which will be missing from the RBI file. -generate the list of unresolved constants +For example, you can take a look at Tapioca's own [`require.rb` file](https://github.com/Shopify/tapioca/blob/main/sorbet/tapioca/require.rb): + +```rb +# typed: strict +# frozen_string_literal: true + +require "ansi/code" +require "google/protobuf" +require "rails/all" +require "rails/generators" +require "rails/generators/app_base" +require "rake/testtask" +require "rubocop/rake_task" ``` - -### Generate DSL RBI files +If you ever run into a case, where you add a gem or update the version of a gem and run `tapioca gem` but don't have some types you expect in the generated gem RBI files, you will need to make sure you have added the necessary requires to the `sorbet/tapioca/require.rb` file and regenerate the RBI file for that gem explicitly using `bin/tapioca gem `. + +To help you get started, you can use the command `tapioca require` to auto-populate the contents of the `sorbet/tapioca/require.rb` file with all the requires found in your application: + +```sh +$ bin/tapioca require + +Compiling sorbet/tapioca/require.rb, this may take a few seconds... Done + +All requires from this application have been written to sorbet/tapioca/require.rb. +Please review changes and commit them, then run `bin/tapioca gem`. +``` + +Once the file is generated, you should review it, remove all unnecessary requires and commit it. + +#### Excluding a gem from RBI generation + +It may be useful to exclude some gems from the generation process. For example for gems that are in Bundle's debug group or gems of which the contents are dependent on the architecture they are loaded on. A typical example is `fakefs`, which, if loaded into memory, changes `File` operations to be no-ops and breaks Tapioca RBI file generation altogether. + +To do so you can pass the list of gems you want to exclude in the command line with the `--exclude` option: + +```sh +$ bin/tapioca gems --exclude gemA gemB +``` + +Or through the configuration file: + +```yaml +gem: + exclude: + - gemA + - gemB +``` + +#### Changing the strictness level of the RBI for a gem + +By default, all RBI files for gems are generated with the [strictness level](https://sorbet.org/docs/static#file-level-granularity-strictness-levels) `typed: true`. Sometimes, this strictness level can create type-checking errors when a gem contains definitions that conflict with [Sorbet internal definitions for Ruby core and standard library](https://sorbet.org/docs/faq#it-looks-like-sorbets-types-for-the-stdlib-are-wrong). -Command: `tapioca dsl [constant...]` +Tapioca comes with an automatic detection (option `--auto-strictness`, enabled by default) of such cases and will switch the strictness level to `typed: false` in RBI files containing conflicts with the core and standard library definitions. It is nonetheless possible to manually switch the strictness level for a gem using the `--typed-overrides` option: + +```sh +$ bin/tapioca gems --typed-overrides gemA:false gemB:false +``` + +Or through the configuration file: + +```yaml +gem: + typed_overrides: + gemA: "false" + gemB: "false" +``` + +#### Keeping RBI files for gems up-to-date + +To ensure all RBI files for gems are up-to-date with the latest changes in your `Gemfile.lock`, Tapioca provides a `--verify` option: + +```sh +$ bin/tapioca gems --verify + +Checking for out-of-date RBIs... + +Nothing to do, all RBIs are up-to-date. +``` + +This option can be used on CI to make sure the RBI files are always up-to-date and ensure accurate type checking. **Warning**: doing so will break your normal Dependabot workflow as every pull-request opened to bump a gem version will fail CI since the RBI will be out-of-date and will require you to manually run `bin/tapioca gems` to update them. + +### Generating RBI files for Rails and other DSLs + +Sorbet by itself does not understand DSLs involving meta-programming, such as Rails. This means that Sorbet won't know about constants and methods generated by `ActiveRecord` or `ActiveSupport`. +To solve this, Tapioca can load your application and introspect it to find the constants and methods that would exist at runtime and compile them into RBI files. + +To generate the RBI files for the DSLs used in your application, run the following command: + +```sh +$ bin/tapioca dsl + +Loading Rails application... Done +Loading DSL compiler classes... Done +Compiling DSL RBI files... + + create sorbet/rbi/dsl/my_model.rbi + ... + +Done +``` This will generate DSL RBIs for specified constants (or for all handled constants, if a constant name is not supplied). You can read about DSL RBI compilers supplied by `tapioca` in [the manual](manual/compilers.md). @@ -202,13 +323,149 @@ generate RBIs for dynamic methods ``` -## Configuration +#### Keeping RBI files for DSLs up-to-date + +To ensure all RBI files for DSLs are up-to-date with the latest changes in your application or database, Tapioca provide a `--verify` option: + +```sh +$ bin/tapioca dsl --verify + +Loading Rails application... Done +Loading DSL compiler classes... Done +Checking for out-of-date RBIs... + + +RBI files are out-of-date. In your development environment, please run: + `bin/tapioca dsl` +Once it is complete, be sure to commit and push any changes + +Reason: + File(s) changed: + - sorbet/rbi/dsl/my_model.rbi +``` + +This option can be used on CI to make sure the RBI files are always up-to-date and ensure accurate type checking. + +#### Writing custom DSL compilers + +**TODO** + +### RBI files for missing constants and methods + +Even after generating the RBIs, it is possible that some constants or methods are still undefined for Sorbet. + +This might be for multiple reasons, with the most frequents ones being: + +* The constant or method comes from a part of the gem that Tapioca cannot load (optional dependency, wrong architecture, etc.) +* The constant or method comes from a DSL or meta-programming that Tapioca doesn't support yet +* The constant or method only exists when a specific code path is executed + +The best way to deal with such occurrences is to manually create RBI files (shims) for them so you can also add types but depending on the amount of meta-programming used in your project this can mean an overwhelming amount of manual work. + +#### Generating the RBI file for missing constants + +To get you started quickly, Tapioca can create a RBI file containing a stub of all the missing constants so you can typecheck your project without missing constants and shim them later as you need them. + +To generate the RBI file for the missing constants used in your application run the following command: + +```sh +$ bin/tapioca todo + +Compiling sorbet/rbi/todo.rbi, this may take a few seconds... Done +All unresolved constants have been written to sorbet/rbi/todo.rbi. +Please review changes and commit them. +``` + +This will generate the file `sorbet/rbi/todo.rbi` defining all unresolved constants as empty modules. Since the constants are "missing", Tapioca does not know if they should be marked as modules or classes and will use modules as a safer default. This file should be reviewed, corrected, if necessary, and then committed in your repository. + + +```shell +$ tapioca help todo + +Usage: + tapioca todo + +Options: + [--todo-file=TODO_FILE] # Path to the generated todo RBI file + # Default: sorbet/rbi/todo.rbi + [--file-header], [--no-file-header] # Add a "This file is generated" header on top of each generated RBI file + # Default: true + -c, [--config=] # Path to the Tapioca configuration file + # Default: sorbet/tapioca/config.yml + -V, [--verbose], [--no-verbose] # Verbose output for debugging purposes + +generate the list of unresolved constants +``` + + +#### Manually writing RBI definitions (shims) + +A _shim_ is a hand-crafted RBI file that tells Sorbet about constants, ancestors, methods, etc. that it can't understand statically and aren't already generated by Tapioca. + +These shims are usually placed in the `sorbet/rbi/shims` directory. From there, conventionally, you should follow the directory structure of the project to the file you'd like to shim. For example, say you had a `person.rb` file found at `app/models/person.rb`. If you were to add a shim for it, you'd want to create your RBI file at `sorbet/rbi/shims/app/models/person.rbi`. + +A shim might be as simple as the class definition with an empty method body as below: + +```ruby +# typed: true + +class Person + sig { void } + def some_method_sorbet_cannot_find; end +end +``` + +As you migrate to newer versions of Sorbet or Tapioca, some shims may become useless as Sorbet's internal definitions for Ruby's core and standard library is enhanced or Tapioca is able to generate definitions for new DSLs. To avoid keeping outdated or useless definitions inside your application shims, Tapioca provides the `check-shims` command: + +```sh +$ bin/tapioca check-shims + +Loading Sorbet payload... Done +Loading shim RBIs from sorbet/rbi/shims... Done +Loading gem RBIs from sorbet/rbi/gems... Done +Loading gem RBIs from sorbet/rbi/dsl... Done +Looking for duplicates... Done + +Duplicated RBI for ::MyModel#title: + * sorbet/rbi/shims/my_model.rbi:2:2-2:14 + * sorbet/rbi/dsl/my_model.rbi:2:2-2:14 + +Duplicated RBI for ::String#capitalize: + * https://github.com/sorbet/sorbet/tree/master/rbi/core/string.rbi#L406 + * sorbet/rbi/shims/core/string.rbi:3:2-3:23 + +Please remove the duplicated definitions from the sorbet/rbi/shims directory. +``` + +This command can be used on CI to make sure the RBI shims are always up-to-date and non-redundant with generated files. + + +```shell +$ bin/tapioca help check-shims + +Usage: + tapioca check-shims + +Options: + [--gem-rbi-dir=GEM_RBI_DIR] # Path to gem RBIs + # Default: sorbet/rbi/gems + [--dsl-rbi-dir=DSL_RBI_DIR] # Path to DSL RBIs + # Default: sorbet/rbi/dsl + [--shim-rbi-dir=SHIM_RBI_DIR] # Path to shim RBIs + # Default: sorbet/rbi/shims + -c, [--config=] # Path to the Tapioca configuration file + # Default: sorbet/tapioca/config.yml + -V, [--verbose], [--no-verbose] # Verbose output for debugging purposes + +check duplicated definitions in shim RBIs +``` + + +### Configuration -Tapioca supports loading command defaults from a configuration file. The default configuration -file location is `sorbet/tapioca/config.yml` but this default can be changed using the `--config` flag -and supplying an alternative configuration file path. +Tapioca supports loading command defaults from a configuration file. The default configuration file location is `sorbet/tapioca/config.yml` but this default can be changed using the `--config` flag and supplying an alternative configuration file path. -A configuration file must be a well-formed YAML file with top-level keys for the various Tapioca commands. Keys under each such top-level command should be the underscore version of a long option name for that command and the value for that key should be the value of the option. +Tapioca's configuration file must be a well-formed YAML file with top-level keys for the various Tapioca commands. Keys under each such top-level command should be the underscore version of a long option name for that command and the value for that key should be the value of the option. For example, if you always want to generate gem RBIs with inline documentation, then you would create the file `sorbet/tapioca/config.yml` as: diff --git a/misc/tapioca-logo.svg b/misc/tapioca-logo.svg new file mode 100644 index 000000000..712e50aca --- /dev/null +++ b/misc/tapioca-logo.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +