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.
- checksums.yaml +5 -5
- data/.gitignore +13 -1
- data/.travis.yml +2 -2
- data/Gemfile +1 -1
- data/README.md +79 -3
- data/lib/sorbet-rails.rb +5 -1
- data/lib/sorbet-rails/activerecord.rbi +27 -0
- data/lib/sorbet-rails/custom_finder_methods.rb +11 -0
- data/lib/sorbet-rails/helper_rbi_formatter.rb +33 -0
- data/lib/sorbet-rails/model_plugins/active_record_assoc.rb +91 -0
- data/lib/sorbet-rails/model_plugins/active_record_attribute.rb +111 -0
- data/lib/sorbet-rails/model_plugins/active_record_enum.rb +43 -0
- data/lib/sorbet-rails/model_plugins/active_record_finder_methods.rb +80 -0
- data/lib/sorbet-rails/model_plugins/active_record_named_scope.rb +28 -0
- data/lib/sorbet-rails/model_plugins/active_record_querying.rb +42 -0
- data/lib/sorbet-rails/model_plugins/active_relation_where_not.rb +28 -0
- data/lib/sorbet-rails/model_plugins/base.rb +33 -0
- data/lib/sorbet-rails/model_plugins/custom_finder_methods.rb +54 -0
- data/lib/sorbet-rails/model_plugins/enumerable_collections.rb +49 -0
- data/lib/sorbet-rails/model_plugins/plugins.rb +45 -0
- data/lib/sorbet-rails/model_rbi_formatter.rb +108 -362
- data/lib/sorbet-rails/model_utils.rb +45 -0
- data/lib/sorbet-rails/railtie.rb +1 -0
- data/lib/sorbet-rails/routes_rbi_formatter.rb +12 -3
- data/lib/sorbet-rails/tasks/rails_rbi.rake +36 -15
- data/lib/sorbet-rails/utils.rb +4 -0
- data/sorbet-rails.gemspec +4 -2
- data/spec/helper_rbi_formatter_spec.rb +13 -0
- data/spec/model_plugins_spec.rb +31 -0
- data/spec/model_rbi_formatter_spec.rb +5 -5
- data/spec/rake_rails_rbi_helpers_spec.rb +14 -0
- data/spec/routes_rbi_formatter_spec.rb +3 -3
- data/spec/sorbet_spec.rb +12 -16
- data/spec/support/rails_shared/app/controllers/application_controller.rb +1 -1
- data/spec/support/rails_shared/app/helpers/bar_helper.rb +2 -0
- data/spec/support/rails_shared/app/helpers/baz_helper.rb +2 -0
- data/spec/support/rails_shared/app/helpers/foo_helper.rb +2 -0
- data/spec/support/rails_shared/app/models/concerns/mythical.rb +10 -0
- data/spec/support/rails_shared/app/models/wand.rb +1 -0
- data/spec/support/rails_shared/config/initializers/sorbet_rails.rb +3 -0
- data/spec/support/rails_shared/lib/mythical_rbi_plugin.rb +16 -0
- data/spec/support/rails_shared/sorbet_test_cases.rb +5 -2
- data/spec/support/rails_shared/typed-override.yaml +2 -0
- data/spec/support/rails_symlinks/app/helpers +1 -0
- data/spec/support/rails_symlinks/config/initializers/sorbet_rails.rb +1 -0
- data/spec/support/rails_symlinks/lib/mythical_rbi_plugin.rb +1 -0
- data/spec/support/rails_symlinks/typed-override.yaml +1 -0
- data/spec/support/v4.2/Gemfile.lock +5 -0
- data/spec/support/v4.2/app/helpers +1 -0
- data/spec/support/v4.2/config/initializers/sorbet_rails.rb +1 -0
- data/spec/support/v4.2/config/routes.rb +0 -1
- data/spec/support/v4.2/lib/mythical_rbi_plugin.rb +1 -0
- data/spec/support/v4.2/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
- data/spec/support/v4.2/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
- data/spec/support/v4.2/typed-override.yaml +1 -0
- data/spec/support/v5.0/Gemfile.lock +5 -0
- data/spec/support/v5.0/app/helpers +1 -0
- data/spec/support/v5.0/config/initializers/sorbet_rails.rb +1 -0
- data/spec/support/v5.0/config/routes.rb +0 -1
- data/spec/support/v5.0/lib/mythical_rbi_plugin.rb +1 -0
- data/spec/support/v5.0/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
- data/spec/support/v5.0/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
- data/spec/support/v5.0/typed-override.yaml +1 -0
- data/spec/support/v5.1/Gemfile.lock +5 -0
- data/spec/support/v5.1/app/helpers +1 -0
- data/spec/support/v5.1/config/initializers/sorbet_rails.rb +1 -0
- data/spec/support/v5.1/lib/mythical_rbi_plugin.rb +1 -0
- data/spec/support/v5.1/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
- data/spec/support/v5.1/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
- data/spec/support/v5.1/typed-override.yaml +1 -0
- data/spec/support/v5.2-no-sorbet/Gemfile +0 -2
- data/spec/support/v5.2-no-sorbet/Gemfile.lock +7 -5
- data/spec/support/v5.2-no-sorbet/app/helpers +1 -0
- data/spec/support/v5.2-no-sorbet/config/initializers/sorbet_rails.rb +1 -0
- data/spec/support/v5.2-no-sorbet/lib/mythical_rbi_plugin.rb +1 -0
- data/spec/support/v5.2-no-sorbet/sorbet_test_cases.rb +1 -0
- data/spec/support/v5.2-no-sorbet/typed-override.yaml +1 -0
- data/spec/support/v5.2/Gemfile +0 -2
- data/spec/support/v5.2/Gemfile.lock +7 -5
- data/spec/support/v5.2/app/helpers +1 -0
- data/spec/support/v5.2/config/initializers/sorbet_rails.rb +1 -0
- data/spec/support/v5.2/lib/mythical_rbi_plugin.rb +1 -0
- data/spec/support/v5.2/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
- data/spec/support/v5.2/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
- data/spec/support/v5.2/typed-override.yaml +1 -0
- data/spec/support/v6.0/Gemfile +1 -3
- data/spec/support/v6.0/Gemfile.lock +61 -59
- data/spec/support/v6.0/app/helpers +1 -0
- data/spec/support/v6.0/config/initializers/sorbet_rails.rb +1 -0
- data/spec/support/v6.0/config/routes.rb +0 -1
- data/spec/support/v6.0/lib/mythical_rbi_plugin.rb +1 -0
- data/spec/support/v6.0/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1 -1
- data/spec/support/v6.0/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1 -1
- data/spec/support/v6.0/typed-override.yaml +1 -0
- data/spec/test_data/v4.2/expected_helpers.rbi +17 -0
- data/spec/test_data/v4.2/expected_potion.rbi +259 -28
- data/spec/test_data/v4.2/expected_spell_book.rbi +278 -37
- data/spec/test_data/v4.2/expected_srb_tc_output.txt +1 -99
- data/spec/test_data/v4.2/expected_wand.rbi +345 -96
- data/spec/test_data/v4.2/expected_wizard.rbi +322 -76
- data/spec/test_data/v4.2/expected_wizard_wo_spellbook.rbi +322 -76
- data/spec/test_data/v5.0/expected_helpers.rbi +17 -0
- data/spec/test_data/v5.0/expected_internal_metadata.rbi +273 -37
- data/spec/test_data/v5.0/expected_potion.rbi +259 -28
- data/spec/test_data/v5.0/expected_schema_migration.rbi +264 -28
- data/spec/test_data/v5.0/expected_spell_book.rbi +278 -37
- data/spec/test_data/v5.0/expected_srb_tc_output.txt +1 -81
- data/spec/test_data/v5.0/expected_wand.rbi +341 -92
- data/spec/test_data/v5.0/expected_wizard.rbi +318 -72
- data/spec/test_data/v5.0/expected_wizard_wo_spellbook.rbi +318 -72
- data/spec/test_data/v5.1/expected_helpers.rbi +17 -0
- data/spec/test_data/v5.1/expected_internal_metadata.rbi +273 -37
- data/spec/test_data/v5.1/expected_potion.rbi +259 -28
- data/spec/test_data/v5.1/expected_schema_migration.rbi +264 -28
- data/spec/test_data/v5.1/expected_spell_book.rbi +278 -37
- data/spec/test_data/v5.1/expected_srb_tc_output.txt +1 -81
- data/spec/test_data/v5.1/expected_wand.rbi +341 -92
- data/spec/test_data/v5.1/expected_wizard.rbi +318 -72
- data/spec/test_data/v5.1/expected_wizard_wo_spellbook.rbi +318 -72
- data/spec/test_data/v5.2-no-sorbet/expected_attachment.rbi +265 -29
- data/spec/test_data/v5.2-no-sorbet/expected_blob.rbi +271 -35
- data/spec/test_data/v5.2-no-sorbet/expected_helpers.rbi +17 -0
- data/spec/test_data/v5.2-no-sorbet/expected_internal_metadata.rbi +273 -37
- data/spec/test_data/v5.2-no-sorbet/expected_potion.rbi +259 -28
- data/spec/test_data/v5.2-no-sorbet/expected_schema_migration.rbi +264 -28
- data/spec/test_data/v5.2-no-sorbet/expected_spell_book.rbi +278 -37
- data/spec/test_data/v5.2-no-sorbet/expected_srb_tc_output.txt +1 -81
- data/spec/test_data/v5.2-no-sorbet/expected_wand.rbi +351 -102
- data/spec/test_data/v5.2-no-sorbet/expected_wizard.rbi +318 -72
- data/spec/test_data/v5.2-no-sorbet/expected_wizard_wo_spellbook.rbi +318 -72
- data/spec/test_data/v5.2/expected_attachment.rbi +265 -29
- data/spec/test_data/v5.2/expected_blob.rbi +271 -35
- data/spec/test_data/v5.2/expected_helpers.rbi +17 -0
- data/spec/test_data/v5.2/expected_internal_metadata.rbi +273 -37
- data/spec/test_data/v5.2/expected_potion.rbi +259 -28
- data/spec/test_data/v5.2/expected_schema_migration.rbi +264 -28
- data/spec/test_data/v5.2/expected_spell_book.rbi +278 -37
- data/spec/test_data/v5.2/expected_srb_tc_output.txt +1 -81
- data/spec/test_data/v5.2/expected_wand.rbi +351 -102
- data/spec/test_data/v5.2/expected_wizard.rbi +318 -72
- data/spec/test_data/v5.2/expected_wizard_wo_spellbook.rbi +318 -72
- data/spec/test_data/v6.0/expected_attachment.rbi +265 -29
- data/spec/test_data/v6.0/expected_blob.rbi +271 -35
- data/spec/test_data/v6.0/expected_helpers.rbi +17 -0
- data/spec/test_data/v6.0/expected_internal_metadata.rbi +273 -37
- data/spec/test_data/v6.0/expected_potion.rbi +259 -28
- data/spec/test_data/v6.0/expected_schema_migration.rbi +264 -28
- data/spec/test_data/v6.0/expected_spell_book.rbi +278 -37
- data/spec/test_data/v6.0/expected_srb_tc_output.txt +1 -81
- data/spec/test_data/v6.0/expected_wand.rbi +351 -102
- data/spec/test_data/v6.0/expected_wizard.rbi +318 -72
- data/spec/test_data/v6.0/expected_wizard_wo_spellbook.rbi +318 -72
- metadata +120 -37
- data/lib/sorbet-rails/rbi/activerecord.rbi +0 -207
- data/spec/support/v4.2/app/helpers/application_helper.rb +0 -3
- data/spec/support/v4.2/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
- data/spec/support/v4.2/sorbet/rbi/hidden-definitions/errors.txt +0 -11998
- data/spec/support/v4.2/sorbet/rbi/hidden-definitions/hidden.rbi +0 -27774
- data/spec/support/v5.0/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
- data/spec/support/v5.0/sorbet/rbi/hidden-definitions/errors.txt +0 -10523
- data/spec/support/v5.0/sorbet/rbi/hidden-definitions/hidden.rbi +0 -24969
- data/spec/support/v5.1/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
- data/spec/support/v5.1/sorbet/rbi/hidden-definitions/errors.txt +0 -10226
- data/spec/support/v5.1/sorbet/rbi/hidden-definitions/hidden.rbi +0 -24635
- data/spec/support/v5.2/.ruby-version +0 -1
- data/spec/support/v5.2/sorbet/rbi/gems/sorbet-runtime.rbi +0 -644
- data/spec/support/v5.2/sorbet/rbi/hidden-definitions/errors.txt +0 -10046
- data/spec/support/v5.2/sorbet/rbi/hidden-definitions/hidden.rbi +0 -24424
- data/spec/support/v6.0/sorbet/rbi/gems/sorbet-runtime.rbi +0 -647
- data/spec/support/v6.0/sorbet/rbi/hidden-definitions/errors.txt +0 -12074
- data/spec/support/v6.0/sorbet/rbi/hidden-definitions/hidden.rbi +0 -28231
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cdf5c9e5cc4d021af0e9206c53d9682288bb204f85179bffc2f041b2b783f8c8
|
4
|
+
data.tar.gz: 72de625982952376be79c47926266d7d25767b3b015b463c9302e75421643278
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
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
|
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.
|
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
|
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
@@ -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
|