From 0b6dc4dcc91763366e6840b474db23cd5783f0ad Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Sun, 2 Feb 2025 22:38:57 +0000 Subject: [PATCH 1/7] Basic setup --- jekyll/.gitignore | 5 + jekyll/404.html | 25 +++ jekyll/Gemfile | 13 ++ jekyll/Gemfile.lock | 172 ++++++++++++++++++ jekyll/_config.yml | 17 ++ .../2025-02-02-welcome-to-jekyll.markdown | 29 +++ jekyll/index.markdown | 15 ++ jekyll/tapioca-logo.svg | 85 +++++++++ sorbet/config | 1 + 9 files changed, 362 insertions(+) create mode 100644 jekyll/.gitignore create mode 100644 jekyll/404.html create mode 100644 jekyll/Gemfile create mode 100644 jekyll/Gemfile.lock create mode 100644 jekyll/_config.yml create mode 100644 jekyll/_posts/2025-02-02-welcome-to-jekyll.markdown create mode 100644 jekyll/index.markdown create mode 100644 jekyll/tapioca-logo.svg diff --git a/jekyll/.gitignore b/jekyll/.gitignore new file mode 100644 index 000000000..f40fbd8ba --- /dev/null +++ b/jekyll/.gitignore @@ -0,0 +1,5 @@ +_site +.sass-cache +.jekyll-cache +.jekyll-metadata +vendor diff --git a/jekyll/404.html b/jekyll/404.html new file mode 100644 index 000000000..3a16ab533 --- /dev/null +++ b/jekyll/404.html @@ -0,0 +1,25 @@ +--- +permalink: /404.html +layout: page +--- + + + +
+

404

+ +

