sorbet-rails 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (171) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +13 -1
  3. data/.travis.yml +2 -2
  4. data/Gemfile +1 -1
  5. data/README.md +79 -3
  6. data/lib/sorbet-rails.rb +5 -1
  7. data/lib/sorbet-rails/activerecord.rbi +27 -0
  8. data/lib/sorbet-rails/custom_finder_methods.rb +11 -0
  9. data/lib/sorbet-rails/helper_rbi_formatter.rb +33 -0
  10. data/lib/sorbet-rails/model_plugins/active_record_assoc.rb +91 -0
  11. data/lib/sorbet-rails/model_plugins/active_record_attribute.rb +111 -0
  12. data/lib/sorbet-rails/model_plugins/active_record_enum.rb +43 -0
  13. data/lib/sorbet-rails/model_plugins/active_record_finder_methods.rb +80 -0
  14. data/lib/sorbet-rails/model_plugins/active_record_named_scope.rb +28 -0
  15. data/lib/sorbet-rails/model_plugins/active_record_querying.rb +42 -0
  16. data/lib/sorbet-rails/model_plugins/active_relation_where_not.rb +28 -0
  17. data/lib/sorbet-rails/model_plugins/base.rb +33 -0
  18. data/lib/sorbet-rails/model_plugins/custom_finder_methods.rb +54 -0
  19. data/lib/sorbet-rails/model_plugins/enumerable_collections.rb +49 -0
  20. data/lib/sorbet-rails/model_plugins/plugins.rb +45 -0
  21. data/lib/sorbet-rails/model_rbi_formatter.rb +108 -362
  22. data/lib/sorbet-rails/model_utils.rb +45 -0
  23. data/lib/sorbet-rails/railtie.rb +1 -0
  24. data/lib/sorbet-rails/routes_rbi_formatter.rb +12 -3
  25. data/lib/sorbet-rails/tasks/rails_rbi.rake +36 -15
  26. data/lib/sorbet-rails/utils.rb +4 -0
  27. data/sorbet-rails.gemspec +4 -2
  28. data/spec/helper_rbi_formatter_spec.rb +13 -0
  29. data/spec/model_plugins_spec.rb +31 -0
  30. data/spec/model_rbi_formatter_spec.rb +5 -5
  31. data/spec/rake_rails_rbi_helpers_spec.rb +14 -0
  32. data/spec/routes_rbi_formatter_spec.rb +3 -3
  33. data/spec/sorbet_spec.rb +12 -16
  34. data/spec/support/rails_shared/app/controllers/application_controller.rb +1 -1
  35. data/spec/support/rails_shared/app/helpers/bar_helper.rb +2 -0
  36. data/spec/support/rails_shared/app/helpers/baz_helper.rb +2 -0
  37. data/spec/support/rails_shared/app/helpers/foo_helper.rb +2 -0
  38. data/spec/support/rails_shared/app/models/concerns/mythical.rb +10 -0
  39. data/spec/support/rails_shared/app/models/wand.rb +1 -0
  40. data/spec/support/rails_shared/config/initializers/sorbet_rails.rb +3 -0
  41. data/spec/support/rails_shared/lib/mythical_rbi_plugin.rb +16 -0
  42. data/spec/support/rails_shared/sorbet_test_cases.rb +5 -2
  43. data/spec/support/rails_shared/typed-override.yaml +2 -0
  44. data/spec/support/rails_symlinks/app/helpers +1 -0
  45. data/spec/support/rails_symlinks/config/initializers/sorbet_rails.rb +1 -0
  46. data/spec/support/rails_symlinks/lib/mythical_rbi_plugin.rb +1 -0
  47. data/spec/support/rails_symlinks/typed-override.yaml +1 -0
  48. data/spec/support/v4.2/Gemfile.lock +5 -0
  49. data/spec/support/v4.2/app/helpers +1 -0
  50. data/spec/support/v4.2/config/initializers/sorbet_rails.rb +1 -0
  51. data/spec/support/v4.2/config/routes.rb +0 -1
  52. data/spec/support/v4.2/lib/mythical_rbi_plugin.rb +1 -0
  53. data/spec/support/v4.2/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
  54. data/spec/support/v4.2/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
  55. data/spec/support/v4.2/typed-override.yaml +1 -0
  56. data/spec/support/v5.0/Gemfile.lock +5 -0
  57. data/spec/support/v5.0/app/helpers +1 -0
  58. data/spec/support/v5.0/config/initializers/sorbet_rails.rb +1 -0
  59. data/spec/support/v5.0/config/routes.rb +0 -1
  60. data/spec/support/v5.0/lib/mythical_rbi_plugin.rb +1 -0
  61. data/spec/support/v5.0/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
  62. data/spec/support/v5.0/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
  63. data/spec/support/v5.0/typed-override.yaml +1 -0
  64. data/spec/support/v5.1/Gemfile.lock +5 -0
  65. data/spec/support/v5.1/app/helpers +1 -0
  66. data/spec/support/v5.1/config/initializers/sorbet_rails.rb +1 -0
  67. data/spec/support/v5.1/lib/mythical_rbi_plugin.rb +1 -0
  68. data/spec/support/v5.1/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
  69. data/spec/support/v5.1/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
  70. data/spec/support/v5.1/typed-override.yaml +1 -0
  71. data/spec/support/v5.2-no-sorbet/Gemfile +0 -2
  72. data/spec/support/v5.2-no-sorbet/Gemfile.lock +7 -5
  73. data/spec/support/v5.2-no-sorbet/app/helpers +1 -0
  74. data/spec/support/v5.2-no-sorbet/config/initializers/sorbet_rails.rb +1 -0
  75. data/spec/support/v5.2-no-sorbet/lib/mythical_rbi_plugin.rb +1 -0
  76. data/spec/support/v5.2-no-sorbet/sorbet_test_cases.rb +1 -0
  77. data/spec/support/v5.2-no-sorbet/typed-override.yaml +1 -0
  78. data/spec/support/v5.2/Gemfile +0 -2
  79. data/spec/support/v5.2/Gemfile.lock +7 -5
  80. data/spec/support/v5.2/app/helpers +1 -0
  81. data/spec/support/v5.2/config/initializers/sorbet_rails.rb +1 -0
  82. data/spec/support/v5.2/lib/mythical_rbi_plugin.rb +1 -0
  83. data/spec/support/v5.2/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
  84. data/spec/support/v5.2/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
  85. data/spec/support/v5.2/typed-override.yaml +1 -0
  86. data/spec/support/v6.0/Gemfile +1 -3
  87. data/spec/support/v6.0/Gemfile.lock +61 -59
  88. data/spec/support/v6.0/app/helpers +1 -0
  89. data/spec/support/v6.0/config/initializers/sorbet_rails.rb +1 -0
  90. data/spec/support/v6.0/config/routes.rb +0 -1
  91. data/spec/support/v6.0/lib/mythical_rbi_plugin.rb +1 -0
  92. data/spec/support/v6.0/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
  93. data/spec/support/v6.0/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
  94. data/spec/support/v6.0/typed-override.yaml +1 -0
  95. data/spec/test_data/v4.2/expected_helpers.rbi +17 -0
  96. data/spec/test_data/v4.2/expected_potion.rbi +259 -28
  97. data/spec/test_data/v4.2/expected_spell_book.rbi +278 -37
  98. data/spec/test_data/v4.2/expected_srb_tc_output.txt +1 -99
  99. data/spec/test_data/v4.2/expected_wand.rbi +345 -96
  100. data/spec/test_data/v4.2/expected_wizard.rbi +322 -76
  101. data/spec/test_data/v4.2/expected_wizard_wo_spellbook.rbi +322 -76
  102. data/spec/test_data/v5.0/expected_helpers.rbi +17 -0
  103. data/spec/test_data/v5.0/expected_internal_metadata.rbi +273 -37
  104. data/spec/test_data/v5.0/expected_potion.rbi +259 -28
  105. data/spec/test_data/v5.0/expected_schema_migration.rbi +264 -28
  106. data/spec/test_data/v5.0/expected_spell_book.rbi +278 -37
  107. data/spec/test_data/v5.0/expected_srb_tc_output.txt +1 -81
  108. data/spec/test_data/v5.0/expected_wand.rbi +341 -92
  109. data/spec/test_data/v5.0/expected_wizard.rbi +318 -72
  110. data/spec/test_data/v5.0/expected_wizard_wo_spellbook.rbi +318 -72
  111. data/spec/test_data/v5.1/expected_helpers.rbi +17 -0
  112. data/spec/test_data/v5.1/expected_internal_metadata.rbi +273 -37
  113. data/spec/test_data/v5.1/expected_potion.rbi +259 -28
  114. data/spec/test_data/v5.1/expected_schema_migration.rbi +264 -28
  115. data/spec/test_data/v5.1/expected_spell_book.rbi +278 -37
  116. data/spec/test_data/v5.1/expected_srb_tc_output.txt +1 -81
  117. data/spec/test_data/v5.1/expected_wand.rbi +341 -92
  118. data/spec/test_data/v5.1/expected_wizard.rbi +318 -72
  119. data/spec/test_data/v5.1/expected_wizard_wo_spellbook.rbi +318 -72
  120. data/spec/test_data/v5.2-no-sorbet/expected_attachment.rbi +265 -29
  121. data/spec/test_data/v5.2-no-sorbet/expected_blob.rbi +271 -35
  122. data/spec/test_data/v5.2-no-sorbet/expected_helpers.rbi +17 -0
  123. data/spec/test_data/v5.2-no-sorbet/expected_internal_metadata.rbi +273 -37
  124. data/spec/test_data/v5.2-no-sorbet/expected_potion.rbi +259 -28
  125. data/spec/test_data/v5.2-no-sorbet/expected_schema_migration.rbi +264 -28
  126. data/spec/test_data/v5.2-no-sorbet/expected_spell_book.rbi +278 -37
  127. data/spec/test_data/v5.2-no-sorbet/expected_srb_tc_output.txt +1 -81
  128. data/spec/test_data/v5.2-no-sorbet/expected_wand.rbi +351 -102
  129. data/spec/test_data/v5.2-no-sorbet/expected_wizard.rbi +318 -72
  130. data/spec/test_data/v5.2-no-sorbet/expected_wizard_wo_spellbook.rbi +318 -72
  131. data/spec/test_data/v5.2/expected_attachment.rbi +265 -29
  132. data/spec/test_data/v5.2/expected_blob.rbi +271 -35
  133. data/spec/test_data/v5.2/expected_helpers.rbi +17 -0
  134. data/spec/test_data/v5.2/expected_internal_metadata.rbi +273 -37
  135. data/spec/test_data/v5.2/expected_potion.rbi +259 -28
  136. data/spec/test_data/v5.2/expected_schema_migration.rbi +264 -28
  137. data/spec/test_data/v5.2/expected_spell_book.rbi +278 -37
  138. data/spec/test_data/v5.2/expected_srb_tc_output.txt +1 -81
  139. data/spec/test_data/v5.2/expected_wand.rbi +351 -102
  140. data/spec/test_data/v5.2/expected_wizard.rbi +318 -72
  141. data/spec/test_data/v5.2/expected_wizard_wo_spellbook.rbi +318 -72
  142. data/spec/test_data/v6.0/expected_attachment.rbi +265 -29
  143. data/spec/test_data/v6.0/expected_blob.rbi +271 -35
  144. data/spec/test_data/v6.0/expected_helpers.rbi +17 -0
  145. data/spec/test_data/v6.0/expected_internal_metadata.rbi +273 -37
  146. data/spec/test_data/v6.0/expected_potion.rbi +259 -28
  147. data/spec/test_data/v6.0/expected_schema_migration.rbi +264 -28
  148. data/spec/test_data/v6.0/expected_spell_book.rbi +278 -37
  149. data/spec/test_data/v6.0/expected_srb_tc_output.txt +1 -81
  150. data/spec/test_data/v6.0/expected_wand.rbi +351 -102
  151. data/spec/test_data/v6.0/expected_wizard.rbi +318 -72
  152. data/spec/test_data/v6.0/expected_wizard_wo_spellbook.rbi +318 -72
  153. metadata +120 -37
  154. data/lib/sorbet-rails/rbi/activerecord.rbi +0 -207
  155. data/spec/support/v4.2/app/helpers/application_helper.rb +0 -3
  156. data/spec/support/v4.2/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
  157. data/spec/support/v4.2/sorbet/rbi/hidden-definitions/errors.txt +0 -11998
  158. data/spec/support/v4.2/sorbet/rbi/hidden-definitions/hidden.rbi +0 -27774
  159. data/spec/support/v5.0/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
  160. data/spec/support/v5.0/sorbet/rbi/hidden-definitions/errors.txt +0 -10523
  161. data/spec/support/v5.0/sorbet/rbi/hidden-definitions/hidden.rbi +0 -24969
  162. data/spec/support/v5.1/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
  163. data/spec/support/v5.1/sorbet/rbi/hidden-definitions/errors.txt +0 -10226
  164. data/spec/support/v5.1/sorbet/rbi/hidden-definitions/hidden.rbi +0 -24635
  165. data/spec/support/v5.2/.ruby-version +0 -1
  166. data/spec/support/v5.2/sorbet/rbi/gems/sorbet-runtime.rbi +0 -644
  167. data/spec/support/v5.2/sorbet/rbi/hidden-definitions/errors.txt +0 -10046
  168. data/spec/support/v5.2/sorbet/rbi/hidden-definitions/hidden.rbi +0 -24424
  169. data/spec/support/v6.0/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
  170. data/spec/support/v6.0/sorbet/rbi/hidden-definitions/errors.txt +0 -12074
  171. data/spec/support/v6.0/sorbet/rbi/hidden-definitions/hidden.rbi +0 -28231
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 319136a77de53915ea8e2999617f3942dc9913dd
4
- data.tar.gz: e3cd87219252f35701df8647edcf752244bf0534
2
+ SHA256:
3
+ metadata.gz: cdf5c9e5cc4d021af0e9206c53d9682288bb204f85179bffc2f041b2b783f8c8
4
+ data.tar.gz: 72de625982952376be79c47926266d7d25767b3b015b463c9302e75421643278
5
5
  SHA512:
