sorbet-rails 0.6.0 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +69 -24
- data/bin/sorbet +6 -0
- data/lib/sorbet-rails/helper_rbi_formatter.rb +1 -1
- data/lib/sorbet-rails/mailer_rbi_formatter.rb +1 -1
- data/lib/sorbet-rails/model_plugins/active_record_attribute.rb +35 -24
- data/lib/sorbet-rails/model_rbi_formatter.rb +5 -1
- data/lib/sorbet-rails/model_utils.rb +17 -1
- data/lib/sorbet-rails/railtie.rb +10 -0
- data/lib/sorbet-rails/routes_rbi_formatter.rb +1 -1
- data/lib/sorbet-rails/sorbet_utils.rb +120 -25
- data/sorbet-rails.gemspec +5 -3
- data/spec/generators/rails-template.rb +16 -0
- data/spec/model_rbi_formatter_spec.rb +7 -0
- data/spec/sorbet_utils_spec.rb +25 -13
- data/spec/support/v5.0/Gemfile.lock +13 -11
- data/spec/support/v5.0/app/mailers/hogwarts_acceptance_mailer.rb +17 -1
- data/spec/support/v5.1/Gemfile.lock +13 -11
- data/spec/support/v5.1/app/mailers/hogwarts_acceptance_mailer.rb +17 -1
- data/spec/support/v5.2/Gemfile.lock +13 -11
- data/spec/support/v5.2/app/mailers/hogwarts_acceptance_mailer.rb +17 -1
- data/spec/support/v6.0/Gemfile.lock +14 -12
- data/spec/support/v6.0/app/mailers/application_mailer.rb +1 -1
- data/spec/support/v6.0/app/mailers/hogwarts_acceptance_mailer.rb +17 -1
- data/spec/test_data/v5.0/expected_application_mailer.rbi +1 -1
- data/spec/test_data/v5.0/expected_daily_prophet_mailer.rbi +1 -1
- data/spec/test_data/v5.0/expected_helpers.rbi +1 -1
- data/spec/test_data/v5.0/expected_helpers_with_application_and_devise_helpers.rbi +1 -1
- data/spec/test_data/v5.0/expected_hogwarts_acceptance_mailer.rbi +11 -1
- data/spec/test_data/v5.0/expected_internal_metadata.rbi +5 -4
- data/spec/test_data/v5.0/expected_potion.rbi +2 -1
- data/spec/test_data/v5.0/expected_robe.rbi +4 -3
- data/spec/test_data/v5.0/expected_routes.rbi +1 -1
- data/spec/test_data/v5.0/expected_schema_migration.rbi +2 -1
- data/spec/test_data/v5.0/expected_school.rbi +4 -3
- data/spec/test_data/v5.0/expected_spell_book.rbi +5 -4
- data/spec/test_data/v5.0/expected_squib.rbi +10 -9
- data/spec/test_data/v5.0/expected_wand.rbi +11 -10
- data/spec/test_data/v5.0/expected_wizard.rbi +10 -9
- data/spec/test_data/v5.0/expected_wizard_wo_spellbook.rbi +10 -9
- data/spec/test_data/v5.1/expected_application_mailer.rbi +1 -1
- data/spec/test_data/v5.1/expected_daily_prophet_mailer.rbi +1 -1
- data/spec/test_data/v5.1/expected_helpers.rbi +1 -1
- data/spec/test_data/v5.1/expected_helpers_with_application_and_devise_helpers.rbi +1 -1
- data/spec/test_data/v5.1/expected_hogwarts_acceptance_mailer.rbi +11 -1
- data/spec/test_data/v5.1/expected_internal_metadata.rbi +5 -4
- data/spec/test_data/v5.1/expected_potion.rbi +2 -1
- data/spec/test_data/v5.1/expected_robe.rbi +4 -3
- data/spec/test_data/v5.1/expected_routes.rbi +1 -1
- data/spec/test_data/v5.1/expected_schema_migration.rbi +2 -1
- data/spec/test_data/v5.1/expected_school.rbi +4 -3
- data/spec/test_data/v5.1/expected_spell_book.rbi +5 -4
- data/spec/test_data/v5.1/expected_squib.rbi +10 -9
- data/spec/test_data/v5.1/expected_wand.rbi +11 -10
- data/spec/test_data/v5.1/expected_wizard.rbi +10 -9
- data/spec/test_data/v5.1/expected_wizard_wo_spellbook.rbi +10 -9
- data/spec/test_data/v5.2/expected_application_mailer.rbi +1 -1
- data/spec/test_data/v5.2/expected_attachment.rbi +2 -1
- data/spec/test_data/v5.2/expected_blob.rbi +2 -1
- data/spec/test_data/v5.2/expected_daily_prophet_mailer.rbi +1 -1
- data/spec/test_data/v5.2/expected_helpers.rbi +1 -1
- data/spec/test_data/v5.2/expected_helpers_with_application_and_devise_helpers.rbi +1 -1
- data/spec/test_data/v5.2/expected_hogwarts_acceptance_mailer.rbi +11 -1
- data/spec/test_data/v5.2/expected_internal_metadata.rbi +5 -4
- data/spec/test_data/v5.2/expected_potion.rbi +2 -1
- data/spec/test_data/v5.2/expected_robe.rbi +4 -3
- data/spec/test_data/v5.2/expected_routes.rbi +1 -1
- data/spec/test_data/v5.2/expected_schema_migration.rbi +2 -1
- data/spec/test_data/v5.2/expected_school.rbi +4 -3
- data/spec/test_data/v5.2/expected_spell_book.rbi +5 -4
- data/spec/test_data/v5.2/expected_squib.rbi +10 -9
- data/spec/test_data/v5.2/expected_wand.rbi +11 -10
- data/spec/test_data/v5.2/expected_wizard.rbi +10 -9
- data/spec/test_data/v5.2/expected_wizard_wo_spellbook.rbi +10 -9
- data/spec/test_data/v6.0/expected_application_mailer.rbi +1 -1
- data/spec/test_data/v6.0/expected_attachment.rbi +2 -1
- data/spec/test_data/v6.0/expected_blob.rbi +2 -1
- data/spec/test_data/v6.0/expected_daily_prophet_mailer.rbi +1 -1
- data/spec/test_data/v6.0/expected_helpers.rbi +1 -1
- data/spec/test_data/v6.0/expected_helpers_with_application_and_devise_helpers.rbi +1 -1
- data/spec/test_data/v6.0/expected_hogwarts_acceptance_mailer.rbi +11 -1
- data/spec/test_data/v6.0/expected_internal_metadata.rbi +5 -4
- data/spec/test_data/v6.0/expected_potion.rbi +2 -1
- data/spec/test_data/v6.0/expected_robe.rbi +4 -3
- data/spec/test_data/v6.0/expected_routes.rbi +1 -1
- data/spec/test_data/v6.0/expected_schema_migration.rbi +2 -1
- data/spec/test_data/v6.0/expected_school.rbi +4 -3
- data/spec/test_data/v6.0/expected_spell_book.rbi +5 -4
- data/spec/test_data/v6.0/expected_squib.rbi +10 -9
- data/spec/test_data/v6.0/expected_wand.rbi +11 -10
- data/spec/test_data/v6.0/expected_wizard.rbi +10 -9
- data/spec/test_data/v6.0/expected_wizard_wo_spellbook.rbi +10 -9
- metadata +33 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b2400ac696f93d779e930dac9f4d6e5781f1ce976e1ee98599a222a78c658588
|
4
|
+
data.tar.gz: 25d8ef2f00b2eb87afe05144323927df66f328ffe2bcff00c578396824d5053e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8bb839d0eb25c882678e7099796b30ad4215d722a809038593b589c3c6b1bb2b37c42813539e0e314977425052f24b11f85f193f03c84aceb0145d32f0e361bb
|
7
|
+
data.tar.gz: ce4d93314cbeb06d881177429ba7df4b02d5c7783e9cae839dbfa46fb746b611d13dcc49fd996baf839319af47d848d44860e4561474db104d79fd8f797220f4
|
data/README.md
CHANGED
@@ -30,14 +30,14 @@ Add `sorbet-rails` to the [`:default` group](https://bundler.io/v2.0/guides/grou
|
|
30
30
|
|
31
31
|
3. Generate RBI files for your routes, models, etc
|
32
32
|
```sh
|
33
|
-
❯ rake rails_rbi:routes
|
34
|
-
❯ rake rails_rbi:models
|
35
|
-
❯ rake rails_rbi:helpers
|
36
|
-
❯ rake rails_rbi:mailers
|
37
|
-
❯ rake rails_rbi:custom
|
33
|
+
❯ bundle exec rake rails_rbi:routes
|
34
|
+
❯ bundle exec rake rails_rbi:models
|
35
|
+
❯ bundle exec rake rails_rbi:helpers
|
36
|
+
❯ bundle exec rake rails_rbi:mailers
|
37
|
+
❯ bundle exec rake rails_rbi:custom
|
38
38
|
|
39
39
|
# or run them all at once
|
40
|
-
❯ rake rails_rbi:all
|
40
|
+
❯ bundle exec rake rails_rbi:all
|
41
41
|
```
|
42
42
|
|
43
43
|
4. Update hidden-definition files and automatically upgrade each file's typecheck level:
|
@@ -53,11 +53,11 @@ Because we've generated RBI files for routes, models, and helpers, a lot more fi
|
|
53
53
|
|
54
54
|
This Rake task generates RBI files for all models in the Rails application (all descendants of `ActiveRecord::Base`):
|
55
55
|
```sh
|
56
|
-
❯ rake rails_rbi:models
|
56
|
+
❯ bundle exec rake rails_rbi:models
|
57
57
|
```
|
58
58
|
You can also regenerate RBI files for specific models. To accommodate for STI, this will generate rbi for all the subclasses of the models included.
|
59
59
|
```sh
|
60
|
-
❯ rake rails_rbi:models[ModelName,AnotherOne,...]
|
60
|
+
❯ bundle exec rake rails_rbi:models[ModelName,AnotherOne,...]
|
61
61
|
```
|
62
62
|
The generation task currently creates the following signatures:
|
63
63
|
- Column getters & setters
|
@@ -73,6 +73,7 @@ We also add following methods to make type-checking more easily:
|
|
73
73
|
- [`find_n`, `first_n`, `last_n`](https://github.com/chanzuckerberg/sorbet-rails#find-first-and-last)
|
74
74
|
- [`pluck_to_tstruct`](#pluck_to_tstruct-instead-of-pluck)
|
75
75
|
- [`typed_enum`](#enums)
|
76
|
+
- [`Model::RelationType`](#relationtype-alias)
|
76
77
|
|
77
78
|
#### `pluck_to_tstruct` instead of `pluck`
|
78
79
|
|
@@ -159,9 +160,25 @@ Generates only typed enum setter & getter:
|
|
159
160
|
def typed_house=(value); end
|
160
161
|
```
|
161
162
|
|
163
|
+
#### `RelationType` alias
|
164
|
+
|
165
|
+
There are several kinds of relations of a model: `User::ActiveRecord_Relation`, `User::ActiveRecord_AssociationRelation` and `User::ActiveRecord_Associations_CollectionProxy`. Usually the code may need just any relation type. We add a `Model::RelationType` type alias for every model to use it.
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
class User
|
169
|
+
RelationType = T.type_alias do
|
170
|
+
T.any(
|
171
|
+
User::ActiveRecord_Relation,
|
172
|
+
User::ActiveRecord_AssociationRelation,
|
173
|
+
User::ActiveRecord_Associations_CollectionProxy
|
174
|
+
)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
162
179
|
### Controllers
|
163
180
|
```sh
|
164
|
-
❯ rake rails_rbi:custom
|
181
|
+
❯ bundle exec rake rails_rbi:custom
|
165
182
|
```
|
166
183
|
|
167
184
|
`sorbet-rails` adds `TypedParams` to extact typed controller parameters.
|
@@ -191,7 +208,7 @@ Note: [`require_typed` and `fetch_typed`](https://github.com/chanzuckerberg/sorb
|
|
191
208
|
|
192
209
|
This Rake task generates an RBI file defining `_path` and `_url` methods for all named routes in `routes.rb`:
|
193
210
|
```sh
|
194
|
-
❯ rake rails_rbi:routes
|
211
|
+
❯ bundle exec rake rails_rbi:routes
|
195
212
|
```
|
196
213
|
|
197
214
|
### Helpers
|
@@ -199,7 +216,7 @@ This Rake task generates an RBI file defining `_path` and `_url` methods for all
|
|
199
216
|
This Rake task generates a `helpers.rbi` file that includes a basic module definition which includes the `Kernel` module and `ActionView::Helpers`, to allow for some basic Ruby methods to be used in helpers without Sorbet complaining.
|
200
217
|
|
201
218
|
```sh
|
202
|
-
❯ rake rails_rbi:helpers
|
219
|
+
❯ bundle exec rake rails_rbi:helpers
|
203
220
|
```
|
204
221
|
|
205
222
|
If you have additional modules that are included in all your helpers and you want `helpers.rbi` to reflect this, you can configure it:
|
@@ -215,7 +232,7 @@ end
|
|
215
232
|
|
216
233
|
This Rake task generates RBI files for all mailer classes in the Rails application (all descendants of `ActionMailer::Base`)
|
217
234
|
```sh
|
218
|
-
❯ rake rails_rbi:mailers
|
235
|
+
❯ bundle exec rake rails_rbi:mailers
|
219
236
|
```
|
220
237
|
|
221
238
|
Since mailing action methods is based on instance methods defined in a mailer class, the signature of a mailing action method will be dependent on the signature the instance method has
|
@@ -231,16 +248,19 @@ instructions specify that `sorbet-rails` should be placed in the [`:default`
|
|
231
248
|
group](https://bundler.io/v2.0/guides/groups.html) of the `Gemfile`, not a
|
232
249
|
specific environment group (eg. `development` only).
|
233
250
|
|
234
|
-
-
|
235
|
-
- Examples: `User::ActiveRecord_Relation`, `User::ActiveRecord_AssociationRelation`
|
236
|
-
- Model:
|
251
|
+
- Model: The gem provides some helper method to a model to make type-checking easier:
|
237
252
|
- `find_n`, `first_n`, `last_n`
|
238
253
|
- `pluck_to_tstruct`
|
239
254
|
- `typed_enum`
|
240
|
-
|
255
|
+
|
256
|
+
- Model Relation:
|
257
|
+
- Make relation classes public. By default, relation classes like `User::ActiveRecord_Relation`, `User::ActiveRecord_AssociationRelation` are private
|
258
|
+
- Add type alias, eg `Model::RelationType`, to represents any type of relation of a model.
|
259
|
+
|
260
|
+
- Controller: use `TypedParams` to convert controller parameters to a typed structure
|
241
261
|
|
242
262
|
In addition to `require`ing `sorbet-rails`, you must also run
|
243
|
-
`rake rails_rbi:custom`, which will produce the RBI for these runtime features.
|
263
|
+
`bundle exec rake rails_rbi:custom`, which will produce the RBI for these runtime features.
|
244
264
|
|
245
265
|
Discussion:
|
246
266
|
[#211](https://github.com/chanzuckerberg/sorbet-rails/issues/211),
|
@@ -248,7 +268,13 @@ Discussion:
|
|
248
268
|
|
249
269
|
## Tips & Tricks
|
250
270
|
|
251
|
-
###
|
271
|
+
### Look for `# typed: ignore` files
|
272
|
+
|
273
|
+
Because Sorbet's initial setup tries to flag files at whichever typecheck level generates 0 errors, there may be files in your repository that are `# typed: ignore`. This is because sometimes Rails allows very dynamic code that Sorbet does not believe it can typecheck.
|
274
|
+
|
275
|
+
It is worth going through the list of files that is ignored and resolve them (and auto upgrade the types of other files; see [initial setup](#initial-setup) above). Usually this will make many more files able to be typechecked.
|
276
|
+
|
277
|
+
### Overriding generated signatures if needed
|
252
278
|
|
253
279
|
`sorbet-rails` relies on Rails reflection to generate signatures. There are features this gem doesn't support yet such as [serialize](https://github.com/chanzuckerberg/sorbet-rails/issues/49) and [attribute custom types](https://github.com/chanzuckerberg/sorbet-rails/issues/16). The gem also doesn't know the signature of any methods you have overridden. However, it is possible to override the signatures that `sorbet-rails` generates.
|
254
280
|
|
@@ -333,12 +359,6 @@ If you wanted to make these changes using [Codemod](https://github.com/facebook/
|
|
333
359
|
```
|
334
360
|
Note that Codemod's preview may show that the indentation is off, but it works.
|
335
361
|
|
336
|
-
### Look for `# typed: ignore` files
|
337
|
-
|
338
|
-
Because Sorbet's initial setup tries to flag files at whichever typecheck level generates 0 errors, there may be files in your repository that are `# typed: ignore`. This is because sometimes Rails allows very dynamic code that Sorbet does not believe it can typecheck.
|
339
|
-
|
340
|
-
It is worth going through the list of files that is ignored and resolve them (and auto upgrade the types of other files; see [initial setup](#initial-setup) above). Usually this will make many more files able to be typechecked.
|
341
|
-
|
342
362
|
### `unscoped` with a block
|
343
363
|
|
344
364
|
The [`unscoped` method](https://apidock.com/rails/ActiveRecord/Scoping/Default/ClassMethods/unscoped) returns a `Relation` when no block is provided. When a block is provided, `unscoped` calls the block and returns its result, which could be any type.
|
@@ -356,6 +376,31 @@ Model.unscoped.scoping do … end
|
|
356
376
|
|
357
377
|
The [`select` method](https://apidock.com/rails/v4.0.2/ActiveRecord/QueryMethods/select) in Rails has two modes: it can be given a list of symbols, in which case rails will only return the given columns from the database, or it can be given a block, in which case it acts like [`Enumerable.select`](https://ruby-doc.org/core-2.6.4/Enumerable.html) and returns an array. We have chosen to support the first use case. If you want to pass a block to `select`, you can simply call `to_a` before you do. Note that this would be done within the `select` call anyway, so the performance penalty will be minimal.
|
358
378
|
|
379
|
+
### `flatten` an array of relation
|
380
|
+
|
381
|
+
When you call `flatten` on an array of ActiveRecord::Relation, sorbet [doesn't recognize](https://github.com/sorbet/sorbet/issues/2767) that it will flatten the relation and return an array of model. The work around is to call `to_a` on the relation first.
|
382
|
+
|
383
|
+
```ruby
|
384
|
+
# doesn't work
|
385
|
+
arr = [Model.recent, Model.old].flatten # T::Array[Model::ActiveRecord_Relation]
|
386
|
+
|
387
|
+
# work around
|
388
|
+
arr = [Model.recent, Model.old].map(&:to_a).flatten # T::Array[Model]
|
389
|
+
```
|
390
|
+
|
391
|
+
### Avoid `and_call_original` in rspecs
|
392
|
+
|
393
|
+
If you run into the following issue when running rspec, it's likely because you're using `expect(:method_name).and_call_original` to mock a method in RSpec. We've found the double mock doesn't interact well with Sorbet's sig wrapper and caused flaky spec. The spec should be rewritten to expect the outcome of the method instead. (It still works with `expect(:method_name)` and `expect(:method_name).and_return(...)`
|
394
|
+
|
395
|
+
```
|
396
|
+
RuntimeError:
|
397
|
+
`sig` not present for method `:method_name` but you're trying to run it anyways. This should
|
398
|
+
only be executed if you used `alias_method` to grab a handle to a method after `sig`ing it, but
|
399
|
+
that clearly isn't what you are doing. Maybe look to see if an exception was thrown in your `sig`
|
400
|
+
lambda or somehow else your `sig` wasn't actually applied to the method. Contact #dev-productivity
|
401
|
+
if you're really stuck.
|
402
|
+
```
|
403
|
+
|
359
404
|
## Extending Model Generation Task with Custom Plugins
|
360
405
|
|
361
406
|
`sorbet-rails` support a customizable plugin system that you can use to generate additional RBI for each model. This will be useful to generate RBI for methods dynamically added by gems or private concerns. If you write plugins for public gems, please feel free to contribute it to this repo.
|
data/bin/sorbet
ADDED
@@ -18,7 +18,7 @@ class SorbetRails::HelperRbiFormatter
|
|
18
18
|
|
19
19
|
@parlour.root.add_comments([
|
20
20
|
'This is an autogenerated file for Rails helpers.',
|
21
|
-
'Please rerun rake rails_rbi:helpers to regenerate.'
|
21
|
+
'Please rerun bundle exec rake rails_rbi:helpers to regenerate.'
|
22
22
|
])
|
23
23
|
|
24
24
|
@helpers.each do |helper|
|
@@ -17,7 +17,7 @@ class SorbetRails::MailerRbiFormatter
|
|
17
17
|
|
18
18
|
@parlour.root.add_comments([
|
19
19
|
'This is an autogenerated file for Rails helpers.',
|
20
|
-
'Please rerun rake rails_rbi:mailers to regenerate.'
|
20
|
+
'Please rerun bundle exec rake rails_rbi:mailers to regenerate.'
|
21
21
|
])
|
22
22
|
|
23
23
|
@parlour.root.create_class(@mailer_class.name) do |mailer_rbi|
|
@@ -2,6 +2,22 @@
|
|
2
2
|
require ('sorbet-rails/model_plugins/base')
|
3
3
|
class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugins::Base
|
4
4
|
|
5
|
+
class ColumnType < T::Struct
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
const :base_type, T.any(Class, String)
|
9
|
+
const :nilable, T.nilable(T::Boolean)
|
10
|
+
const :array_type, T.nilable(T::Boolean)
|
11
|
+
|
12
|
+
sig { returns(String) }
|
13
|
+
def to_s
|
14
|
+
type = base_type.to_s
|
15
|
+
type = "T.nilable(#{type})" if nilable
|
16
|
+
type = "T::Array[#{type}]" if array_type
|
17
|
+
type
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
5
21
|
sig { override.params(root: Parlour::RbiGenerator::Namespace).void }
|
6
22
|
def generate(root)
|
7
23
|
columns_hash = @model_class.table_exists? ? @model_class.columns_hash : {}
|
@@ -123,7 +139,7 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
|
|
123
139
|
end
|
124
140
|
end
|
125
141
|
|
126
|
-
sig { params(column_def: T.untyped).returns(
|
142
|
+
sig { params(column_def: T.untyped).returns(ColumnType) }
|
127
143
|
def type_for_column_def(column_def)
|
128
144
|
cast_type = ActiveRecord::Base.connection.respond_to?(:lookup_cast_type_from_column) ?
|
129
145
|
ActiveRecord::Base.connection.lookup_cast_type_from_column(column_def) :
|
@@ -135,10 +151,11 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
|
|
135
151
|
time_zone_aware: time_zone_aware_column?(column_def, cast_type),
|
136
152
|
)
|
137
153
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
154
|
+
ColumnType.new(
|
155
|
+
base_type: strict_type,
|
156
|
+
nilable: column_def.null,
|
157
|
+
array_type: column_def.try(:array?),
|
158
|
+
)
|
142
159
|
end
|
143
160
|
|
144
161
|
sig do
|
@@ -197,29 +214,23 @@ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugi
|
|
197
214
|
)
|
198
215
|
end
|
199
216
|
|
200
|
-
sig { params(column_type:
|
217
|
+
sig { params(column_type: ColumnType).returns(String) }
|
201
218
|
def value_type_for_attr_writer(column_type)
|
202
219
|
# it's safe - and convenient - to assign any "time like" object to a time zone
|
203
220
|
# aware attribute because Rails will cast it to a `ActiveSupport::TimeWithZone`
|
204
221
|
# (so rereading the attribute will always return the `TimeWithZone` type)
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
"T.nilable(T.any(#{assignable_numeric_types.join(', ')}))"
|
217
|
-
elsif ["Float", "Integer"].include?(column_type.to_s)
|
218
|
-
"T.any(#{assignable_numeric_types.join(', ')})"
|
219
|
-
elsif column_type == String
|
220
|
-
'T.any(String, Symbol)'
|
221
|
-
else
|
222
|
-
column_type.to_s
|
222
|
+
assignable_time_supertypes = [Date, Time, ActiveSupport::TimeWithZone].map(&:to_s)
|
223
|
+
|
224
|
+
type = column_type.base_type
|
225
|
+
if type.is_a?(Class)
|
226
|
+
if type == ActiveSupport::TimeWithZone
|
227
|
+
type = "T.any(#{assignable_time_supertypes.join(', ')})"
|
228
|
+
elsif type < Numeric || type == ActiveSupport::Duration
|
229
|
+
type = "T.any(Numeric, ActiveSupport::Duration)"
|
230
|
+
elsif type == String
|
231
|
+
type = "T.any(String, Symbol)"
|
232
|
+
end
|
223
233
|
end
|
234
|
+
ColumnType.new(base_type: type, nilable: column_type.nilable, array_type: column_type.array_type).to_s
|
224
235
|
end
|
225
236
|
end
|
@@ -58,7 +58,7 @@ class SorbetRails::ModelRbiFormatter
|
|
58
58
|
|
59
59
|
rbi = <<~MESSAGE
|
60
60
|
# This is an autogenerated file for dynamic methods in #{self.model_class_name}
|
61
|
-
# Please rerun rake rails_rbi:models[#{self.model_class_name}] to regenerate.
|
61
|
+
# Please rerun bundle exec rake rails_rbi:models[#{self.model_class_name}] to regenerate.
|
62
62
|
|
63
63
|
MESSAGE
|
64
64
|
|
@@ -109,6 +109,10 @@ class SorbetRails::ModelRbiFormatter
|
|
109
109
|
)
|
110
110
|
model_rbi.create_extend("T::Sig")
|
111
111
|
model_rbi.create_extend("T::Generic")
|
112
|
+
model_rbi.create_type_alias(
|
113
|
+
self.model_relation_type_class_name,
|
114
|
+
type: self.model_relation_type_alias
|
115
|
+
)
|
112
116
|
end
|
113
117
|
|
114
118
|
sig {
|
@@ -28,6 +28,22 @@ module SorbetRails::ModelUtils
|
|
28
28
|
"#{model_class.name}::ActiveRecord_AssociationRelation"
|
29
29
|
end
|
30
30
|
|
31
|
+
sig { returns(String) }
|
32
|
+
def model_relation_type_alias
|
33
|
+
types = [
|
34
|
+
self.model_relation_class_name,
|
35
|
+
self.model_assoc_proxy_class_name,
|
36
|
+
self.model_assoc_relation_class_name
|
37
|
+
].join(', ')
|
38
|
+
|
39
|
+
"T.any(#{types})"
|
40
|
+
end
|
41
|
+
|
42
|
+
sig { returns(String) }
|
43
|
+
def model_relation_type_class_name
|
44
|
+
'RelationType'
|
45
|
+
end
|
46
|
+
|
31
47
|
sig { params(module_name: String).returns(String) }
|
32
48
|
def model_module_name(module_name)
|
33
49
|
"#{model_class.name}::#{module_name}"
|
@@ -89,4 +105,4 @@ module SorbetRails::ModelUtils
|
|
89
105
|
return_type: self.model_assoc_relation_class_name,
|
90
106
|
)
|
91
107
|
end
|
92
|
-
end
|
108
|
+
end
|
data/lib/sorbet-rails/railtie.rb
CHANGED
@@ -34,6 +34,16 @@ class SorbetRails::Railtie < Rails::Railtie
|
|
34
34
|
child.send(:public_constant, :ActiveRecord_Relation)
|
35
35
|
child.send(:public_constant, :ActiveRecord_AssociationRelation)
|
36
36
|
child.send(:public_constant, :ActiveRecord_Associations_CollectionProxy)
|
37
|
+
|
38
|
+
relation_type = T.type_alias do
|
39
|
+
T.any(
|
40
|
+
child.const_get(:ActiveRecord_Relation),
|
41
|
+
child.const_get(:ActiveRecord_AssociationRelation),
|
42
|
+
child.const_get(:ActiveRecord_Associations_CollectionProxy)
|
43
|
+
)
|
44
|
+
end
|
45
|
+
child.const_set(:RelationType, relation_type)
|
46
|
+
child.send(:public_constant, :RelationType)
|
37
47
|
end
|
38
48
|
end
|
39
49
|
end
|
@@ -21,7 +21,7 @@ class SorbetRails::RoutesRbiFormatter
|
|
21
21
|
def header(routes)
|
22
22
|
@parlour.root.add_comments([
|
23
23
|
'This is an autogenerated file for Rails routes.',
|
24
|
-
'Please run rake rails_rbi:routes to regenerate.'
|
24
|
+
'Please run bundle exec rake rails_rbi:routes to regenerate.'
|
25
25
|
])
|
26
26
|
|
27
27
|
@parlour.root.create_class('ActionController::Base') do |klass|
|
@@ -2,76 +2,171 @@
|
|
2
2
|
|
3
3
|
require('parlour')
|
4
4
|
require('sorbet-runtime')
|
5
|
+
require('method_source')
|
6
|
+
require('parser/current')
|
5
7
|
|
6
8
|
module SorbetRails::SorbetUtils
|
7
9
|
extend T::Sig
|
10
|
+
include Kernel
|
11
|
+
|
12
|
+
class ParsedParamDef < T::Struct
|
13
|
+
const :name, Symbol
|
14
|
+
const :kind, Symbol
|
15
|
+
const :type_str, String
|
16
|
+
prop :default, T.nilable(String), default: nil
|
17
|
+
prop :prefix, T.nilable(String)
|
18
|
+
prop :suffix, T.nilable(String)
|
19
|
+
end
|
8
20
|
|
9
21
|
sig { params(method_def: UnboundMethod).returns(T::Array[Parlour::RbiGenerator::Parameter]) }
|
10
22
|
def self.parameters_from_method_def(method_def)
|
11
23
|
signature = T::Private::Methods.signature_for_method(method_def)
|
24
|
+
method_def = signature.nil? ? method_def : signature.method
|
12
25
|
|
13
26
|
parameters_with_type = signature.nil? ?
|
14
27
|
method_def.parameters.map { |p|
|
15
|
-
|
16
|
-
|
28
|
+
ParsedParamDef.new(
|
29
|
+
name: p.size == 1 ? :_ : p[1], # give param without name default name _
|
30
|
+
kind: p[0], # append untyped as type of each param
|
31
|
+
type_str: 'T.untyped',
|
32
|
+
)
|
17
33
|
} :
|
18
34
|
get_ordered_parameters_with_type(signature)
|
19
35
|
|
20
|
-
|
21
|
-
|
22
|
-
prefix =
|
23
|
-
case param_def
|
36
|
+
# add prefix & suffix
|
37
|
+
parameters_with_type.each do |param_def|
|
38
|
+
param_def.prefix =
|
39
|
+
case param_def.kind
|
24
40
|
when :rest; '*'
|
25
41
|
when :keyrest; '**'
|
26
42
|
when :block; '&'
|
27
43
|
# being comprehensive
|
28
44
|
when :req, :opt; ''
|
29
45
|
when :key, :keyreq; ''
|
30
|
-
else
|
46
|
+
else nil
|
31
47
|
end
|
32
48
|
|
33
|
-
suffix =
|
34
|
-
case param_def
|
49
|
+
param_def.suffix =
|
50
|
+
case param_def.kind
|
35
51
|
when :key, :keyreq; ':'
|
36
|
-
else
|
52
|
+
else nil
|
37
53
|
end
|
54
|
+
end
|
38
55
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
param_type = param_type.gsub('returns(<VOID>)', 'void')
|
44
|
-
end
|
56
|
+
extract_default_value_for_params!(
|
57
|
+
parameters_with_type,
|
58
|
+
method_def,
|
59
|
+
)
|
45
60
|
|
46
|
-
|
61
|
+
parameters_with_type.map do |param_def|
|
62
|
+
::Parlour::RbiGenerator::Parameter.new(
|
63
|
+
"#{param_def.prefix}#{param_def.name}#{param_def.suffix}",
|
64
|
+
type: param_def.type_str,
|
65
|
+
default: param_def.default,
|
66
|
+
)
|
47
67
|
end
|
48
68
|
end
|
49
69
|
|
50
70
|
sig {
|
51
71
|
params(signature: T::Private::Methods::Signature).
|
52
|
-
returns(T::Array[
|
72
|
+
returns(T::Array[ParsedParamDef])
|
53
73
|
}
|
54
74
|
def self.get_ordered_parameters_with_type(signature)
|
55
75
|
# extract original method param from signature
|
56
76
|
# https://github.com/sorbet/sorbet/blob/master/gems/sorbet-runtime/lib/types/private/methods/signature.rb#L5-L8
|
57
|
-
params = []
|
77
|
+
params = T.let([], T::Array[ParsedParamDef])
|
58
78
|
signature.arg_types.each do |arg_type|
|
59
79
|
# could be :opt, but doesn't matter
|
60
|
-
params <<
|
80
|
+
params << ParsedParamDef.new(
|
81
|
+
name: arg_type[0],
|
82
|
+
kind: :req,
|
83
|
+
type_str: arg_type[1].to_s,
|
84
|
+
)
|
61
85
|
end
|
62
|
-
signature.
|
86
|
+
signature.kwarg_types.each do |kwarg_name, kwarg_type|
|
63
87
|
# could be :key, but doesn't matter
|
64
|
-
params <<
|
88
|
+
params << ParsedParamDef.new(
|
89
|
+
name: kwarg_name,
|
90
|
+
kind: :keyreq,
|
91
|
+
type_str: kwarg_type.to_s,
|
92
|
+
)
|
65
93
|
end
|
66
94
|
if signature.has_rest
|
67
|
-
params <<
|
95
|
+
params << ParsedParamDef.new(
|
96
|
+
name: signature.rest_name,
|
97
|
+
kind: :rest,
|
98
|
+
type_str: signature.rest_type.to_s,
|
99
|
+
)
|
68
100
|
end
|
69
101
|
if signature.has_keyrest
|
70
|
-
params <<
|
102
|
+
params << ParsedParamDef.new(
|
103
|
+
name: signature.keyrest_name,
|
104
|
+
kind: :keyrest,
|
105
|
+
type_str: signature.keyrest_type.to_s,
|
106
|
+
)
|
71
107
|
end
|
72
108
|
if !signature.block_name.nil?
|
73
|
-
|
109
|
+
# special case `.void` in a proc
|
110
|
+
# see https://github.com/sorbet/sorbet/blob/master/gems/sorbet-runtime/lib/types/types/proc.rb#L10
|
111
|
+
block_param_type = signature.block_type.to_s
|
112
|
+
block_param_type = block_param_type.gsub('returns(<VOID>)', 'void')
|
113
|
+
params << ParsedParamDef.new(
|
114
|
+
name: signature.block_name,
|
115
|
+
kind: :block,
|
116
|
+
type_str: block_param_type,
|
117
|
+
)
|
74
118
|
end
|
75
119
|
params
|
76
120
|
end
|
121
|
+
|
122
|
+
sig {
|
123
|
+
params(
|
124
|
+
parsed_params: T::Array[ParsedParamDef],
|
125
|
+
method_def: UnboundMethod,
|
126
|
+
).void
|
127
|
+
}
|
128
|
+
def self.extract_default_value_for_params!(parsed_params, method_def)
|
129
|
+
source = method_def.source
|
130
|
+
parsed_ast = Parser::CurrentRuby.parse(source)
|
131
|
+
if parsed_ast.type != :def
|
132
|
+
# could be a method added at runtime? ignore it
|
133
|
+
puts "Warning: unable to parse the source of #{method_def.name}"
|
134
|
+
return
|
135
|
+
end
|
136
|
+
|
137
|
+
args = parsed_ast.children[1]
|
138
|
+
if args.type != :args
|
139
|
+
puts "Warning: unable to parse the source of #{method_def.name}"
|
140
|
+
return
|
141
|
+
end
|
142
|
+
|
143
|
+
parsed_params_map = Hash[parsed_params.map {|p| [p.name, p]}]
|
144
|
+
args.children.each do |arg|
|
145
|
+
arg_name = arg.children[0]
|
146
|
+
default = arg.children[1] ? node_to_s(arg.children[1]) : nil
|
147
|
+
|
148
|
+
next if arg_name.blank?
|
149
|
+
|
150
|
+
param_def = parsed_params_map[arg_name]
|
151
|
+
|
152
|
+
raise UnexpectedParam.new(
|
153
|
+
"Unexpected param #{arg_name} when parsing #{method_def.name}"
|
154
|
+
) unless param_def.present?
|
155
|
+
|
156
|
+
param_def.default = default
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Given an AST node, returns the source code from which it was constructed.
|
161
|
+
# If the given AST node is nil, this returns nil.
|
162
|
+
# Taken from https://github.com/AaronC81/parlour/blob/master/lib/parlour/type_parser.rb#L506
|
163
|
+
sig { params(node: T.nilable(Parser::AST::Node)).returns(T.nilable(String)) }
|
164
|
+
def self.node_to_s(node)
|
165
|
+
return nil unless node
|
166
|
+
|
167
|
+
exp = node.loc.expression
|
168
|
+
exp.source_buffer.source[exp.begin_pos...exp.end_pos]
|
169
|
+
end
|
170
|
+
|
171
|
+
class UnexpectedParam < StandardError; end
|
77
172
|
end
|