Page not found :(

+

The requested page could not be found.

+
diff --git a/jekyll/Gemfile b/jekyll/Gemfile new file mode 100644 index 000000000..c8e12d98a --- /dev/null +++ b/jekyll/Gemfile @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# We only need to use Jekyll with CRuby +gem "jekyll", "~> 4.3.3" +gem "jekyll-feed", "~> 0.12" + +# Theme +gem "just-the-docs", "~> 0.10.0" + +gem "csv" +gem "base64" diff --git a/jekyll/Gemfile.lock b/jekyll/Gemfile.lock new file mode 100644 index 000000000..6265ca3a3 --- /dev/null +++ b/jekyll/Gemfile.lock @@ -0,0 +1,172 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + base64 (0.2.0) + bigdecimal (3.1.9) + colorator (1.1.0) + concurrent-ruby (1.3.5) + csv (3.3.2) + em-websocket (0.5.3) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0) + eventmachine (1.2.7) + ffi (1.17.1) + ffi (1.17.1-aarch64-linux-gnu) + ffi (1.17.1-aarch64-linux-musl) + ffi (1.17.1-arm-linux-gnu) + ffi (1.17.1-arm-linux-musl) + ffi (1.17.1-arm64-darwin) + ffi (1.17.1-x86-linux-gnu) + ffi (1.17.1-x86-linux-musl) + ffi (1.17.1-x86_64-darwin) + ffi (1.17.1-x86_64-linux-gnu) + ffi (1.17.1-x86_64-linux-musl) + forwardable-extended (2.6.0) + google-protobuf (4.29.3) + bigdecimal + rake (>= 13) + google-protobuf (4.29.3-aarch64-linux) + bigdecimal + rake (>= 13) + google-protobuf (4.29.3-arm64-darwin) + bigdecimal + rake (>= 13) + google-protobuf (4.29.3-x86-linux) + bigdecimal + rake (>= 13) + google-protobuf (4.29.3-x86_64-darwin) + bigdecimal + rake (>= 13) + google-protobuf (4.29.3-x86_64-linux) + bigdecimal + rake (>= 13) + http_parser.rb (0.8.0) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + jekyll (4.3.4) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 1.0) + jekyll-sass-converter (>= 2.0, < 4.0) + jekyll-watch (~> 2.0) + kramdown (~> 2.3, >= 2.3.1) + kramdown-parser-gfm (~> 1.0) + liquid (~> 4.0) + mercenary (>= 0.3.6, < 0.5) + pathutil (~> 0.9) + rouge (>= 3.0, < 5.0) + safe_yaml (~> 1.0) + terminal-table (>= 1.8, < 4.0) + webrick (~> 1.7) + jekyll-feed (0.17.0) + jekyll (>= 3.7, < 5.0) + jekyll-include-cache (0.2.1) + jekyll (>= 3.7, < 5.0) + jekyll-sass-converter (3.1.0) + sass-embedded (~> 1.75) + jekyll-seo-tag (2.8.0) + jekyll (>= 3.8, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + just-the-docs (0.10.1) + jekyll (>= 3.8.5) + jekyll-include-cache + jekyll-seo-tag (>= 2.0) + rake (>= 12.3.1) + kramdown (2.5.1) + rexml (>= 3.3.9) + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.4) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.4.0) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (6.0.1) + rake (13.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rexml (3.4.0) + rouge (4.5.1) + safe_yaml (1.0.5) + sass-embedded (1.83.4) + google-protobuf (~> 4.29) + rake (>= 13) + sass-embedded (1.83.4-aarch64-linux-android) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-aarch64-linux-gnu) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-aarch64-linux-musl) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-aarch64-mingw-ucrt) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-arm-linux-androideabi) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-arm-linux-gnueabihf) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-arm-linux-musleabihf) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-arm64-darwin) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-riscv64-linux-android) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-riscv64-linux-gnu) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-riscv64-linux-musl) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-x86_64-cygwin) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-x86_64-darwin) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-x86_64-linux-android) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-x86_64-linux-gnu) + google-protobuf (~> 4.29) + sass-embedded (1.83.4-x86_64-linux-musl) + google-protobuf (~> 4.29) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + unicode-display_width (2.6.0) + webrick (1.9.1) + +PLATFORMS + aarch64-linux + aarch64-linux-android + aarch64-linux-gnu + aarch64-linux-musl + aarch64-mingw-ucrt + arm-linux-androideabi + arm-linux-gnu + arm-linux-gnueabihf + arm-linux-musl + arm-linux-musleabihf + arm64-darwin + riscv64-linux-android + riscv64-linux-gnu + riscv64-linux-musl + ruby + x86-linux + x86-linux-gnu + x86-linux-musl + x86_64-cygwin + x86_64-darwin + x86_64-linux + x86_64-linux-android + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + base64 + csv + jekyll (~> 4.3.3) + jekyll-feed (~> 0.12) + just-the-docs (~> 0.10.0) + +BUNDLED WITH + 2.6.2 diff --git a/jekyll/_config.yml b/jekyll/_config.yml new file mode 100644 index 000000000..8b34cf241 --- /dev/null +++ b/jekyll/_config.yml @@ -0,0 +1,17 @@ +title: Tapioca +description: >- + The swiss army knife of RBI generation +baseurl: "/tapioca" +url: "https://shopify.github.io" +github_username: shopify +aux_links: + "Tapioca on GitHub": + - "https://github.com/Shopify/tapioca" + "Report an Issue": + - "https://github.com/Shopify/tapioca/issues/new" + +theme: just-the-docs +layout: minimal +plugins: + - jekyll-feed +destination: ../doc diff --git a/jekyll/_posts/2025-02-02-welcome-to-jekyll.markdown b/jekyll/_posts/2025-02-02-welcome-to-jekyll.markdown new file mode 100644 index 000000000..602ea7d97 --- /dev/null +++ b/jekyll/_posts/2025-02-02-welcome-to-jekyll.markdown @@ -0,0 +1,29 @@ +--- +layout: post +title: "Welcome to Jekyll!" +date: 2025-02-02 22:21:28 +0000 +categories: jekyll update +--- +You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated. + +Jekyll requires blog post files to be named according to the following format: + +`YEAR-MONTH-DAY-title.MARKUP` + +Where `YEAR` is a four-digit number, `MONTH` and `DAY` are both two-digit numbers, and `MARKUP` is the file extension representing the format used in the file. After that, include the necessary front matter. Take a look at the source for this post to get an idea about how it works. + +Jekyll also offers powerful support for code snippets: + +{% highlight ruby %} +def print_hi(name) + puts "Hi, #{name}" +end +print_hi('Tom') +#=> prints 'Hi, Tom' to STDOUT. +{% endhighlight %} + +Check out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk]. + +[jekyll-docs]: https://jekyllrb.com/docs/home +[jekyll-gh]: https://github.com/jekyll/jekyll +[jekyll-talk]: https://talk.jekyllrb.com/ diff --git a/jekyll/index.markdown b/jekyll/index.markdown new file mode 100644 index 000000000..b9565e743 --- /dev/null +++ b/jekyll/index.markdown @@ -0,0 +1,15 @@ +--- +layout: home +title: Tapioca +--- + +# Tapioca + +

+ Tapioca logo +

+ +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. diff --git a/jekyll/tapioca-logo.svg b/jekyll/tapioca-logo.svg new file mode 100644 index 000000000..712e50aca --- /dev/null +++ b/jekyll/tapioca-logo.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sorbet/config b/sorbet/config index c54e716d8..4712382dd 100644 --- a/sorbet/config +++ b/sorbet/config @@ -1,5 +1,6 @@ --dir . --ignore=/vendor +--ignore=/jekyll --enable-experimental-requires-ancestor --suppress-payload-superclass-redefinition-for=Reline::ANSI From 34312a4881356db04152aca86aa914f7f7df91c5 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Sun, 2 Feb 2025 22:55:35 +0000 Subject: [PATCH 2/7] Add content from readme --- jekyll/_config.yml | 2 + jekyll/index.markdown | 75 ++- ...bi_annotation_from_remote_sources.markdown | 116 +++++ jekyll/rbi_files_for_dsls.markdown | 465 ++++++++++++++++++ jekyll/rbi_files_for_gems.markdown | 213 ++++++++ ...for_missing_constants_and_methods.markdown | 88 ++++ 6 files changed, 958 insertions(+), 1 deletion(-) create mode 100644 jekyll/rbi_annotation_from_remote_sources.markdown create mode 100644 jekyll/rbi_files_for_dsls.markdown create mode 100644 jekyll/rbi_files_for_gems.markdown create mode 100644 jekyll/rbi_files_for_missing_constants_and_methods.markdown diff --git a/jekyll/_config.yml b/jekyll/_config.yml index 8b34cf241..3266fcbc5 100644 --- a/jekyll/_config.yml +++ b/jekyll/_config.yml @@ -9,6 +9,8 @@ aux_links: - "https://github.com/Shopify/tapioca" "Report an Issue": - "https://github.com/Shopify/tapioca/issues/new" + "Sorbet.org": + - "https://sorbet.org" theme: just-the-docs layout: minimal diff --git a/jekyll/index.markdown b/jekyll/index.markdown index b9565e743..b3599f91c 100644 --- a/jekyll/index.markdown +++ b/jekyll/index.markdown @@ -1,6 +1,7 @@ --- -layout: home +layout: default title: Tapioca +nav_order: 0 --- # Tapioca @@ -13,3 +14,75 @@ 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. + +## Installation + +Add this line to your application's `Gemfile`: + +```rb +group :development, :test do + gem 'tapioca', require: false +end +``` + +Run `bundle install` and make sure Tapioca is properly installed: + + +```shell +$ tapioca help + +Commands: + tapioca --version, -v # Show version + tapioca annotations # Pull gem RBI annotations from remote sources + tapioca check-shims # Check duplicated definitions in shim RBIs + tapioca configure # Initialize folder structure and type checking configuration + tapioca dsl [constant...] # Generate RBIs for dynamic methods + tapioca gem [gem...] # Generate RBIs from gems + tapioca help [COMMAND] # Describe available commands or one specific command + tapioca init # Get project ready for type checking + tapioca require # Generate the list of files to be required by tapioca + tapioca todo # Generate the list of unresolved constants + +Options: + -c, [--config=] # Path to the Tapioca configuration file + # Default: sorbet/tapioca/config.yml + -V, [--verbose], [--no-verbose], [--skip-verbose] # Verbose output for debugging purposes + # Default: false + +``` + + +## Getting started + +Execute this command to get started: + +```shell +$ bundle exec tapioca init +``` + +This will: + +1. 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) +2. 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 +3. pull the community RBI annotations from the [central repository](https://github.com/Shopify/rbi-central) matching your app's gems +4. generate the RBIs for your app's gems +5. generate the RBI file for missing constants + +See the following sections for more details about each step. + + +```shell +$ tapioca help init + +Usage: + tapioca init + +Options: + -c, [--config=] # Path to the Tapioca configuration file + # Default: sorbet/tapioca/config.yml + -V, [--verbose], [--no-verbose], [--skip-verbose] # Verbose output for debugging purposes + # Default: false + +Get project ready for type checking +``` + diff --git a/jekyll/rbi_annotation_from_remote_sources.markdown b/jekyll/rbi_annotation_from_remote_sources.markdown new file mode 100644 index 000000000..9764c7924 --- /dev/null +++ b/jekyll/rbi_annotation_from_remote_sources.markdown @@ -0,0 +1,116 @@ +--- +layout: default +title: Pulling RBI annotations from remote sources +parent: Tapioca +nav_order: 3 +--- + +# Pulling RBI annotations from remote sources + +Since Tapioca does not perform any type inference, the RBI files generated for the gems do not contain any type signatures. Instead, Tapioca relies on the community to provide high-quality, manually written RBI annotations for public gems. + +To pull the annotations relevant to your project from the central repository, run the `annotations` command: + +```shell +$ bin/tapioca annotations + +Retrieving index from central repository... Done +Listing gems from Gemfile.lock... Done +Removing annotations for gems that have been removed... Nothing to do +Fetching gem annotations from central repository... + + Fetched activesupport + created sorbet/rbi/annotations/activesupport.rbi + +Done +``` + + +```shell +$ tapioca help annotations + +Usage: + tapioca annotations + +Options: + [--sources=one two three] # URIs of the sources to pull gem RBI annotations from + # Default: "https://raw.githubusercontent.com/Shopify/rbi-central/main" + [--netrc], [--no-netrc], [--skip-netrc] # Use .netrc to authenticate to private sources + # Default: true + [--netrc-file=NETRC_FILE] # Path to .netrc file + [--auth=AUTH] # HTTP authorization header for private sources + --typed, -t, [--typed-overrides=gem:level [gem:level ...]] # Override for typed sigils for pulled annotations + -c, [--config=] # Path to the Tapioca configuration file + # Default: sorbet/tapioca/config.yml + -V, [--verbose], [--no-verbose], [--skip-verbose] # Verbose output for debugging purposes + # Default: false + +Pull gem RBI annotations from remote sources +``` + + +By default, Tapioca will pull the annotations stored in the central repository located at https://github.com/Shopify/rbi-central. It is possible to use a custom repository by changing the value of the `--sources` options. For example if your repository is stored on Github: + +```shell +$ bin/tapioca annotations --sources https://raw.githubusercontent.com/$USER/$REPO/$BRANCH +``` + +Tapioca also supports pulling annotations from multiple sources: + +```shell +$ bin/tapioca annotations --sources https://raw.githubusercontent.com/$USER/$REPO1/$BRANCH https://raw.githubusercontent.com/$USER/$REPO2/$BRANCH +``` + +## Basic authentication + +Private repositories can be used as sources by passing the option `--auth` with an authentication string. For Github, this string is `token $TOKEN` where `$TOKEN` is a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token): + +```shell +$ bin/tapioca annotations --sources https://raw.githubusercontent.com/$USER/$PRIVATE_REPO/$BRANCH --auth "token $TOKEN" +``` + +## Using a .netrc file + +Tapioca supports reading credentials from a [netrc](https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html) file (defaulting to `~/.netrc`). + +Given these lines in your netrc: + +```netrc +machine raw.githubusercontent.com + login $USERNAME + password $TOKEN +``` + +where `$USERNAME` is your Github username and `$TOKEN` is a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token), then, if you run Tapioca with the `--netrc` option (enabled by default), your annotation requests should be authenticated properly. + +The `--netrc-file` option can be specified to read from a file other than `~/.netrc`: + +```shell +$ bin/tapioca annotations --netrc-file /path/to/my/netrc/file +``` + +Similar to `--netrc-file`, you can also specify an alternative netrc file by using the `TAPIOCA_NETRC_FILE` environment variable: + +```shell +$ TAPIOCA_NETRC_FILE=/path/to/my/netrc/file bin/tapioca annotations +``` + +Tapioca will first try to find the netrc file as specified by the `--netrc-file` option. If that option is not supplied, it will try the `TAPIOCA_NETRC_FILE` environment variable value. If that value is not supplied either, it will fallback to `~/.netrc`. + +## Changing the typed strictness of annotations files + +Sometimes the annotations files pulled by Tapioca will create type errors in your project because of incompatibilities. +It is possible to ignore such files by switching their strictness level `--typed-overrides` option: + +```shell +$ bin/tapioca annotations --typed-overrides gemA:ignore gemB:false +``` + +Or through the configuration file: + +```yaml +annotations: + typed_overrides: + gemA: "ignore" + gemB: "false" +``` diff --git a/jekyll/rbi_files_for_dsls.markdown b/jekyll/rbi_files_for_dsls.markdown new file mode 100644 index 000000000..19186e89e --- /dev/null +++ b/jekyll/rbi_files_for_dsls.markdown @@ -0,0 +1,465 @@ +--- +layout: default +title: RBI files for Rails and other DSLs +parent: Tapioca +nav_order: 2 +--- + +# 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: + +```shell +$ 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). + + +```shell +$ tapioca help dsl + +Usage: + tapioca dsl [constant...] + +Options: + --out, -o, [--outdir=directory] # The output directory for generated DSL RBI files + # Default: sorbet/rbi/dsl + [--file-header], [--no-file-header], [--skip-file-header] # Add a "This file is generated" header on top of each generated RBI file + # Default: true + [--only=compiler [compiler ...]] # Only run supplied DSL compiler(s) + [--exclude=compiler [compiler ...]] # Exclude supplied DSL compiler(s) + [--verify], [--no-verify], [--skip-verify] # Verifies RBIs are up-to-date + # Default: false + -q, [--quiet], [--no-quiet], [--skip-quiet] # Suppresses file creation output + # Default: false + -w, [--workers=N] # Number of parallel workers to use when generating RBIs (default: auto) + [--rbi-max-line-length=N] # Set the max line length of generated RBIs. Signatures longer than the max line length will be wrapped + # Default: 120 + -e, [--environment=ENVIRONMENT] # The Rack/Rails environment to use when generating RBIs + # Default: development + -l, [--list-compilers], [--no-list-compilers], [--skip-list-compilers] # List all loaded compilers + # Default: false + [--app-root=APP_ROOT] # The path to the Rails application + # Default: . + [--halt-upon-load-error], [--no-halt-upon-load-error], [--skip-halt-upon-load-error] # Halt upon a load error while loading the Rails application + # Default: true + [--skip-constant=constant [constant ...]] # Do not generate RBI definitions for the given application constant(s) + [--compiler-options=key:value] # Options to pass to the DSL compilers + -c, [--config=] # Path to the Tapioca configuration file + # Default: sorbet/tapioca/config.yml + -V, [--verbose], [--no-verbose], [--skip-verbose] # Verbose output for debugging purposes + # Default: false + +Generate RBIs for dynamic methods +``` + + +## 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: + +```shell +$ 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. + +If you are using Rails, you can configure `tapioca dsl` to run after each migration: + +```ruby +# Rakefile +if Rails.env.development? + namespace :db do + task :migrate do # Appends to the existing `db:migrate` task + system("bundle exec tapioca dsl", exception: true) + end + end +``` + +## Using DSL compiler options + +Some DSL compilers are able to change their behaviour based on the options passed to them. For example, the +`ActiveRecordColumns` compiler can be configured to change how it generates types for method related to Active Record +column attributes. To pass options during DSL RBI generation, use the `--compiler-options` flag: +```shell +$ bin/tapioca dsl --compiler-options=ActiveRecordColumnTypes:untyped +``` +which will make the `ActiveRecordColumns` compiler generate untyped signatures for column attribute methods. + +Compiler options can be passed through the configuration file, as like any other option, and we expect most users to +configure them this way. For example, to configure the `ActiveRecordColumns` compiler to generate untyped signatures, +you need to add the following to your `sorbet/tapioca/config.yml` file: +```yaml +dsl: + compiler_options: + ActiveRecordColumnTypes: untyped +``` + +## Writing custom DSL compilers + +It is possible to create your own compilers for DSLs not supported by Tapioca out of the box. + +Let's take for example this `Encryptable` module that uses the [`included` hook](https://ruby-doc.org/core-3.1.1/Module.html#method-i-included) to dynamically add a few methods to the classes that include it: + +```rb +module Encryptable + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def attr_encrypted(attr_name) + encrypted_attributes << attr_name + + attr_accessor(attr_name) + + encrypted_attr_name = :"#{attr_name}_encrypted" + + define_method(encrypted_attr_name) do + value = send(attr_name) + encrypt(value) + end + + define_method("#{encrypted_attr_name}=") do |value| + send("#{attr_name}=", decrypt(value)) + end + end + + def encrypted_attributes + @encrypted_attributes ||= [] + end + end + + private + + def encrypt(value) + value.unpack("H*").first + end + + def decrypt(value) + [value].pack("H*") + end +end +``` + +When `Encryptable` is included in a class like this one, it makes it possible to call `attr_encrypted` to define an attribute, its accessors and its encrypted accessors: + +```rb +class CreditCard + include Encryptable + + attr_encrypted :number +end +``` + +These accessors can then be used on the `CreditCard` instance without having to define them in the class: + +```rb +# typed: true +# file: example.rb + +card = CreditCard.new +card.number = "1234 5678 9012 3456" + +p card.number # => "1234 5678 9012 3456" +p card.number_encrypted # => "31323334203536373820393031322033343536" + +card.number_encrypted = "31323334203536373820393031322033343536" +p card.number # => "1234 5678 9012 3456" +``` + +Sadly, since these methods have been created dynamically at runtime, when our `attr_encryptable` method was run, there are no static traces of the `number`, `number=`, `number_encrypted` and `number_encrypted=` methods. Since Sorbet does not run the Ruby code but analyses it statically, it can't see these methods and running type-checking will show a bunch of errors: + +```shell +$ bundle exec srb tc + +lib/example.rb:5: Method number= does not exist on CreditCard https://srb.help/7003 +lib/example.rb:7: Method number does not exist on CreditCard https://srb.help/7003 +lib/example.rb:8: Method number_encrypted does not exist on CreditCard https://srb.help/7003 +lib/example.rb:10: Method number_encrypted= does not exist on CreditCard https://srb.help/7003 +lib/example.rb:11: Method number does not exist on CreditCard https://srb.help/7003 + +Errors: 5 +``` + +To solve this you will have to create your own DSL compiler able that understands the `Encryptable` DSL and can generate the RBI definitions representing the actual shape of `CreditCard` at runtime. + +To do so, you need to create a new DSL compiler similar to the following: + +```rb +module Tapioca + module Compilers + class Encryptable < Tapioca::Dsl::Compiler + extend T::Sig + + ConstantType = type_member {{ fixed: T.class_of(Encryptable) }} + + sig { override.returns(T::Enumerable[Module]) } + def self.gather_constants + # Collect all the classes that include Encryptable + all_classes.select { |c| c < ::Encryptable } + end + + sig { override.void } + def decorate + # Create a RBI definition for each class that includes Encryptable + root.create_path(constant) do |klass| + # For each encrypted attribute we find in the class + constant.encrypted_attributes.each do |attr_name| + # Create the RBI definitions for all the missing methods + klass.create_method(attr_name, return_type: "String") + klass.create_method("#{attr_name}=", parameters: [ create_param("value", type: "String") ], return_type: "void") + klass.create_method("#{attr_name}_encrypted", return_type: "String") + klass.create_method("#{attr_name}_encrypted=", parameters: [ create_param("value", type: "String") ], return_type: "void") + end + end + end + end + end +end +``` + +In order for this DSL compiler to be discovered by Tapioca, it either needs to be placed inside the `sorbet/tapioca/compilers` directory of your application or be inside a `tapioca/dsl/compilers` folder on the load path. For example, if `Encryptable` was being exposed by a gem, all the gem needs to do is to place the DSL compiler inside the `lib/tapioca/dsl/compilers` folder and it will be automatically discovered and loaded by Tapioca. + +There are two main parts to the DSL compiler API: `gather_constants` and `decorate`: + +* The `gather_constants` class method collects all classes (or modules) that should be processed by this specific DSL compiler. +* The `decorate` method defines how to generate the necessary RBI definitions for the gathered constants. + +Every compiler must declare the type member `ConstantType` in order for Sorbet to understand what the return type of the `constant` attribute reader is. It needs to be assigned the correct type variable matching the type of constants that `gather_constants` returns. This generic variable allows Sorbet to type-check method calls on the `constant` reader in your `decorate` method. See the Sorbet documentation on [generics](https://sorbet.org/docs/generics) for more information. + +You can now run the new RBI compiler through the normal DSL generation process (your custom compiler will be loaded automatically by Tapioca): + +```shell +$ bin/tapioca dsl + +Loading Rails application... Done +Loading DSL compiler classes... Done +Compiling DSL RBI files... + + create sorbet/rbi/dsl/credit_card.rbi + +Done +``` + +And then run Sorbet without error: + +```shell +$ bundle exec srb tc + +No errors! Great job. +``` + +For more concrete and advanced examples, take a look at [Tapioca's default DSL compilers](https://github.com/Shopify/tapioca/tree/main/lib/tapioca/dsl/compilers). + +## Writing custom DSL extensions + +When writing custom DSL compilers, it is sometimes necessary to rely on an extension, i.e. a bit of code that is being loaded before the application in order to override some behavior. This is typically useful when a DSL's implementation does not store enough information for the compiler to properly define signatures. + +Let's reuse the previous `Encryptable` module as an example, but this time let's imagine that the implementation of `attr_encrypted` does not store attribute names: + + +```rb +module Encryptable + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def attr_encrypted(attr_name) + attr_accessor(attr_name) + + encrypted_attr_name = :"#{attr_name}_encrypted" + + define_method(encrypted_attr_name) do + value = send(attr_name) + encrypt(value) + end + + define_method("#{encrypted_attr_name}=") do |value| + send("#{attr_name}=", decrypt(value)) + end + end + end + + private + + def encrypt(value) + value.unpack("H*").first + end + + def decrypt(value) + [value].pack("H*") + end +end +``` + +Without the `attribute_names` array, the compiler has no way of knowing which methods were defined by the `attr_encrypted` DSL. This can be solved by defining an extension that will override the behavior of `attr_encrypted`: + +```rb +require "encryptable" + +module Tapioca + module Extensions + module Encryptable + attr_reader :__tapioca_encrypted_attributes + + def attr_encrypted(attr_name) + @__tapioca_encrypted_attributes ||= [] + @__tapioca_encrypted_attributes << attr_name.to_s + + super + end + + ::Encryptable::ClassMethods.prepend(self) + end + end +end +``` + +The compiler can now use the `__tapioca_encrypted_attributes` array managed by the extension: + +```rb +module Tapioca + module Compilers + class Encryptable < Tapioca::Dsl::Compiler + extend T::Sig + + ConstantType = type_member {{ fixed: T.class_of(Encryptable) }} + + sig { override.returns(T::Enumerable[Module]) } + def self.gather_constants + # Collect all the classes that include Encryptable + all_classes.select { |c| c < ::Encryptable } + end + + sig { override.void } + def decorate + # Create a RBI definition for each class that includes Encryptable + root.create_path(constant) do |klass| + # For each encrypted attribute we find in the class + constant.__tapioca_encrypted_attributes.each do |attr_name| + # Create the RBI definitions for all the missing methods + klass.create_method(attr_name, return_type: "String") + klass.create_method("#{attr_name}=", parameters: [ create_param("value", type: "String") ], return_type: "void") + klass.create_method("#{attr_name}_encrypted", return_type: "String") + klass.create_method("#{attr_name}_encrypted=", parameters: [ create_param("value", type: "String") ], return_type: "void") + end + end + end + end + end +end +``` + +In order for DSL extensions to be discovered by Tapioca, they either needs to be placed inside the `sorbet/tapioca/extensions` directory of your application or be inside a `tapioca/dsl/extensions` folder on the load path. + +For more concrete and advanced examples, take a look at [Tapioca's default DSL extensions](https://github.com/Shopify/tapioca/tree/main/lib/tapioca/dsl/extensions). + +### 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 _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: + +```shell +$ 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 +Loading annotation RBIs from sorbet/rbi/annotations... 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 +$ 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 + [--annotations-rbi-dir=ANNOTATIONS_RBI_DIR] # Path to annotations RBIs + # Default: sorbet/rbi/annotations + [--todo-rbi-file=TODO_RBI_FILE] # Path to the generated todo RBI file + # Default: sorbet/rbi/todo.rbi + [--payload], [--no-payload], [--skip-payload] # Check shims against Sorbet's payload + # Default: true + -w, [--workers=N] # Number of parallel workers (default: auto) + -c, [--config=] # Path to the Tapioca configuration file + # Default: sorbet/tapioca/config.yml + -V, [--verbose], [--no-verbose], [--skip-verbose] # Verbose output for debugging purposes + # Default: false + +Check duplicated definitions in shim RBIs +``` + + +Depending on the amount of meta-programming used in your project this can mean an overwhelming amount of manual work. In this case, you should consider [writing a custom DSL compiler](#writing-custom-dsl-compilers). diff --git a/jekyll/rbi_files_for_gems.markdown b/jekyll/rbi_files_for_gems.markdown new file mode 100644 index 000000000..45abea8fa --- /dev/null +++ b/jekyll/rbi_files_for_gems.markdown @@ -0,0 +1,213 @@ +--- +layout: default +title: RBI files for gems +parent: Tapioca +nav_order: 1 +--- + +# 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: + +```shell +$ bin/tapioca gems [gems...] + +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 +$ tapioca help gem + +Usage: + tapioca gem [gem...] + +Options: + --out, -o, [--outdir=directory] # The output directory for generated gem RBI files + # Default: sorbet/rbi/gems + [--file-header], [--no-file-header], [--skip-file-header] # Add a "This file is generated" header on top of each generated RBI file + # Default: true + [--all], [--no-all], [--skip-all] # Regenerate RBI files for all gems + # Default: false + --pre, -b, [--prerequire=file] # A file to be required before Bundler.require is called + --post, -a, [--postrequire=file] # A file to be required after Bundler.require is called + # Default: sorbet/tapioca/require.rb + -x, [--exclude=gem [gem ...]] # Exclude the given gem(s) from RBI generation + [--include-dependencies], [--no-include-dependencies], [--skip-include-dependencies] # Generate RBI files for dependencies of the given gem(s) + # Default: false + --typed, -t, [--typed-overrides=gem:level [gem:level ...]] # Override for typed sigils for generated gem RBIs + # Default: {"activesupport" => "false"} + [--verify], [--no-verify], [--skip-verify] # Verify RBIs are up-to-date + # Default: false + [--doc], [--no-doc], [--skip-doc] # Include YARD documentation from sources when generating RBIs. Warning: this might be slow + # Default: true + [--loc], [--no-loc], [--skip-loc] # Include comments with source location when generating RBIs + # Default: true + [--exported-gem-rbis], [--no-exported-gem-rbis], [--skip-exported-gem-rbis] # Include RBIs found in the `rbi/` directory of the gem + # Default: true + -w, [--workers=N] # Number of parallel workers to use when generating RBIs (default: auto) + [--auto-strictness], [--no-auto-strictness], [--skip-auto-strictness] # Autocorrect strictness in gem RBIs in case of conflict with the DSL RBIs + # Default: true + --dsl-dir, [--dsl-dir=directory] # The DSL directory used to correct gems strictnesses + # Default: sorbet/rbi/dsl + [--rbi-max-line-length=N] # Set the max line length of generated RBIs. Signatures longer than the max line length will be wrapped + # Default: 120 + -e, [--environment=ENVIRONMENT] # The Rack/Rails environment to use when generating RBIs + # Default: development + [--halt-upon-load-error], [--no-halt-upon-load-error], [--skip-halt-upon-load-error] # Halt upon a load error while loading the Rails application + # Default: true + -c, [--config=] # Path to the Tapioca configuration file + # Default: sorbet/tapioca/config.yml + -V, [--verbose], [--no-verbose], [--skip-verbose] # Verbose output for debugging purposes + # Default: false + +Generate RBIs from gems +``` + + +By default, running `tapioca gem` will only generate the RBI files for gems that have been added to or removed from the project's `Gemfile` this means that Tapioca will not regenerate the RBI files for untouched gems. If you want to force the regeneration you can supply gem names to the `tapioca gem` command. When supplying gem names if you want to generate RBI files for their dependencies as well, you can use the `--include-dependencies` option. When changing Tapioca configuration or bumping its version, it may be useful to force the regeneration of all the RBI files previously generated. This can be done with the `--all` option: + +```shell +bin/tapioca gems --all +``` + +> 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). + +## Manually requiring parts of a gem + +It may happen that the RBI file generated for a gem listed inside your `Gemfile.lock` is missing some definitions that 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 +$ bundle exec irb + +irb(main):001> require 'better_html' +=> true +irb(main):002> BetterHtml +=> BetterHtml +irb(main):003> BetterHtml::Parser +(irb):3:in '
': uninitialized constant BetterHtml::Parser (NameError) +Did you mean? BetterHtml::ParserError +irb(main):004> require 'better_html/parser' +=> true +irb(main):005> 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 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. + +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" +``` + +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: + +```shell +$ 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. + +To do so you can pass the list of gems you want to exclude in the command line with the `--exclude` option: + +```shell +$ bin/tapioca gems --exclude gemA gemB +``` + +Or through the configuration file: + +```yaml +gem: + exclude: + - gemA + - gemB +``` + +There are a few development/test environment gems that can cause RBI generation issues, so Tapioca skips them by default: + +* `debug` +* `fakefs` + +## 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). + +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: + +```shell +$ 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 present and have the correct version based on your `Gemfile.lock`, Tapioca provides a `--verify` option: + +```shell +$ bin/tapioca gems --verify + +Checking for out-of-date RBIs... + +Nothing to do, all RBIs are up-to-date. +``` + +This option can be used in CI to make sure the RBI files are *up-to-date* and ensure accurate type checking. + +**Warning**: doing so will break your normal automated dependency update workflow as every pull request opened to bump a gem version will fail CI since the RBI will be out-of-date. You will need to either set up additional automation (eg [Dependabot](https://github.com/dependabot/dependabot-core/issues/5962#issuecomment-1303781931)), or manually run `bin/tapioca gems` and commit the results. + +**Warning**: Verification ONLY ensures the RBI files are present, used and have the correct version based on the gem version in your `Gemfile.lock`. It's possible for your RBIs to be out-of-date if RBIs were not regenerated following an update to tapioca itself or if a another gem that injects functionality (e.g. `turbo-rails`) was installed/updated/removed. To ensure RBIs are completely up-to-date, you must run `bin/tapioca gems --all` but it's not recommended to do this in CI as it's an expensive operation. + +## Importing hand written signatures from gem's `rbi/` folder + +Tapioca will import any signatures found in the `rbi/` folder of a given gem and combine them with the RBIs it generates. This is useful when a gem doesn't want to depend on `sorbet-runtime` but still wants to provide type safety to users during static checks. Note that the `rbi/` folder needs to be included in the gem release using the `.gemspec` file. Applications can choose not to import these signatures using the `--no-exported-gem-rbis` flag. diff --git a/jekyll/rbi_files_for_missing_constants_and_methods.markdown b/jekyll/rbi_files_for_missing_constants_and_methods.markdown new file mode 100644 index 000000000..f0376692b --- /dev/null +++ b/jekyll/rbi_files_for_missing_constants_and_methods.markdown @@ -0,0 +1,88 @@ +--- +layout: default +title: RBI files for missing constants and methods +parent: Tapioca +nav_order: 4 +--- + +# 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 _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: + +```shell +$ 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 +Loading annotation RBIs from sorbet/rbi/annotations... 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 +$ 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 + [--annotations-rbi-dir=ANNOTATIONS_RBI_DIR] # Path to annotations RBIs + # Default: sorbet/rbi/annotations + [--todo-rbi-file=TODO_RBI_FILE] # Path to the generated todo RBI file + # Default: sorbet/rbi/todo.rbi + [--payload], [--no-payload], [--skip-payload] # Check shims against Sorbet's payload + # Default: true + -w, [--workers=N] # Number of parallel workers (default: auto) + -c, [--config=] # Path to the Tapioca configuration file + # Default: sorbet/tapioca/config.yml + -V, [--verbose], [--no-verbose], [--skip-verbose] # Verbose output for debugging purposes + # Default: false + +Check duplicated definitions in shim RBIs +``` + + +Depending on the amount of meta-programming used in your project this can mean an overwhelming amount of manual work. In this case, you should consider [writing a custom DSL compiler](#writing-custom-dsl-compilers). From fb0b6af662f70ba66ed2c277ff8736f2ec26b92a Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Sun, 2 Feb 2025 23:07:24 +0000 Subject: [PATCH 3/7] Improve configs --- jekyll/_config.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/jekyll/_config.yml b/jekyll/_config.yml index 3266fcbc5..19aabd63d 100644 --- a/jekyll/_config.yml +++ b/jekyll/_config.yml @@ -4,16 +4,27 @@ description: >- baseurl: "/tapioca" url: "https://shopify.github.io" github_username: shopify + +defaults: + - scope: + path: "" + values: + image: /tapioca-logo.svg + aux_links: - "Tapioca on GitHub": - - "https://github.com/Shopify/tapioca" "Report an Issue": - "https://github.com/Shopify/tapioca/issues/new" - "Sorbet.org": - - "https://sorbet.org" +nav_external_links: + - title: "Tapioca on GitHub" + url: "https://github.com/Shopify/tapioca" + opens_in_new_tab: true + - title: "Sorbet.org" + url: "https://sorbet.org" + opens_in_new_tab: true theme: just-the-docs layout: minimal plugins: - jekyll-feed + - jekyll-seo-tag destination: ../doc From b3dab7fea9e425bf5455fbcb2e3ee17539075ae9 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Sun, 2 Feb 2025 23:17:14 +0000 Subject: [PATCH 4/7] Add configuration section and update page titles --- jekyll/index.markdown | 93 +++++++++++++++++++ ...bi_annotation_from_remote_sources.markdown | 2 +- jekyll/rbi_files_for_dsls.markdown | 2 +- jekyll/rbi_files_for_gems.markdown | 2 +- ...for_missing_constants_and_methods.markdown | 2 +- 5 files changed, 97 insertions(+), 4 deletions(-) diff --git a/jekyll/index.markdown b/jekyll/index.markdown index b3599f91c..fe7ca596f 100644 --- a/jekyll/index.markdown +++ b/jekyll/index.markdown @@ -2,6 +2,7 @@ layout: default title: Tapioca nav_order: 0 +has_toc: false --- # Tapioca @@ -86,3 +87,95 @@ Options: Get project ready for type checking ``` + +## Usage + +- [Generate RBI files for gems](rbi_files_for_gems) +- [Generate RBI files for Rails and other DSLs](rbi_files_for_dsls) +- [Pull RBI annotations from remote sources](rbi_annotation_from_remote_sources) +- [Create RBI files for missing constants and methods](rbi_files_for_missing_constants_and_methods) + +## 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'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: + +```yaml +gem: + doc: true +``` + +Additionally, if you always want to exclude the `AASM` and `ActiveRecordFixtures` DSL compilers in your DSL RBI generation runs, your config file would then look like this: + +```yaml +gem: + doc: true +dsl: + exclude: + - UrlHelpers + - ActiveRecordFixtures +``` + +The full configuration file, with each option and its default value, would look something like this: + +```yaml +--- +require: + postrequire: sorbet/tapioca/require.rb +todo: + todo_file: sorbet/rbi/todo.rbi + file_header: true +dsl: + outdir: sorbet/rbi/dsl + file_header: true + only: [] + exclude: [] + verify: false + quiet: false + workers: 1 + rbi_max_line_length: 120 + environment: development + list_compilers: false + app_root: "." + halt_upon_load_error: true + skip_constant: [] + compiler_options: {} +gem: + outdir: sorbet/rbi/gems + file_header: true + all: false + prerequire: '' + postrequire: sorbet/tapioca/require.rb + exclude: [] + include_dependencies: false + typed_overrides: + activesupport: 'false' + verify: false + doc: true + loc: true + exported_gem_rbis: true + workers: 1 + auto_strictness: true + dsl_dir: sorbet/rbi/dsl + rbi_max_line_length: 120 + environment: development + halt_upon_load_error: true +check_shims: + gem_rbi_dir: sorbet/rbi/gems + dsl_rbi_dir: sorbet/rbi/dsl + shim_rbi_dir: sorbet/rbi/shims + annotations_rbi_dir: sorbet/rbi/annotations + todo_rbi_file: sorbet/rbi/todo.rbi + payload: true + workers: 1 +annotations: + sources: + - https://raw.githubusercontent.com/Shopify/rbi-central/main + netrc: true + netrc_file: '' + typed_overrides: {} +``` + diff --git a/jekyll/rbi_annotation_from_remote_sources.markdown b/jekyll/rbi_annotation_from_remote_sources.markdown index 9764c7924..c35f140f9 100644 --- a/jekyll/rbi_annotation_from_remote_sources.markdown +++ b/jekyll/rbi_annotation_from_remote_sources.markdown @@ -1,6 +1,6 @@ --- layout: default -title: Pulling RBI annotations from remote sources +title: Pull RBI annotations from remote sources parent: Tapioca nav_order: 3 --- diff --git a/jekyll/rbi_files_for_dsls.markdown b/jekyll/rbi_files_for_dsls.markdown index 19186e89e..7915f342f 100644 --- a/jekyll/rbi_files_for_dsls.markdown +++ b/jekyll/rbi_files_for_dsls.markdown @@ -1,6 +1,6 @@ --- layout: default -title: RBI files for Rails and other DSLs +title: Generate RBI files for Rails and other DSLs parent: Tapioca nav_order: 2 --- diff --git a/jekyll/rbi_files_for_gems.markdown b/jekyll/rbi_files_for_gems.markdown index 45abea8fa..558e3e008 100644 --- a/jekyll/rbi_files_for_gems.markdown +++ b/jekyll/rbi_files_for_gems.markdown @@ -1,6 +1,6 @@ --- layout: default -title: RBI files for gems +title: Generate RBI files for gems parent: Tapioca nav_order: 1 --- diff --git a/jekyll/rbi_files_for_missing_constants_and_methods.markdown b/jekyll/rbi_files_for_missing_constants_and_methods.markdown index f0376692b..e8ce2b0a6 100644 --- a/jekyll/rbi_files_for_missing_constants_and_methods.markdown +++ b/jekyll/rbi_files_for_missing_constants_and_methods.markdown @@ -1,6 +1,6 @@ --- layout: default -title: RBI files for missing constants and methods +title: Create RBI files for missing constants and methods parent: Tapioca nav_order: 4 --- From 0fa00d58ed04dd6a66344b16d297cf997f902f2c Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Mon, 3 Feb 2025 15:42:31 +0000 Subject: [PATCH 5/7] Add workflow to publish new docs --- .github/workflows/publish_docs.yml | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/publish_docs.yml diff --git a/.github/workflows/publish_docs.yml b/.github/workflows/publish_docs.yml new file mode 100644 index 000000000..44fdba72e --- /dev/null +++ b/.github/workflows/publish_docs.yml @@ -0,0 +1,38 @@ +name: Publish docs + +on: + push: + branches: [main] + paths: + - 'jekyll/**' + - '.github/workflows/publish_docs.yml' + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + if: github.repository == 'Shopify/tapioca' + name: Publish documentation website + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + working-directory: ./jekyll + + - name: Configure git + run: | + git config user.name github-actions + git config user.email github-actions@github.com + + - name: Generate documentation + working-directory: ./jekyll + run: bundle exec jekyll build + + - name: Commit to gh-pages + run: | + git add doc -f + git commit -m "Publish website $(git log --format=format:%h -1)" + git push --force origin main:gh-pages From d3cabd7a7541dbd11d2778dd6aa33a74f976141f Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Mon, 3 Feb 2025 16:13:17 +0000 Subject: [PATCH 6/7] Use docs folder to match GH pages' convention --- .github/workflows/publish_docs.yml | 2 +- .gitignore | 2 +- jekyll/_config.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish_docs.yml b/.github/workflows/publish_docs.yml index 44fdba72e..7be22bcee 100644 --- a/.github/workflows/publish_docs.yml +++ b/.github/workflows/publish_docs.yml @@ -33,6 +33,6 @@ jobs: - name: Commit to gh-pages run: | - git add doc -f + git add docs -f git commit -m "Publish website $(git log --format=format:%h -1)" git push --force origin main:gh-pages diff --git a/.gitignore b/.gitignore index 379d5d1b2..716629600 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ /.yardoc /_yardoc/ /coverage/ -/doc/ +/docs/ /pkg/ /spec/dummy/log/ /spec/dummy/tmp/ diff --git a/jekyll/_config.yml b/jekyll/_config.yml index 19aabd63d..e9cb40e97 100644 --- a/jekyll/_config.yml +++ b/jekyll/_config.yml @@ -27,4 +27,4 @@ layout: minimal plugins: - jekyll-feed - jekyll-seo-tag -destination: ../doc +destination: ../docs From 234121c3e421611d666649a86167872f93803bc1 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Mon, 3 Feb 2025 16:14:53 +0000 Subject: [PATCH 7/7] Bump Jekyll version --- jekyll/Gemfile | 2 +- jekyll/Gemfile.lock | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/jekyll/Gemfile b/jekyll/Gemfile index c8e12d98a..bca57adfa 100644 --- a/jekyll/Gemfile +++ b/jekyll/Gemfile @@ -3,7 +3,7 @@ source "https://rubygems.org" # We only need to use Jekyll with CRuby -gem "jekyll", "~> 4.3.3" +gem "jekyll", "~> 4.4.1" gem "jekyll-feed", "~> 0.12" # Theme diff --git a/jekyll/Gemfile.lock b/jekyll/Gemfile.lock index 6265ca3a3..cc70c285a 100644 --- a/jekyll/Gemfile.lock +++ b/jekyll/Gemfile.lock @@ -45,17 +45,20 @@ GEM http_parser.rb (0.8.0) i18n (1.14.7) concurrent-ruby (~> 1.0) - jekyll (4.3.4) + jekyll (4.4.1) addressable (~> 2.4) + base64 (~> 0.2) colorator (~> 1.0) + csv (~> 3.0) em-websocket (~> 0.5) i18n (~> 1.0) jekyll-sass-converter (>= 2.0, < 4.0) jekyll-watch (~> 2.0) + json (~> 2.6) kramdown (~> 2.3, >= 2.3.1) kramdown-parser-gfm (~> 1.0) liquid (~> 4.0) - mercenary (>= 0.3.6, < 0.5) + mercenary (~> 0.3, >= 0.3.6) pathutil (~> 0.9) rouge (>= 3.0, < 5.0) safe_yaml (~> 1.0) @@ -71,6 +74,7 @@ GEM jekyll (>= 3.8, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) + json (2.9.1) just-the-docs (0.10.1) jekyll (>= 3.8.5) jekyll-include-cache @@ -164,7 +168,7 @@ PLATFORMS DEPENDENCIES base64 csv - jekyll (~> 4.3.3) + jekyll (~> 4.4.1) jekyll-feed (~> 0.12) just-the-docs (~> 0.10.0)