6
- metadata.gz: fbd53d9c25ae7d561d1a817f07c042fc7f014a3765726dfdefc7d2ab528a0321463d3f2511bbcdf1dcc4a9a6d10b973eaf4f0497f2b59b3edd6fe947f0ca178e
7
- data.tar.gz: b5e9cdb6b32400026629532bdb75da9aee0518432dac9608ac68b7587ec8d04c602a0aa2983ff9ff905f8220fe2a89ad4ea5b479d302cfda2dd5349dbf74b892
6
+ metadata.gz: 3e59b36bf6be3ee5f8eb15db96a0b51a624557a4c1debebfeffe49b764b9670763e4437b6eba08137995126c07d150898f0cdceaa486b103f265929917a514ad
7
+ data.tar.gz: 5ae9763e3c5d1bafbfe6d847077abd8af4a7a80cabaa626ed723a66a036f8cc383dd26e242bffb055adedf40f5ee000d40394cadb405dd23c5dd1449748a722c
data/.gitignore CHANGED
@@ -79,8 +79,20 @@ build-iPhoneSimulator/
79
79
  # Ignore sorbet-rails generated files
80
80
  /spec/support/**/sorbet/rails-rbi/*
81
81
  /spec/support/**/sorbet/rails-rbi/**/*
82
+ # Ignore hidden-definition.rbi because we will rerun it
83
+ /spec/support/**/sorbet/rbi/hidden-definitions/*
84
+ /spec/support/**/sorbet/rbi/hidden-definitions/**/*
82
85
 
