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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -24
  3. data/bin/sorbet +6 -0
  4. data/lib/sorbet-rails/helper_rbi_formatter.rb +1 -1
  5. data/lib/sorbet-rails/mailer_rbi_formatter.rb +1 -1
  6. data/lib/sorbet-rails/model_plugins/active_record_attribute.rb +35 -24
  7. data/lib/sorbet-rails/model_rbi_formatter.rb +5 -1
  8. data/lib/sorbet-rails/model_utils.rb +17 -1
  9. data/lib/sorbet-rails/railtie.rb +10 -0
  10. data/lib/sorbet-rails/routes_rbi_formatter.rb +1 -1
  11. data/lib/sorbet-rails/sorbet_utils.rb +120 -25
  12. data/sorbet-rails.gemspec +5 -3
  13. data/spec/generators/rails-template.rb +16 -0
  14. data/spec/model_rbi_formatter_spec.rb +7 -0
  15. data/spec/sorbet_utils_spec.rb +25 -13
  16. data/spec/support/v5.0/Gemfile.lock +13 -11
  17. data/spec/support/v5.0/app/mailers/hogwarts_acceptance_mailer.rb +17 -1
  18. data/spec/support/v5.1/Gemfile.lock +13 -11
  19. data/spec/support/v5.1/app/mailers/hogwarts_acceptance_mailer.rb +17 -1
  20. data/spec/support/v5.2/Gemfile.lock +13 -11
  21. data/spec/support/v5.2/app/mailers/hogwarts_acceptance_mailer.rb +17 -1
  22. data/spec/support/v6.0/Gemfile.lock +14 -12
  23. data/spec/support/v6.0/app/mailers/application_mailer.rb +1 -1
  24. data/spec/support/v6.0/app/mailers/hogwarts_acceptance_mailer.rb +17 -1
  25. data/spec/test_data/v5.0/expected_application_mailer.rbi +1 -1
  26. data/spec/test_data/v5.0/expected_daily_prophet_mailer.rbi +1 -1
  27. data/spec/test_data/v5.0/expected_helpers.rbi +1 -1
  28. data/spec/test_data/v5.0/expected_helpers_with_application_and_devise_helpers.rbi +1 -1
  29. data/spec/test_data/v5.0/expected_hogwarts_acceptance_mailer.rbi +11 -1
  30. data/spec/test_data/v5.0/expected_internal_metadata.rbi +5 -4
  31. data/spec/test_data/v5.0/expected_potion.rbi +2 -1
  32. data/spec/test_data/v5.0/expected_robe.rbi +4 -3
  33. data/spec/test_data/v5.0/expected_routes.rbi +1 -1
  34. data/spec/test_data/v5.0/expected_schema_migration.rbi +2 -1
  35. data/spec/test_data/v5.0/expected_school.rbi +4 -3
  36. data/spec/test_data/v5.0/expected_spell_book.rbi +5 -4
  37. data/spec/test_data/v5.0/expected_squib.rbi +10 -9
  38. data/spec/test_data/v5.0/expected_wand.rbi +11 -10
  39. data/spec/test_data/v5.0/expected_wizard.rbi +10 -9
  40. data/spec/test_data/v5.0/expected_wizard_wo_spellbook.rbi +10 -9
  41. data/spec/test_data/v5.1/expected_application_mailer.rbi +1 -1
  42. data/spec/test_data/v5.1/expected_daily_prophet_mailer.rbi +1 -1
  43. data/spec/test_data/v5.1/expected_helpers.rbi +1 -1
  44. data/spec/test_data/v5.1/expected_helpers_with_application_and_devise_helpers.rbi +1 -1
  45. data/spec/test_data/v5.1/expected_hogwarts_acceptance_mailer.rbi +11 -1
  46. data/spec/test_data/v5.1/expected_internal_metadata.rbi +5 -4
  47. data/spec/test_data/v5.1/expected_potion.rbi +2 -1
  48. data/spec/test_data/v5.1/expected_robe.rbi +4 -3
  49. data/spec/test_data/v5.1/expected_routes.rbi +1 -1
  50. data/spec/test_data/v5.1/expected_schema_migration.rbi +2 -1
  51. data/spec/test_data/v5.1/expected_school.rbi +4 -3
  52. data/spec/test_data/v5.1/expected_spell_book.rbi +5 -4
  53. data/spec/test_data/v5.1/expected_squib.rbi +10 -9
  54. data/spec/test_data/v5.1/expected_wand.rbi +11 -10
  55. data/spec/test_data/v5.1/expected_wizard.rbi +10 -9
  56. data/spec/test_data/v5.1/expected_wizard_wo_spellbook.rbi +10 -9
  57. data/spec/test_data/v5.2/expected_application_mailer.rbi +1 -1
  58. data/spec/test_data/v5.2/expected_attachment.rbi +2 -1
  59. data/spec/test_data/v5.2/expected_blob.rbi +2 -1
  60. data/spec/test_data/v5.2/expected_daily_prophet_mailer.rbi +1 -1
  61. data/spec/test_data/v5.2/expected_helpers.rbi +1 -1
  62. data/spec/test_data/v5.2/expected_helpers_with_application_and_devise_helpers.rbi +1 -1
  63. data/spec/test_data/v5.2/expected_hogwarts_acceptance_mailer.rbi +11 -1
  64. data/spec/test_data/v5.2/expected_internal_metadata.rbi +5 -4
  65. data/spec/test_data/v5.2/expected_potion.rbi +2 -1
  66. data/spec/test_data/v5.2/expected_robe.rbi +4 -3
  67. data/spec/test_data/v5.2/expected_routes.rbi +1 -1
  68. data/spec/test_data/v5.2/expected_schema_migration.rbi +2 -1
  69. data/spec/test_data/v5.2/expected_school.rbi +4 -3
  70. data/spec/test_data/v5.2/expected_spell_book.rbi +5 -4
  71. data/spec/test_data/v5.2/expected_squib.rbi +10 -9
  72. data/spec/test_data/v5.2/expected_wand.rbi +11 -10
  73. data/spec/test_data/v5.2/expected_wizard.rbi +10 -9
  74. data/spec/test_data/v5.2/expected_wizard_wo_spellbook.rbi +10 -9
  75. data/spec/test_data/v6.0/expected_application_mailer.rbi +1 -1
  76. data/spec/test_data/v6.0/expected_attachment.rbi +2 -1
  77. data/spec/test_data/v6.0/expected_blob.rbi +2 -1
  78. data/spec/test_data/v6.0/expected_daily_prophet_mailer.rbi +1 -1
  79. data/spec/test_data/v6.0/expected_helpers.rbi +1 -1
  80. data/spec/test_data/v6.0/expected_helpers_with_application_and_devise_helpers.rbi +1 -1
  81. data/spec/test_data/v6.0/expected_hogwarts_acceptance_mailer.rbi +11 -1
  82. data/spec/test_data/v6.0/expected_internal_metadata.rbi +5 -4
  83. data/spec/test_data/v6.0/expected_potion.rbi +2 -1
  84. data/spec/test_data/v6.0/expected_robe.rbi +4 -3
  85. data/spec/test_data/v6.0/expected_routes.rbi +1 -1
  86. data/spec/test_data/v6.0/expected_schema_migration.rbi +2 -1
  87. data/spec/test_data/v6.0/expected_school.rbi +4 -3
  88. data/spec/test_data/v6.0/expected_spell_book.rbi +5 -4
  89. data/spec/test_data/v6.0/expected_squib.rbi +10 -9
  90. data/spec/test_data/v6.0/expected_wand.rbi +11 -10
  91. data/spec/test_data/v6.0/expected_wizard.rbi +10 -9
  92. data/spec/test_data/v6.0/expected_wizard_wo_spellbook.rbi +10 -9
  93. metadata +33 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58c588bf2eb9eb09d2c4322d54436840452bb5a026e6ca4132df8e651aa26968
