tapioca 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/README.md +491 -73
- data/lib/tapioca/cli.rb +40 -3
- data/lib/tapioca/commands/annotations.rb +154 -0
- data/lib/tapioca/commands/dsl.rb +20 -1
- data/lib/tapioca/commands/gem.rb +17 -57
- data/lib/tapioca/commands/init.rb +1 -0
- data/lib/tapioca/commands/todo.rb +4 -2
- data/lib/tapioca/commands.rb +1 -0
- data/lib/tapioca/dsl/compiler.rb +2 -2
- data/lib/tapioca/dsl/compilers/aasm.rb +1 -1
- data/lib/tapioca/dsl/compilers/action_controller_helpers.rb +1 -1
- data/lib/tapioca/dsl/compilers/action_mailer.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_job.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_model_secure_password.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_associations.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_columns.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_enum.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_fixtures.rb +3 -1
- data/lib/tapioca/dsl/compilers/active_record_relations.rb +8 -8
- data/lib/tapioca/dsl/compilers/active_record_scope.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_typed_store.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_resource.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_storage.rb +6 -2
- data/lib/tapioca/dsl/compilers/active_support_concern.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_support_current_attributes.rb +1 -1
- data/lib/tapioca/dsl/compilers/config.rb +2 -2
- data/lib/tapioca/dsl/compilers/frozen_record.rb +1 -1
- data/lib/tapioca/dsl/compilers/identity_cache.rb +1 -1
- data/lib/tapioca/dsl/compilers/mixed_in_class_attributes.rb +1 -1
- data/lib/tapioca/dsl/compilers/protobuf.rb +27 -3
- data/lib/tapioca/dsl/compilers/rails_generators.rb +1 -1
- data/lib/tapioca/dsl/compilers/sidekiq_worker.rb +1 -1
- data/lib/tapioca/dsl/compilers/smart_properties.rb +1 -1
- data/lib/tapioca/dsl/compilers/state_machines.rb +1 -1
- data/lib/tapioca/dsl/compilers/url_helpers.rb +5 -2
- data/lib/tapioca/dsl/helpers/param_helper.rb +4 -1
- data/lib/tapioca/dsl/pipeline.rb +32 -1
- data/lib/tapioca/dsl.rb +6 -0
- data/lib/tapioca/executor.rb +4 -46
- data/lib/tapioca/gem/listeners/methods.rb +26 -1
- data/lib/tapioca/gem/listeners/sorbet_props.rb +1 -1
- data/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb +1 -0
- data/lib/tapioca/gem/listeners/sorbet_signatures.rb +1 -1
- data/lib/tapioca/gem/pipeline.rb +5 -1
- data/lib/tapioca/gemfile.rb +50 -3
- data/lib/tapioca/helpers/config_helper.rb +13 -0
- data/lib/tapioca/helpers/rbi_helper.rb +114 -7
- data/lib/tapioca/helpers/shims_helper.rb +36 -8
- data/lib/tapioca/helpers/signatures_helper.rb +17 -0
- data/lib/tapioca/helpers/sorbet_helper.rb +5 -11
- data/lib/tapioca/helpers/test/content.rb +1 -0
- data/lib/tapioca/helpers/test/dsl_compiler.rb +1 -0
- data/lib/tapioca/helpers/test/template.rb +1 -0
- data/lib/tapioca/helpers/type_variable_helper.rb +43 -0
- data/lib/tapioca/internal.rb +4 -1
- data/lib/tapioca/rbi_ext/model.rb +14 -2
- data/lib/tapioca/repo_index.rb +41 -0
- data/lib/tapioca/runtime/generic_type_registry.rb +4 -2
- data/lib/tapioca/runtime/loader.rb +3 -0
- data/lib/tapioca/runtime/reflection.rb +17 -13
- data/lib/tapioca/sorbet_ext/generic_name_patch.rb +38 -21
- data/lib/tapioca/static/symbol_table_parser.rb +2 -0
- data/lib/tapioca/version.rb +1 -1
- data/lib/tapioca.rb +5 -0
- metadata +26 -21
data/README.md
CHANGED
@@ -1,60 +1,48 @@
|
|
1
1
|
> :warning: **Note**: This software is currently under active development. The API and interface should be considered unstable until a v1.0.0 release.
|
2
2
|
|
3
|
-
|
3
|
+
<p align="center">
|
4
|
+
<img alt="Tapioca logo" width="200" src="misc/tapioca-logo.svg" />
|
5
|
+
</p>
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
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.
|
8
|
-
|
9
|
-
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).
|
10
|
-
|
11
|
-
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.
|
12
|
-
|
13
|
-
## Manual gem requires
|
14
|
-
|
15
|
-
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.
|
16
|
-
|
17
|
-
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:
|
7
|
+
# Tapioca - The swiss army knife of RBI generation
|
18
8
|
|
19
|
-
|
20
|
-
$ bundle exec pry
|
21
|
-
[1] pry(main)> require 'better_html'
|
22
|
-
=> true
|
23
|
-
[2] pry(main)> BetterHtml
|
24
|
-
=> BetterHtml
|
25
|
-
[3] pry(main)> BetterHtml::Parser
|
26
|
-
NameError: uninitialized constant BetterHtml::Parser
|
27
|
-
from (pry):3:in `__pry__`
|
28
|
-
[4] pry(main)> require 'better_html/parser'
|
29
|
-
=> true
|
30
|
-
[5] pry(main)> BetterHtml::Parser
|
31
|
-
=> BetterHtml::Parser
|
32
|
-
```
|
33
|
-
|
34
|
-
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.
|
35
|
-
|
36
|
-
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.
|
37
|
-
|
38
|
-
You can use the command `tapioca require` to auto-populate the `sorbet/tapioca/require.rb` file with all the requires found
|
39
|
-
in your application. Once the file generated, you should review it, remove all unnecessary requires and commit it.
|
40
|
-
|
41
|
-
## How does tapioca compare to "srb rbi gems" ?
|
9
|
+
![Build Status](https://github.com/Shopify/tapioca/workflows/CI/badge.svg)
|
42
10
|
|
43
|
-
[
|
11
|
+
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.
|
12
|
+
|
13
|
+
**Features**:
|
14
|
+
|
15
|
+
* Easy installation and configuration
|
16
|
+
* Generation of RBI files for the gems used in your application
|
17
|
+
* Automatic generation from your application's Gemfile
|
18
|
+
* Importing of signatures from the source code of gems
|
19
|
+
* Importing of documentation from the source code of gems
|
20
|
+
* Synchronization validation for your CI
|
21
|
+
* Generation of RBI files for various DSL patterns that relies on meta-programming
|
22
|
+
* Automatic generation from your application's content
|
23
|
+
* Support many DSL patterns such as Rails, Google Protobuf, SmartProperties and more out of the box
|
24
|
+
* Extensible interface that allows you to write your own DSL compilers for other DSL patterns
|
25
|
+
* Automatic generation of signatures for methods from known DSLs
|
26
|
+
* Synchronization validation for your CI
|
27
|
+
* Management of shim RBI files
|
28
|
+
* Find useless definitions in shim RBI files from gems generated RBI files
|
29
|
+
* Find useless definitions in shim RBI files from DSL generated RBI files
|
30
|
+
* Find useless definitions in shim RBI files from Sorbet's embedded RBI for core and stdlib
|
31
|
+
* Synchronization validation for your CI
|
44
32
|
|
45
33
|
## Installation
|
46
34
|
|
47
35
|
Add this line to your application's `Gemfile`:
|
48
36
|
|
49
|
-
```
|
37
|
+
```rb
|
50
38
|
group :development do
|
51
39
|
gem 'tapioca', require: false
|
52
40
|
end
|
53
41
|
```
|
54
42
|
|
55
|
-
|
43
|
+
Run `bundle install` and make sure Tapioca is properly installed:
|
56
44
|
|
57
|
-
```
|
45
|
+
```sh
|
58
46
|
$ bundle exec tapioca help
|
59
47
|
Commands:
|
60
48
|
tapioca --version, -v # show version
|
@@ -72,16 +60,27 @@ Options:
|
|
72
60
|
-V, [--verbose], [--no-verbose] # Verbose output for debugging purposes
|
73
61
|
```
|
74
62
|
|
75
|
-
##
|
63
|
+
## Getting started
|
76
64
|
|
77
|
-
|
65
|
+
Execute this command to get started:
|
78
66
|
|
79
|
-
|
67
|
+
```sh
|
68
|
+
$ bundle exec tapioca init
|
69
|
+
create sorbet/config
|
70
|
+
create sorbet/tapioca/config.yml
|
71
|
+
create sorbet/tapioca/require.rb
|
72
|
+
create bin/tapioca
|
73
|
+
```
|
80
74
|
|
81
|
-
This will
|
75
|
+
This will:
|
76
|
+
|
77
|
+
* 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)
|
78
|
+
* 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
|
82
79
|
|
83
80
|
<!-- START_HELP_COMMAND_INIT -->
|
84
81
|
```shell
|
82
|
+
$ tapioca help init
|
83
|
+
|
85
84
|
Usage:
|
86
85
|
tapioca init
|
87
86
|
|
@@ -94,14 +93,40 @@ initializes folder structure
|
|
94
93
|
```
|
95
94
|
<!-- END_HELP_COMMAND_INIT -->
|
96
95
|
|
97
|
-
|
96
|
+
## Usage
|
97
|
+
|
98
|
+
### Generating RBI files for gems
|
99
|
+
|
100
|
+
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.
|
101
|
+
|
102
|
+
In order to generate the RBI files for the gems used in your application, run the following command:
|
103
|
+
|
104
|
+
```sh
|
105
|
+
$ bin/tapioca gems [gems...]
|
106
|
+
|
107
|
+
Removing RBI files of gems that have been removed:
|
98
108
|
|
99
|
-
|
109
|
+
Nothing to do.
|
100
110
|
|
101
|
-
|
111
|
+
Generating RBI files of gems that are added or updated:
|
112
|
+
|
113
|
+
Requiring all gems to prepare for compiling... Done
|
114
|
+
|
115
|
+
Compiled ansi
|
116
|
+
create sorbet/rbi/gems/ansi@1.5.0.rbi
|
117
|
+
|
118
|
+
...
|
119
|
+
|
120
|
+
All operations performed in working directory.
|
121
|
+
Please review changes and commit them.
|
122
|
+
```
|
123
|
+
|
124
|
+
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.
|
102
125
|
|
103
126
|
<!-- START_HELP_COMMAND_GEM -->
|
104
127
|
```shell
|
128
|
+
$ tapioca help gem
|
129
|
+
|
105
130
|
Usage:
|
106
131
|
tapioca gem [gem...]
|
107
132
|
|
@@ -119,6 +144,7 @@ Options:
|
|
119
144
|
# Default: {"activesupport"=>"false"}
|
120
145
|
[--verify], [--no-verify] # Verify RBIs are up-to-date
|
121
146
|
[--doc], [--no-doc] # Include YARD documentation from sources when generating RBIs. Warning: this might be slow
|
147
|
+
# Default: true
|
122
148
|
[--exported-gem-rbis], [--no-exported-gem-rbis] # Include RBIs found in the `rbi/` directory of the gem
|
123
149
|
# Default: true
|
124
150
|
-w, [--workers=N] # EXPERIMENTAL: Number of parallel workers to use when generating RBIs
|
@@ -137,38 +163,142 @@ generate RBIs from gems
|
|
137
163
|
```
|
138
164
|
<!-- END_HELP_COMMAND_GEM -->
|
139
165
|
|
140
|
-
|
166
|
+
> 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).
|
141
167
|
|
142
|
-
|
168
|
+
#### Manually requiring parts of a gem
|
143
169
|
|
144
|
-
|
170
|
+
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.
|
171
|
+
|
172
|
+
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.
|
173
|
+
|
174
|
+
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:
|
145
175
|
|
146
|
-
<!-- START_HELP_COMMAND_TODO -->
|
147
176
|
```shell
|
148
|
-
|
149
|
-
tapioca todo
|
177
|
+
$ bundle exec pry
|
150
178
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
179
|
+
[1] pry(main)> require 'better_html'
|
180
|
+
=> true
|
181
|
+
[2] pry(main)> BetterHtml
|
182
|
+
=> BetterHtml
|
183
|
+
[3] pry(main)> BetterHtml::Parser
|
184
|
+
NameError: uninitialized constant BetterHtml::Parser
|
185
|
+
from (pry):3:in `__pry__`
|
186
|
+
[4] pry(main)> require 'better_html/parser'
|
187
|
+
=> true
|
188
|
+
[5] pry(main)> BetterHtml::Parser
|
189
|
+
=> BetterHtml::Parser
|
190
|
+
```
|
159
191
|
|
160
|
-
|
192
|
+
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.
|
193
|
+
|
194
|
+
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):
|
195
|
+
|
196
|
+
```rb
|
197
|
+
# typed: strict
|
198
|
+
# frozen_string_literal: true
|
199
|
+
|
200
|
+
require "ansi/code"
|
201
|
+
require "google/protobuf"
|
202
|
+
require "rails/all"
|
203
|
+
require "rails/generators"
|
204
|
+
require "rails/generators/app_base"
|
205
|
+
require "rake/testtask"
|
206
|
+
require "rubocop/rake_task"
|
207
|
+
```
|
208
|
+
|
209
|
+
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 <gem-name>`.
|
210
|
+
|
211
|
+
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:
|
212
|
+
|
213
|
+
```sh
|
214
|
+
$ bin/tapioca require
|
215
|
+
|
216
|
+
Compiling sorbet/tapioca/require.rb, this may take a few seconds... Done
|
217
|
+
|
218
|
+
All requires from this application have been written to sorbet/tapioca/require.rb.
|
219
|
+
Please review changes and commit them, then run `bin/tapioca gem`.
|
220
|
+
```
|
221
|
+
|
222
|
+
Once the file is generated, you should review it, remove all unnecessary requires and commit it.
|
223
|
+
|
224
|
+
#### Excluding a gem from RBI generation
|
225
|
+
|
226
|
+
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.
|
227
|
+
|
228
|
+
To do so you can pass the list of gems you want to exclude in the command line with the `--exclude` option:
|
229
|
+
|
230
|
+
```sh
|
231
|
+
$ bin/tapioca gems --exclude gemA gemB
|
161
232
|
```
|
162
|
-
<!-- END_HELP_COMMAND_TODO -->
|
163
233
|
|
164
|
-
|
234
|
+
Or through the configuration file:
|
165
235
|
|
166
|
-
|
236
|
+
```yaml
|
237
|
+
gem:
|
238
|
+
exclude:
|
239
|
+
- gemA
|
240
|
+
- gemB
|
241
|
+
```
|
242
|
+
|
243
|
+
#### Changing the strictness level of the RBI for a gem
|
244
|
+
|
245
|
+
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).
|
246
|
+
|
247
|
+
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:
|
248
|
+
|
249
|
+
```sh
|
250
|
+
$ bin/tapioca gems --typed-overrides gemA:false gemB:false
|
251
|
+
```
|
252
|
+
|
253
|
+
Or through the configuration file:
|
254
|
+
|
255
|
+
```yaml
|
256
|
+
gem:
|
257
|
+
typed_overrides:
|
258
|
+
gemA: "false"
|
259
|
+
gemB: "false"
|
260
|
+
```
|
261
|
+
|
262
|
+
#### Keeping RBI files for gems up-to-date
|
263
|
+
|
264
|
+
To ensure all RBI files for gems are up-to-date with the latest changes in your `Gemfile.lock`, Tapioca provides a `--verify` option:
|
265
|
+
|
266
|
+
```sh
|
267
|
+
$ bin/tapioca gems --verify
|
268
|
+
|
269
|
+
Checking for out-of-date RBIs...
|
270
|
+
|
271
|
+
Nothing to do, all RBIs are up-to-date.
|
272
|
+
```
|
273
|
+
|
274
|
+
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.
|
275
|
+
|
276
|
+
### Generating RBI files for Rails and other DSLs
|
277
|
+
|
278
|
+
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`.
|
279
|
+
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.
|
280
|
+
|
281
|
+
To generate the RBI files for the DSLs used in your application, run the following command:
|
282
|
+
|
283
|
+
```sh
|
284
|
+
$ bin/tapioca dsl
|
285
|
+
|
286
|
+
Loading Rails application... Done
|
287
|
+
Loading DSL compiler classes... Done
|
288
|
+
Compiling DSL RBI files...
|
289
|
+
|
290
|
+
create sorbet/rbi/dsl/my_model.rbi
|
291
|
+
...
|
292
|
+
|
293
|
+
Done
|
294
|
+
```
|
167
295
|
|
168
296
|
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).
|
169
297
|
|
170
298
|
<!-- START_HELP_COMMAND_DSL -->
|
171
299
|
```shell
|
300
|
+
$ tapioca help dsl
|
301
|
+
|
172
302
|
Usage:
|
173
303
|
tapioca dsl [constant...]
|
174
304
|
|
@@ -180,7 +310,7 @@ Options:
|
|
180
310
|
[--only=compiler [compiler ...]] # Only run supplied DSL compiler(s)
|
181
311
|
[--exclude=compiler [compiler ...]] # Exclude supplied DSL compiler(s)
|
182
312
|
[--verify], [--no-verify] # Verifies RBIs are up-to-date
|
183
|
-
-q, [--quiet], [--no-quiet] #
|
313
|
+
-q, [--quiet], [--no-quiet] # Suppresses file creation output
|
184
314
|
-w, [--workers=N] # EXPERIMENTAL: Number of parallel workers to use when generating RBIs
|
185
315
|
# Default: 1
|
186
316
|
[--rbi-max-line-length=N] # Set the max line length of generated RBIs. Signatures longer than the max line length will be wrapped
|
@@ -193,13 +323,298 @@ generate RBIs for dynamic methods
|
|
193
323
|
```
|
194
324
|
<!-- END_HELP_COMMAND_DSL -->
|
195
325
|
|
196
|
-
|
326
|
+
#### Keeping RBI files for DSLs up-to-date
|
327
|
+
|
328
|
+
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:
|
329
|
+
|
330
|
+
```sh
|
331
|
+
$ bin/tapioca dsl --verify
|
332
|
+
|
333
|
+
Loading Rails application... Done
|
334
|
+
Loading DSL compiler classes... Done
|
335
|
+
Checking for out-of-date RBIs...
|
336
|
+
|
337
|
+
|
338
|
+
RBI files are out-of-date. In your development environment, please run:
|
339
|
+
`bin/tapioca dsl`
|
340
|
+
Once it is complete, be sure to commit and push any changes
|
341
|
+
|
342
|
+
Reason:
|
343
|
+
File(s) changed:
|
344
|
+
- sorbet/rbi/dsl/my_model.rbi
|
345
|
+
```
|
346
|
+
|
347
|
+
This option can be used on CI to make sure the RBI files are always up-to-date and ensure accurate type checking.
|
348
|
+
|
349
|
+
#### Writing custom DSL compilers
|
350
|
+
|
351
|
+
It is possible to create your own compilers for DSLs not supported by Tapioca out of the box.
|
352
|
+
|
353
|
+
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:
|
354
|
+
|
355
|
+
```rb
|
356
|
+
module Encryptable
|
357
|
+
def self.included(base)
|
358
|
+
base.extend(ClassMethods)
|
359
|
+
end
|
360
|
+
|
361
|
+
module ClassMethods
|
362
|
+
def attr_encrypted(attr_name)
|
363
|
+
encrypted_attributes << attr_name
|
364
|
+
|
365
|
+
attr_accessor(attr_name)
|
366
|
+
|
367
|
+
encrypted_attr_name = :"#{attr_name}_encrypted"
|
368
|
+
|
369
|
+
define_method(encrypted_attr_name) do
|
370
|
+
value = send(attr_name)
|
371
|
+
encrypt(value)
|
372
|
+
end
|
373
|
+
|
374
|
+
define_method("#{encrypted_attr_name}=") do |value|
|
375
|
+
send("#{attr_name}=", decrypt(value))
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def encrypted_attributes
|
380
|
+
@encrypted_attributes ||= []
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
private
|
385
|
+
|
386
|
+
def encrypt(value)
|
387
|
+
value.unpack("H*").first
|
388
|
+
end
|
389
|
+
|
390
|
+
def decrypt(value)
|
391
|
+
[value].pack("H*")
|
392
|
+
end
|
393
|
+
end
|
394
|
+
```
|
395
|
+
|
396
|
+
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:
|
397
|
+
|
398
|
+
```rb
|
399
|
+
class CreditCard
|
400
|
+
include Encryptable
|
401
|
+
|
402
|
+
attr_encrypted :number
|
403
|
+
end
|
404
|
+
```
|
405
|
+
|
406
|
+
These accessors can then be used on the `CreditCard` instance without having to define them in the class:
|
407
|
+
|
408
|
+
```rb
|
409
|
+
# typed: true
|
410
|
+
# file: example.rb
|
411
|
+
|
412
|
+
card = CreditCard.new
|
413
|
+
card.number = "1234 5678 9012 3456"
|
414
|
+
|
415
|
+
p card.number # => "1234 5678 9012 3456"
|
416
|
+
p card.number_encrypted # => "31323334203536373820393031322033343536"
|
417
|
+
|
418
|
+
card.number_encrypted = "31323334203536373820393031322033343536"
|
419
|
+
p card.number # => "1234 5678 9012 3456"
|
420
|
+
```
|
421
|
+
|
422
|
+
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:
|
423
|
+
|
424
|
+
```shell
|
425
|
+
$ bundle exec srb tc
|
426
|
+
|
427
|
+
lib/example.rb:5: Method number= does not exist on CreditCard https://srb.help/7003
|
428
|
+
lib/example.rb:7: Method number does not exist on CreditCard https://srb.help/7003
|
429
|
+
lib/example.rb:8: Method number_encrypted does not exist on CreditCard https://srb.help/7003
|
430
|
+
lib/example.rb:10: Method number_encrypted= does not exist on CreditCard https://srb.help/7003
|
431
|
+
lib/example.rb:11: Method number does not exist on CreditCard https://srb.help/7003
|
432
|
+
|
433
|
+
Errors: 5
|
434
|
+
```
|
435
|
+
|
436
|
+
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.
|
437
|
+
|
438
|
+
To do so, create the new DSL compiler inside the `sorbet/tapioca/compilers` directory of your application with the following contents:
|
439
|
+
|
440
|
+
```rb
|
441
|
+
module Tapioca
|
442
|
+
module Compilers
|
443
|
+
class Encryptable < Tapioca::Dsl::Compiler
|
444
|
+
extend T::Sig
|
445
|
+
|
446
|
+
ConstantType = type_member {{ fixed: T.class_of(Encryptable) }}
|
447
|
+
|
448
|
+
sig { override.returns(T::Enumerable[Module]) }
|
449
|
+
def self.gather_constants
|
450
|
+
# Collect all the classes that include Encryptable
|
451
|
+
all_classes.select { |c| c < ::Encryptable }
|
452
|
+
end
|
453
|
+
|
454
|
+
sig { override.void }
|
455
|
+
def decorate
|
456
|
+
# Create a RBI definition for each class that includes Encryptable
|
457
|
+
root.create_path(constant) do |klass|
|
458
|
+
# For each encrypted attribute we find in the class
|
459
|
+
constant.encrypted_attributes.each do |attr_name|
|
460
|
+
# Create the RBI definitions for all the missing methods
|
461
|
+
klass.create_method(attr_name, return_type: "String")
|
462
|
+
klass.create_method("#{attr_name}=", parameters: [ create_param("value", type: "String") ], return_type: "void")
|
463
|
+
klass.create_method("#{attr_name}_encrypted", return_type: "String")
|
464
|
+
klass.create_method("#{attr_name}_encrypted=", parameters: [ create_param("value", type: "String") ], return_type: "void")
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
```
|
472
|
+
|
473
|
+
There are two main parts to the DSL compiler API: `gather_constants` and `decorate`:
|
474
|
+
|
475
|
+
* The `gather_constants` class method collects all classes (or modules) that should be processed by this specific DSL compiler.
|
476
|
+
* The `decorate` method defines how to generate the necessary RBI definitions for the gathered constants.
|
477
|
+
|
478
|
+
You can now run the new RBI compiler through the normal DSL generation process (your custom compiler will be loaded automatically by Tapioca):
|
479
|
+
|
480
|
+
```shell
|
481
|
+
$ bin/tapioca dsl
|
482
|
+
|
483
|
+
Loading Rails application... Done
|
484
|
+
Loading DSL compiler classes... Done
|
485
|
+
Compiling DSL RBI files...
|
486
|
+
|
487
|
+
create sorbet/rbi/dsl/credit_card.rbi
|
488
|
+
|
489
|
+
Done
|
490
|
+
```
|
491
|
+
|
492
|
+
And then run Sorbet without error:
|
493
|
+
|
494
|
+
```shell
|
495
|
+
$ bundle exec srb tc
|
496
|
+
|
497
|
+
No errors! Great job.
|
498
|
+
```
|
499
|
+
|
500
|
+
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).
|
501
|
+
|
502
|
+
### RBI files for missing constants and methods
|
197
503
|
|
198
|
-
|
199
|
-
file location is `sorbet/tapioca/config.yml` but this default can be changed using the `--config` flag
|
200
|
-
and supplying an alternative configuration file path.
|
504
|
+
Even after generating the RBIs, it is possible that some constants or methods are still undefined for Sorbet.
|
201
505
|
|
202
|
-
|
506
|
+
This might be for multiple reasons, with the most frequents ones being:
|
507
|
+
|
508
|
+
* The constant or method comes from a part of the gem that Tapioca cannot load (optional dependency, wrong architecture, etc.)
|
509
|
+
* The constant or method comes from a DSL or meta-programming that Tapioca doesn't support yet
|
510
|
+
* The constant or method only exists when a specific code path is executed
|
511
|
+
|
512
|
+
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.
|
513
|
+
|
514
|
+
#### Generating the RBI file for missing constants
|
515
|
+
|
516
|
+
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.
|
517
|
+
|
518
|
+
To generate the RBI file for the missing constants used in your application run the following command:
|
519
|
+
|
520
|
+
```sh
|
521
|
+
$ bin/tapioca todo
|
522
|
+
|
523
|
+
Compiling sorbet/rbi/todo.rbi, this may take a few seconds... Done
|
524
|
+
All unresolved constants have been written to sorbet/rbi/todo.rbi.
|
525
|
+
Please review changes and commit them.
|
526
|
+
```
|
527
|
+
|
528
|
+
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.
|
529
|
+
|
530
|
+
<!-- START_HELP_COMMAND_TODO -->
|
531
|
+
```shell
|
532
|
+
$ tapioca help todo
|
533
|
+
|
534
|
+
Usage:
|
535
|
+
tapioca todo
|
536
|
+
|
537
|
+
Options:
|
538
|
+
[--todo-file=TODO_FILE] # Path to the generated todo RBI file
|
539
|
+
# Default: sorbet/rbi/todo.rbi
|
540
|
+
[--file-header], [--no-file-header] # Add a "This file is generated" header on top of each generated RBI file
|
541
|
+
# Default: true
|
542
|
+
-c, [--config=<config file path>] # Path to the Tapioca configuration file
|
543
|
+
# Default: sorbet/tapioca/config.yml
|
544
|
+
-V, [--verbose], [--no-verbose] # Verbose output for debugging purposes
|
545
|
+
|
546
|
+
generate the list of unresolved constants
|
547
|
+
```
|
548
|
+
<!-- END_HELP_COMMAND_TODO -->
|
549
|
+
|
550
|
+
#### Manually writing RBI definitions (shims)
|
551
|
+
|
552
|
+
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.
|
553
|
+
|
554
|
+
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`.
|
555
|
+
|
556
|
+
A shim might be as simple as the class definition with an empty method body as below:
|
557
|
+
|
558
|
+
```ruby
|
559
|
+
# typed: true
|
560
|
+
|
561
|
+
class Person
|
562
|
+
sig { void }
|
563
|
+
def some_method_sorbet_cannot_find; end
|
564
|
+
end
|
565
|
+
```
|
566
|
+
|
567
|
+
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:
|
568
|
+
|
569
|
+
```sh
|
570
|
+
$ bin/tapioca check-shims
|
571
|
+
|
572
|
+
Loading Sorbet payload... Done
|
573
|
+
Loading shim RBIs from sorbet/rbi/shims... Done
|
574
|
+
Loading gem RBIs from sorbet/rbi/gems... Done
|
575
|
+
Loading gem RBIs from sorbet/rbi/dsl... Done
|
576
|
+
Looking for duplicates... Done
|
577
|
+
|
578
|
+
Duplicated RBI for ::MyModel#title:
|
579
|
+
* sorbet/rbi/shims/my_model.rbi:2:2-2:14
|
580
|
+
* sorbet/rbi/dsl/my_model.rbi:2:2-2:14
|
581
|
+
|
582
|
+
Duplicated RBI for ::String#capitalize:
|
583
|
+
* https://github.com/sorbet/sorbet/tree/master/rbi/core/string.rbi#L406
|
584
|
+
* sorbet/rbi/shims/core/string.rbi:3:2-3:23
|
585
|
+
|
586
|
+
Please remove the duplicated definitions from the sorbet/rbi/shims directory.
|
587
|
+
```
|
588
|
+
|
589
|
+
This command can be used on CI to make sure the RBI shims are always up-to-date and non-redundant with generated files.
|
590
|
+
|
591
|
+
<!-- START_HELP_COMMAND_CHECK-SHIMS -->
|
592
|
+
```shell
|
593
|
+
$ bin/tapioca help check-shims
|
594
|
+
|
595
|
+
Usage:
|
596
|
+
tapioca check-shims
|
597
|
+
|
598
|
+
Options:
|
599
|
+
[--gem-rbi-dir=GEM_RBI_DIR] # Path to gem RBIs
|
600
|
+
# Default: sorbet/rbi/gems
|
601
|
+
[--dsl-rbi-dir=DSL_RBI_DIR] # Path to DSL RBIs
|
602
|
+
# Default: sorbet/rbi/dsl
|
603
|
+
[--shim-rbi-dir=SHIM_RBI_DIR] # Path to shim RBIs
|
604
|
+
# Default: sorbet/rbi/shims
|
605
|
+
-c, [--config=<config file path>] # Path to the Tapioca configuration file
|
606
|
+
# Default: sorbet/tapioca/config.yml
|
607
|
+
-V, [--verbose], [--no-verbose] # Verbose output for debugging purposes
|
608
|
+
|
609
|
+
check duplicated definitions in shim RBIs
|
610
|
+
```
|
611
|
+
<!-- END_HELP_COMMAND_CHECK-SHIMS -->
|
612
|
+
|
613
|
+
### Configuration
|
614
|
+
|
615
|
+
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.
|
616
|
+
|
617
|
+
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.
|
203
618
|
|
204
619
|
For example, if you always want to generate gem RBIs with inline documentation, then you would create the file `sorbet/tapioca/config.yml` as:
|
205
620
|
|
@@ -247,7 +662,7 @@ gem:
|
|
247
662
|
typed_overrides:
|
248
663
|
activesupport: 'false'
|
249
664
|
verify: false
|
250
|
-
doc:
|
665
|
+
doc: true
|
251
666
|
exported_gem_rbis: true
|
252
667
|
workers: 1
|
253
668
|
auto_strictness: true
|
@@ -257,6 +672,9 @@ check_shims:
|
|
257
672
|
gem_rbi_dir: sorbet/rbi/gems
|
258
673
|
dsl_rbi_dir: sorbet/rbi/dsl
|
259
674
|
shim_rbi_dir: sorbet/rbi/shims
|
675
|
+
payload: true
|
676
|
+
annotations:
|
677
|
+
repo_uri: https://raw.githubusercontent.com/Shopify/rbi-central/main
|
260
678
|
```
|
261
679
|
<!-- END_CONFIG_TEMPLATE -->
|
262
680
|
|