83
86
  # Ignore sorbet files in v5.2-no-sorbet
84
- /spec/support/v5.2-no-sorbet/sorbet_test_cases.rb
85
87
  /spec/support/v5.2-no-sorbet/sorbet/*
86
88
  /spec/support/v5.2-no-sorbet/sorbet/**/*
89
+
90
+ # editor swap files
91
+ *.swp
92
+ *.swo
93
+ *.swn
94
+ *.swm
95
+ *.swl
96
+ *.swk
97
+
98
+ lib/sorbet/rbi/hidden-definitions/errors.txt
data/.travis.yml CHANGED
@@ -17,7 +17,7 @@ rvm:
17
17
  matrix:
18
18
  include:
19
19
  - rvm: 2.5
20
- env: RAILS_VERSION=5.2 SORBET_VERSION=0.4.4386
20
+ env: RAILS_VERSION=5.2 SORBET_VERSION=0.4.4602
21
21
  - rvm: 2.5
22
22
  env: TEST_SRB_INIT=true
23
23
  exclude:
@@ -27,12 +27,12 @@ matrix:
27
27
  env: RAILS_VERSION=6.0
28
28
  allow_failures:
29
29
  - rvm: ruby-head
30
- - env: RAILS_VERSION=5.2
31
30
  before_install:
32
31
  - gem install bundler -v 2.0.1 --no-doc
33
32
  - gem install bundler -v 1.17.3 --no-doc
34
33
  install: "./spec/bin/install.sh"
35
34
  script:
35
+ - (cd lib && bundle exec srb tc)
36
36
  - "./spec/bin/run_spec.sh"
37
37
  deploy:
38
38
  provider: rubygems
data/Gemfile CHANGED
@@ -48,4 +48,4 @@ else
48
48
  # prefer to test against latest version because sorbet is updated frequently
49
49
  gem 'sorbet'
50
50
  gem 'sorbet-runtime'
51
- end
51
+ end
data/README.md CHANGED
@@ -25,17 +25,22 @@ gem 'sorbet-rails'
25
25
  ❯ bundle install
26
26
  ```
27
27
 
28
- 3. Generate RBI files for your routes and models:
28
+ 3. Generate RBI files for your routes, models, and helpers:
29
29
  ```sh
30
30
  ❯ rake rails_rbi:routes
31
31
  ❯ rake rails_rbi:models
32
+ ❯ rake rails_rbi:helpers
33
+
34
+ # or run them all at once
35
+ ❯ rake rails_rbi:all
32
36
  ```
33
37
 
34
- 4. Automatically upgrade each file's typecheck level:
38
+ 4. Update hidden-definition files and automatically upgrade each file's typecheck level:
35
39
  ```sh