4
- data.tar.gz: 11b54b70f6c86f5d97152fc5a7c9f0b48b021b8be03472fcb83014557353dc4c
3
+ metadata.gz: b2400ac696f93d779e930dac9f4d6e5781f1ce976e1ee98599a222a78c658588
4
+ data.tar.gz: 25d8ef2f00b2eb87afe05144323927df66f328ffe2bcff00c578396824d5053e
5
5
  SHA512:
6
- metadata.gz: 6b88d51c0fae405a9249974746b250ad6ad2ce8ca09a5620c385105bffd0617dd2fbf5584a07a35abb35104c8242a1954156be9f98ddc456554803ad3b2a92ac
7
- data.tar.gz: 98c2836dcbbb20798d0b57579eb7c3a07175bf8e5c58ed062cb652d2c7826bf62c1586cc4a9a5337702e88151862c8caf4cda81a21765f583a3c85caefc3d659
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
- - Relation class: Making the relations available at runtime (they are normally private constants, the gem makes them public)
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
- - Controller: using `TypedParams`
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
- ### Overriding generated signatures
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.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ `bundle show sorbet-static`/libexec/sorbet $@
6
+
@@ -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(T.any(String, Class)) }
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
- if column_def.respond_to?(:array?) && column_def.array?
139
- strict_type = "T::Array[#{strict_type}]"
140
- end
141
- column_def.null ? "T.nilable(#{strict_type})" : strict_type
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: Object).returns(String) }
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
- assignable_time_types = [DateTime, Date, Time, ActiveSupport::TimeWithZone].map(&:to_s)
206
-
207
- # same thing applies with the many types that respond to `#to_i` or `#to_f`
208
- assignable_numeric_types = [Integer, Float, ActiveSupport::Duration]
209
-
210
- # TODO: this could be a lot tidier
211
- if column_type == ActiveSupport::TimeWithZone
212
- "T.any(#{assignable_time_types.join(', ')})"
213
- elsif column_type == "T.nilable(ActiveSupport::TimeWithZone)"
214
- "T.nilable(T.any(#{assignable_time_types.join(', ')}))"
215
- elsif ["T.nilable(Float)", "T.nilable(Integer)"].include?(column_type)
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
@@ -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
- p1 = p.size == 1 ? p + ['_'] : p # give param without name default name _
16
- p1 + ['T.untyped'] # append untyped as type of each param
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
- parameters_with_type.map do |param_def|
21
- param_name = param_def[1]
22
- prefix =
23
- case param_def[0]
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[0]
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
- param_type = param_def[2].to_s
40
- if param_def[0] == :block
41
- # special case `.void` in a proc
42
- # see https://github.com/sorbet/sorbet/blob/master/gems/sorbet-runtime/lib/types/types/proc.rb#L10
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
- ::Parlour::RbiGenerator::Parameter.new("#{prefix}#{param_name}#{suffix}", type: param_type)
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[[Symbol, Symbol, T::Types::Base]])
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 << [:req, arg_type[0], arg_type[1]]
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.req_kwarg_names.each do |kwarg_name|
86
+ signature.kwarg_types.each do |kwarg_name, kwarg_type|
63
87
  # could be :key, but doesn't matter
64
- params << [:keyreq, kwarg_name, signature.kwarg_types[kwarg_name]]
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 << [:rest, signature.rest_name, signature.rest_type]
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 << [:keyrest, signature.keyrest_name, signature.keyrest_type]
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
- params << [:block, signature.block_name, signature.block_type]
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