40
+ ❯ srb rbi hidden-definitions
36
41
  ❯ srb rbi suggest-typed
37
42
  ```
38
- Because we've generated RBI files for routes and models, a lot more files should be typecheckable now.
43
+ Because we've generated RBI files for routes, models, and helpers, a lot more files should be typecheckable now. Many methods in `hidden.rbi` may be removed because they are now typed.
39
44
 
40
45
  ## RBI Files
41
46
 
@@ -66,6 +71,16 @@ The generation task currently creates the following signatures:
66
71
  - Named scopes
67
72
  - Model relation class
68
73
 
74
+ It is possible to add custom RBI generation logic for your custom module or gems via the plugin system. Check out the [plugins section](#extending-model-generation-task-with-custom-plugins) below if you are interested.
75
+
76
+ ### Helpers
77
+
78
+ This Rake task generates a `helpers.rbi` file that includes a basic module definition which includes the `Kernel` module, to allow for some basic Ruby methods to be used in helpers without Sorbet complaining.
79
+
80
+ ```sh
81
+ ❯ rake rails_rbi:helpers
82
+ ```
83
+
69
84
  ## Tips & Tricks
70
85
 
71
86
  ### Overriding generated signatures
@@ -154,6 +169,67 @@ with:
154
169
  Model.unscoped.scoping do … end
155
170
  ```
156
171
 
172
+ ## Extending Model Generation Task with Custom Plugins
173
+
174
+ `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.
175
+
176
+ ### Defining a Custom `ModelPlugin`
177
+
178
+ A custom plugin should be a subclass of `SorbetRails::ModelPlugins::Base`. Each plugin would implement a `generate(root)` method that generate additional rbi for the model.
179
+
180
+ At a high level, here is the structure of a plugin:
181
+ ```ruby
182
+ # -- lib/my_custom_plugin.rb
183
+ class MyCustomPlugin < SorbetRails::ModelPlugins::Base
184
+ sig { implementation.params(root: Parlour::RbiGenerator::Namespace).void }
185
+ def generate(root)
186
+ # TODO: implement the generation logic
187
+ # You can use @model_class and @available_classes here
188
+ end
189
+ end
190
+ ```
191
+ During the generation phase, the system will create a new instance of the plugin, with the `model_class` to be generated and a set of all `available_classes` (available models) detected in the Rails App.
192
+
193
+ We use [Parlour gem](https://github.com/AaronC81/parlour) to generate the RBI for a model. Please check out [Parlour wiki](https://github.com/AaronC81/parlour/wiki/Using-and-creating-RBI-objects) for guide to add RBI, eg create a new module, class, or method in the generated file.
194
+
195
+ At a high level, you'd usually want to create a model-scoped module for your methods, and include or extend it in the base model class. The generation logic usually looks like this:
196
+ ```ruby
197
+ def generate(root)
198
+ # Make sure this is only run for relevant class
199
+ return unless @model_class.include?(CustomModule)
200
+
201
+ custom_module_name = self.model_module_name("CustomModule")
202
+ custom_module_rbi = root.create_module(custom_module_name)
203
+
204
+ # here we re-create the model class!
205
+ model_class_rbi = root.create_class(self.model_class_name)
206
+ model_class_rbi.create_extend(custom_module_name)
207
+
208
+ # then create custom methods, constants, etc. for this module.
209
+ custom_module_rbi.create_method(...)
210
+
211
+ # this is allowed but not recommended, because it limit the ability to override the method.
212
+ model_class_rbi.create_method(...)
213
+ end
214
+ ```
215
+ Notice that we re-create `model_class_rbi` here. Parlour's [ConflictResolver](https://github.com/AaronC81/parlour/wiki/Internals#overall-flow) will merge the classes or modules with the same name together to generate 1 beautiful RBI file. It'll also flag and skip if any method is created multiple times with conflict signatures. Check-out useful predefined module names & helper methods in [model_utils](https://github.com/chanzuckerberg/sorbet-rails/blob/master/lib/sorbet-rails/model_utils.rb).
216
+
217
+ It is also allowed to put methods into a model class directly. However, it is not recommended because it'll be harder to override the method. `sorbet` will enforce that the overriding method match the signature generated. It also makes the generated RBI file less modularized.
218
+
219
+ However, sometimes this is required to make `sorbet` recognize the signature. This is the case for class methods added by `ActiveRecord::Concerns`. Because `ActiveSupport::Concern` class methods will be inserted to the class directly, you need to also put the sig in the model class rbi directly.
220
+
221
+ It is also recommended to check if the generated methods are detected by `sorbet` as a gem method (in `sorbet/rbi/gem/gem_name.rbi`) or hidden methods (in `sorbet/rbi/hidden-definitions/hidden.rbi`). If so, you may need to re-run `srb rbi hidden-definition` or put method in the model class directly.
222
+
223
+ Check out the [plugins](https://github.com/chanzuckerberg/sorbet-rails/tree/master/lib/sorbet-rails/model_plugins) written for `sorbet-rails`'s own model RBI generation logic for examples.
224
+
225
+ ### Registering new plugins
226
+ You can register your plugins in an initializer:
227
+ ```
228
+ # -- config/initializers/sorbet_rails.rb
229
+ SorbetRails::ModelRbiFormatter.register_plugin(MyCustomPlugin)
230
+ ```
231
+ The default plugins and other customizations are defined [here](https://github.com/chanzuckerberg/sorbet-rails/blob/master/lib/sorbet-rails/model_plugins/plugins.rb).
232
+
157
233
  ## Contributing
158
234
 
159
235
  Contributions and ideas are welcome! Please see [our contributing guide](CONTRIBUTING.md) and don't hesitate to open an issue or send a pull request to improve the functionality of this gem.
data/lib/sorbet-rails.rb CHANGED
@@ -1,3 +1,7 @@
1
+ # typed: strong
1
2
  module SorbetRails
2
- require 'sorbet-rails/railtie' if defined?(Rails)
3
+ if defined?(Rails)
4
+ require 'sorbet-rails/railtie'
5
+ require 'sorbet-rails/model_rbi_formatter'
6
+ end
3
7
  end
@@ -0,0 +1,27 @@
1
+ # typed: strong
2
+
3
+ class ActiveRecord::Base < Object
4
+ extend T::Sig
5
+
6
+ sig { returns(T::Boolean) }
7
+ def self.table_exists?; end
8
+
9
+ sig { returns(T::Hash[String, T::Hash[String, T.untyped]]) }
10
+ def self.defined_enums; end
11
+
12
+ sig { returns(T::Hash[String, T.untyped]) }
13
+ def self.columns_hash; end
14
+
15
+ sig { returns(T::Hash[String, T.untyped]) }
16
+ def self.reflections; end
17
+
18
+ sig { returns(T.untyped) }
19
+ def self.connection; end
20
+
21
+ sig { returns(T::Boolean) }
22
+ def self.abstract_class?; end
23
+ end
24
+
25
+ class ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter;
26
+ def klass; end
27
+ end
@@ -1,3 +1,4 @@
1
+ # typed: false
1
2
  module SorbetRails
2
3
  module CustomFinderMethods
3
4
  def find_n(*ids)
@@ -11,5 +12,15 @@ module SorbetRails
11
12
  def last_n(n)
12
13
  last(n)
13
14
  end
15
+
16
+ # Redeclare these dynamic methods here otherwise they will be generated by
17
+ # `srb rbi hidden-definitions`
18
+ def find_by_id(id)
19
+ find_by(id: id)
20
+ end
21
+
22
+ def find_by_id!(id)
23
+ find_by!(id: id)
24
+ end
14
25
  end
15
26
  end
@@ -0,0 +1,33 @@
1
+ # typed: strict
2
+ require 'parlour'
3
+ class SorbetRails::HelperRbiFormatter
4
+ extend T::Sig
5
+ # @param [Array<Module>] helpers
6
+ # @return [void]
7
+ sig { params(helpers: T::Array[Module]).void }
8
+ def initialize(helpers)
9
+ @parlour = T.let(Parlour::RbiGenerator.new, Parlour::RbiGenerator)
10
+ @helpers = T.let(helpers, T::Array[Module])
11
+ end
12
+
13
+ # Generates RBI file's contents.
14
+ # @return [String]
15
+ sig {returns(String)}
16
+ def generate_rbi
17
+ puts "-- Generate sigs for helpers --"
18
+
19
+ @parlour.root.add_comments([
20
+ 'This is an autogenerated file for Rails helpers.',
21
+ 'Please rerun rake rails_rbi:helpers to regenerate.'
22
+ ])
23
+
24
+ @helpers.each do |helper|
25
+ @parlour.root.create_module(helper.to_s) do |mod|
26
+ mod.create_include('Kernel')
27
+ mod.create_include('ActionView::Helpers')
28
+ end
29
+ end
30
+
31
+ return @parlour.rbi
32
+ end
33
+ end
@@ -0,0 +1,91 @@
1
+ # typed: true
2
+ require ('sorbet-rails/model_plugins/base')
3
+ class SorbetRails::ModelPlugins::ActiveRecordAssoc < SorbetRails::ModelPlugins::Base
4
+ sig {params(model_class: T.class_of(ActiveRecord::Base), available_classes: T::Set[String]).void}
5
+ def initialize(model_class, available_classes)
6
+ super
7
+ @columns_hash = @model_class.table_exists? ? @model_class.columns_hash : {}
8
+ end
9
+
10
+ sig { implementation.params(root: Parlour::RbiGenerator::Namespace).void }
11
+ def generate(root)
12
+ return unless @model_class.reflections.length > 0
13
+
14
+ assoc_module_name = self.model_module_name("GeneratedAssociationMethods")
15
+ assoc_module_rbi = root.create_module(assoc_module_name)
16
+ assoc_module_rbi.create_extend("T::Sig")
17
+
18
+ model_class_rbi = root.create_class(self.model_class_name)
19
+ model_class_rbi.create_include(assoc_module_name)
20
+
21
+ @model_class.reflections.sort.each do |assoc_name, reflection|
22
+ reflection.collection? ?
23
+ populate_collection_assoc_getter_setter(assoc_module_rbi, assoc_name, reflection) :
24
+ populate_single_assoc_getter_setter(assoc_module_rbi, assoc_name, reflection)
25
+ end
26
+ end
27
+
28
+ def populate_single_assoc_getter_setter(assoc_module_rbi, assoc_name, reflection)
29
+ # TODO allow people to specify the possible values of polymorphic associations
30
+ assoc_class = assoc_should_be_untyped?(reflection) ? "T.untyped" : "::#{reflection.klass.name}"
31
+ assoc_type = "T.nilable(#{assoc_class})"
32
+ if reflection.belongs_to?
33
+ # if this is a belongs_to connection, we may be able to detect whether
34
+ # this field is required & use a stronger type
35
+ column_def = @columns_hash[reflection.foreign_key.to_s]
36
+ if column_def
37
+ assoc_type = assoc_class if !column_def.null
38
+ end
39
+ end
40
+
41
+ assoc_module_rbi.create_method(
42
+ assoc_name.to_s,
43
+ return_type: assoc_type,
44
+ )
45
+ assoc_module_rbi.create_method(
46
+ "#{assoc_name}=",
47
+ parameters: [
48
+ Parameter.new("value", type: assoc_type)
49
+ ],
50
+ return_type: nil,
51
+ )
52
+ end
53
+
54
+ def populate_collection_assoc_getter_setter(assoc_module_rbi, assoc_name, reflection)
55
+ # TODO allow people to specify the possible values of polymorphic associations
56
+ assoc_class = assoc_should_be_untyped?(reflection) ? "T.untyped" : "::#{reflection.klass.name}"
57
+ relation_class = relation_should_be_untyped?(reflection) ?
58
+ "ActiveRecord::Associations::CollectionProxy" :
59
+ "#{assoc_class}::ActiveRecord_Associations_CollectionProxy"
60
+
61
+ assoc_module_rbi.create_method(
62
+ assoc_name.to_s,
63
+ return_type: relation_class,
64
+ )
65
+ assoc_module_rbi.create_method(
66
+ "#{assoc_name}=",
67
+ parameters: [
68
+ Parameter.new("value", type: "T.any(T::Array[#{assoc_class}], #{relation_class})")
69
+ ],
70
+ return_type: nil,
71
+ )
72
+ end
73
+
74
+ sig { params(reflection: T.untyped).returns(T.nilable(T::Boolean)) }
75
+ def assoc_should_be_untyped?(reflection)
76
+ polymorphic_assoc?(reflection) || !@available_classes.include?(reflection.klass.name)
77
+ end
78
+
79
+ sig { params(reflection: T.untyped).returns(T.nilable(T::Boolean)) }
80
+ def relation_should_be_untyped?(reflection)
81
+ # only type the relation we'll generate
82
+ assoc_should_be_untyped?(reflection) || !@available_classes.include?(reflection.klass.name)
83
+ end
84
+
85
+ sig { params(reflection: T.untyped).returns(T.nilable(T::Boolean)) }
86
+ def polymorphic_assoc?(reflection)
87
+ reflection.through_reflection ?
88
+ polymorphic_assoc?(reflection.source_reflection) :
89
+ reflection.polymorphic?
90
+ end
91
+ end
@@ -0,0 +1,111 @@
1
+ # typed: strict
2
+ require ('sorbet-rails/model_plugins/base')
3
+ class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugins::Base
4
+
5
+ sig { implementation.params(root: Parlour::RbiGenerator::Namespace).void }
6
+ def generate(root)
7
+ columns_hash = @model_class.table_exists? ? @model_class.columns_hash : {}
8
+ return unless columns_hash.size > 0
9
+
10
+ attribute_module_name = self.model_module_name("GeneratedAttributeMethods")
11
+ attribute_module_rbi = root.create_module(attribute_module_name)
12
+ attribute_module_rbi.create_extend("T::Sig")
13
+
14
+ model_class_rbi = root.create_class(self.model_class_name)
15
+ model_class_rbi.create_include(attribute_module_name)
16
+
17
+ columns_hash.sort.each do |column_name, column_def|
18
+ if @model_class.defined_enums.has_key?(column_name)
19
+ # enum attribute is treated differently
20
+ assignable_type = "T.any(Integer, String, Symbol)"
21
+ assignable_type = "T.nilable(#{assignable_type})" if column_def.null
22
+ return_type = "String"
23
+ return_type = "T.nilable(#{return_type})" if column_def.null
24
+
25
+ attribute_module_rbi.create_method(
26
+ column_name.to_s,
27
+ return_type: return_type,
28
+ )
29
+ attribute_module_rbi.create_method(
30
+ "#{column_name}=",
31
+ parameters: [
32
+ Parameter.new("value", type: assignable_type)
33
+ ],
34
+ return_type: nil,
35
+ )
36
+ else
37
+ column_type = type_for_column_def(column_def)
38
+ attribute_module_rbi.create_method(
39
+ column_name.to_s,
40
+ return_type: column_type.to_s,
41
+ )
42
+ attribute_module_rbi.create_method(
43
+ "#{column_name}=",
44
+ parameters: [
45
+ Parameter.new("value", type: column_type.to_s)
46
+ ],
47
+ return_type: nil,
48
+ )
49
+ end
50
+
51
+ attribute_module_rbi.create_method(
52
+ "#{column_name}?",
53
+ return_type: "T::Boolean",
54
+ )
55
+ end
56
+ end
57
+
58
+ sig {params(column_def: T.untyped).returns(T.any(String, Class))}
59
+ def type_for_column_def(column_def)
60
+ cast_type = ActiveRecord::Base.connection.respond_to?(:lookup_cast_type_from_column) ?
61
+ ActiveRecord::Base.connection.lookup_cast_type_from_column(column_def) :
62
+ column_def.cast_type
63
+
64
+ strict_type = active_record_type_to_sorbet_type(cast_type)
65
+
66
+ if column_def.respond_to?(:array?) && column_def.array?
67
+ strict_type = "T::Array[#{strict_type}]"
68
+ end
69
+ column_def.null ? "T.nilable(#{strict_type})" : strict_type
70
+ end
71
+
72
+ sig {
73
+ params(
74
+ # in v4.2, datetime can be TimeZoneConverter
75
+ klass: T.any(Object, ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter)
76
+ ).
77
+ returns(T.any(String, Class))
78
+ }
79
+ def active_record_type_to_sorbet_type(klass)
80
+ case klass
81
+ when ActiveRecord::Type::Boolean
82
+ "T::Boolean"
83
+ when ActiveRecord::Type::DateTime
84
+ DateTime
85
+ when ActiveRecord::Type::Date
86
+ Date
87
+ when ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter
88
+ klass.klass
89
+ when ActiveRecord::Type::Decimal
90
+ BigDecimal
91
+ when ActiveRecord::Type::Float
92
+ Float
93
+ when ActiveRecord::Type::Time
94
+ Time
95
+ when ActiveRecord::Type::BigInteger, ActiveRecord::Type::Integer, ActiveRecord::Type::DecimalWithoutScale, ActiveRecord::Type::UnsignedInteger
96
+ Integer
97
+ when ActiveRecord::Type::Binary, ActiveRecord::Type::String, ActiveRecord::Type::Text
98
+ String
99
+ else
100
+ # Json type is only supported in Rails 5.2 and above
101
+ case
102
+ when Object.const_defined?('ActiveRecord::Type::Json') && klass.is_a?(ActiveRecord::Type::Json)
103
+ "T.any(T::Array[T.untyped], T::Boolean, Float, T::Hash[T.untyped, T.untyped], Integer, String)"
104
+ when Object.const_defined?('ActiveRecord::Enum::EnumType') && klass.is_a?(ActiveRecord::Enum::EnumType)
105
+ String
106
+ else
107
+ "T.untyped"
108
+ end
109
+ end
110
+ end
111
+ end