sorbet-rails 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +1 -0
- data/LICENSE +21 -0
- data/README.md +95 -0
- data/lib/sorbet-rails.rb +3 -0
- data/lib/sorbet-rails/custom_finder_methods.rb +15 -0
- data/lib/sorbet-rails/model_rbi_formatter.rb +367 -0
- data/lib/sorbet-rails/railtie.rb +18 -0
- data/lib/sorbet-rails/routes_rbi_formatter.rb +63 -0
- metadata +51 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e3f7dd83f8d099f5951a1a8bd13707c4e38d60e5
|
4
|
+
data.tar.gz: 4c7e2dc570d7695826c1c88d3f6711c7600dbe57
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '0780d48a3e347edd504e6b3b2e11ca88e60118a29e25b3b0b4c931192aec27e8c2f2d1f2ddeff5c111a106a5174ed2feb8e7d462f92a59cc8a91284c9d0c48eb'
|
7
|
+
data.tar.gz: 8a3af472623c72b6cf615acd4a19b52b341b82ed18392feb8e25ddcd9a10177b329d3ab77c557b1ca32dacada4216f1ac92e6dcc1503ada0fa9507a087ebf786
|
data/Gemfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
source 'https://rubygems.org'
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 Chan Zuckerberg Initiative
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# sorbet-rails
|
2
|
+
Set of tools to make sorbet work with rails seamlessly.
|
3
|
+
|
4
|
+
This gem adds a few rake tasks to generate RBI for dynamic methods generated by Rails. It also includes sigs for related Rails classes. The generated rbis are added to `sorbet/rails-rbi/` folder.
|
5
|
+
|
6
|
+
Please feel free to send PR requests or file issues to improve the functionality of the gem!
|
7
|
+
|
8
|
+
## Initial setup
|
9
|
+
|
10
|
+
Following the steps below to setup the rbis after `srb tc`
|
11
|
+
1. Include ActiveRecord RBI
|
12
|
+
```
|
13
|
+
$ srb tc sorbet-typed
|
14
|
+
```
|
15
|
+
2. Generate routes RBI
|
16
|
+
```
|
17
|
+
$ rake rails_rbi:routes
|
18
|
+
```
|
19
|
+
3. Generate models RBI
|
20
|
+
```
|
21
|
+
$ rake rails_rbi:models
|
22
|
+
```
|
23
|
+
4. Auto-upgrade the typecheck level of files
|
24
|
+
```
|
25
|
+
$ srb tc --suggest-typed --typed=strict --error-white-list=7022 -a
|
26
|
+
```
|
27
|
+
Because we've generated RBI files for models & routes, a lot more files should be type-checkable now
|
28
|
+
|
29
|
+
## ActiveRecord RBI
|
30
|
+
|
31
|
+
There are ActiveRecord RBI that we vendor with this gem. Please run `srb tc sorbet-typed` to include the provided RBI in your `sorbet/rbi_list`
|
32
|
+
|
33
|
+
## Routes RBI
|
34
|
+
The following rake task generates `_path` and `_url` methods for all named routes defined in `routes.rb`
|
35
|
+
```
|
36
|
+
rake rails_rbi:routes
|
37
|
+
```
|
38
|
+
## Models RBI
|
39
|
+
The following rake task generates rbi files for all models in the Rails App (all descendants of ActiveRecord::Base)
|
40
|
+
```
|
41
|
+
rake rails_rbi:models
|
42
|
+
```
|
43
|
+
You can also regenerate rbi files for specific models
|
44
|
+
```
|
45
|
+
rake rails_rbi:models[ModelName,AnotherOne,...]
|
46
|
+
```
|
47
|
+
At the moment, the generation task generate the following signatures
|
48
|
+
- Column getters & setters
|
49
|
+
- Associations getters & setters
|
50
|
+
- Enum values, checkers & scopes
|
51
|
+
- Named scopes
|
52
|
+
- Model relation class
|
53
|
+
|
54
|
+
## Tips & Tricks
|
55
|
+
### `find`, `first` and `last`
|
56
|
+
These 3 methods can either return a single nilable record or an array of records. Sorbet does not allow us to define multiple sigs for a function. It doesn't support defining one function sig that has varying returning value depending on the input param type. We opt to define the most commonly used sig for these methods, and monkey-patch new functions for the secondary use.
|
57
|
+
|
58
|
+
In short:
|
59
|
+
- Use `find`, `first` and `last` to fetch a single record
|
60
|
+
- Use `find_n`, `first_n`, `last_n` to fetch an array of records.
|
61
|
+
|
62
|
+
### `find_by_<attributes>`, `<attribute>_changed?`, etc.
|
63
|
+
Rails supports dynamic methods based on attribute names, such as `find_by_<attribute>`, `<attribute>_changed?`, etc. They all have static counterparts. Instead of generating all possible dynamic methods that Rails support, we recommend to use of the static version of these methods instead (also recommended by RuboCop). We've added sigs for th
|
64
|
+
|
65
|
+
Following are the list of attribute dynamic methods and their static counterpart. The static methods have proper sigs.
|
66
|
+
- `find_by_<attributes>` -> `find_by(<attributes>)`
|
67
|
+
- `find_by_<attributes>!` -> `find_by!(<attributes>)`
|
68
|
+
- `<attribute>_changed?` -> `attribute_changed?(<attribute>)`
|
69
|
+
- `saved_change_to_<attribute>?` -> `saved_change_to_attribute?(<attribute>)`
|
70
|
+
|
71
|
+
### `after_commit` and other callbacks
|
72
|
+
Consider codemod-ing `after_commit` callbacks to use instance method functions. Sorbet doesn't support binding an optional block with a different context. Because of this, when using a callback with a custom block, the block is evaluated in the wrong context (Class-level context).
|
73
|
+
|
74
|
+
```
|
75
|
+
after_commit do ... end
|
76
|
+
```
|
77
|
+
codemod to
|
78
|
+
```
|
79
|
+
after_commit :after_commit
|
80
|
+
def after_commit
|
81
|
+
...
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
I've created a gist with codemod commands to convert the code automatically
|
86
|
+
https://gist.github.com/manhhung741/d2e0a8f9c4178f328b241dd8b28ccc67
|
87
|
+
|
88
|
+
See this link for a full list of callbacks available in Rails:
|
89
|
+
https://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
|
90
|
+
|
91
|
+
### Look for `# typed: ignore` files
|
92
|
+
|
93
|
+
Because sorbet initial setup tries to flag files at the typecheck level that generates 0 errors, there may be files in your repository that is `# typed: ignore`. This is because sometimes Rails allow very dynamic code that Sorbet does not regard as typecheck-able.
|
94
|
+
|
95
|
+
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 #4). Usually this will resolve in many more files typecheckable.
|
data/lib/sorbet-rails.rb
ADDED
@@ -0,0 +1,367 @@
|
|
1
|
+
# typed: true
|
2
|
+
class ModelRbiFormatter
|
3
|
+
|
4
|
+
RUBY_TO_SORBET_TYPE_MAPPING = {
|
5
|
+
boolean: 'T::Boolean',
|
6
|
+
date: 'Date',
|
7
|
+
datetime: 'DateTime',
|
8
|
+
decimal: 'Integer',
|
9
|
+
integer: 'Integer',
|
10
|
+
string: 'String',
|
11
|
+
text: 'String',
|
12
|
+
json: 'Hash',
|
13
|
+
jsonb: 'Hash',
|
14
|
+
}
|
15
|
+
|
16
|
+
def initialize(model_class, available_classes)
|
17
|
+
@model_class = model_class
|
18
|
+
@available_classes = available_classes
|
19
|
+
@columns_hash = model_class.columns_hash
|
20
|
+
@generated_sigs = ActiveSupport::HashWithIndifferentAccess.new
|
21
|
+
@generated_class_sigs = ActiveSupport::HashWithIndifferentAccess.new
|
22
|
+
@generated_scope_sigs = ActiveSupport::HashWithIndifferentAccess.new
|
23
|
+
@generated_querying_sigs = ActiveSupport::HashWithIndifferentAccess.new
|
24
|
+
begin
|
25
|
+
# Load all dynamic instance methods of this model by instantiating a fake model
|
26
|
+
@model_class.new
|
27
|
+
rescue StandardError
|
28
|
+
puts "Note: Unable to create new instance of #{model_class.name}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate_rbi
|
33
|
+
puts "-- Generate sigs for #{@model_class.name} --"
|
34
|
+
populate_activerecord_querying_methods
|
35
|
+
populate_named_scope_methods
|
36
|
+
populate_generated_column_methods
|
37
|
+
populate_generated_association_methods
|
38
|
+
populate_generated_enum_methods
|
39
|
+
|
40
|
+
@buffer = []
|
41
|
+
@buffer << draw_class_header
|
42
|
+
|
43
|
+
@model_class.instance_methods.sort.each do |method_name|
|
44
|
+
expected_sig = @generated_sigs[method_name]
|
45
|
+
next unless expected_sig.present?
|
46
|
+
method_obj = @model_class.instance_method(method_name)
|
47
|
+
draw_method(method_name, method_obj, expected_sig, false)
|
48
|
+
end
|
49
|
+
|
50
|
+
@model_class.methods.sort.each do |method_name|
|
51
|
+
expected_sig = @generated_class_sigs[method_name]
|
52
|
+
next unless expected_sig.present?
|
53
|
+
method_obj = @model_class.method(method_name)
|
54
|
+
draw_method(method_name, method_obj, expected_sig, true)
|
55
|
+
end
|
56
|
+
|
57
|
+
@buffer << draw_class_footer
|
58
|
+
|
59
|
+
# <Model>::NamedScope is a fake module added so that when a method is defined
|
60
|
+
# in this module, it'll be added to both the Model class as a class method
|
61
|
+
# and to its relation as an instance method.
|
62
|
+
#
|
63
|
+
# We need to define the NamedScope module after the other classes
|
64
|
+
# to work around Sorbet loading order bug
|
65
|
+
# https://sorbet-ruby.slack.com/archives/CHN2L03NH/p1556065791047300
|
66
|
+
@buffer << "\n"
|
67
|
+
@buffer << draw_named_scope_header
|
68
|
+
# For simplicity, generate both in the same module for now.
|
69
|
+
# We don't need to define two fake modules to share methods between <Model> and <Relation>
|
70
|
+
({}.
|
71
|
+
merge(@generated_scope_sigs).
|
72
|
+
merge(@generated_querying_sigs)
|
73
|
+
).each do |method_name, expected_sig|
|
74
|
+
method_obj = @model_class.method(method_name) if @model_class.methods.include?(method_name.to_sym)
|
75
|
+
# this is not a class method because it is added to NamedScope
|
76
|
+
draw_method(method_name, method_obj, expected_sig, false)
|
77
|
+
end
|
78
|
+
@buffer << draw_named_scope_footer
|
79
|
+
@buffer << "\n"
|
80
|
+
@buffer.join("\n")
|
81
|
+
end
|
82
|
+
|
83
|
+
def draw_method(method_name, method_obj, expected_sig, is_class_method)
|
84
|
+
if !method_obj.present?
|
85
|
+
# not very actionable because this could be a method in a newer version of Rails
|
86
|
+
# puts "Skip method '#{method_name}' because there is no matching method object."
|
87
|
+
return
|
88
|
+
end
|
89
|
+
if !is_method_autogenerated?(method_obj)
|
90
|
+
puts "Skip method '#{method_name}' because it is not autogenerated by Rails."
|
91
|
+
return
|
92
|
+
end
|
93
|
+
if !matched_signature?(method_obj, expected_sig)
|
94
|
+
puts "Skip method '#{method_name}' because it has different signature from expected."
|
95
|
+
return
|
96
|
+
end
|
97
|
+
@buffer << generate_method_sig(method_name, expected_sig, is_class_method).indent(2)
|
98
|
+
end
|
99
|
+
|
100
|
+
def populate_activerecord_querying_methods
|
101
|
+
# All is a named scope that most method from ActiveRecord::Querying delegate to
|
102
|
+
# rails/activerecord/lib/active_record/querying.rb:21
|
103
|
+
@generated_scope_sigs["all"] = { ret: "#{@model_class.name}::Relation" }
|
104
|
+
# It's not possible to typedef all methods in ActiveRecord::Querying module to have the
|
105
|
+
# matching type. By generating model-specific sig, we can typedef these methods to return
|
106
|
+
# <Model>::Relation class.
|
107
|
+
# rails/activerecord/lib/active_record/querying.rb
|
108
|
+
model_query_relation_methods = [
|
109
|
+
:select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
|
110
|
+
:where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
|
111
|
+
:having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only,
|
112
|
+
]
|
113
|
+
model_query_relation_methods.each do |method_name|
|
114
|
+
@generated_querying_sigs[method_name.to_s] = {
|
115
|
+
args: [
|
116
|
+
{name: :args, arg_type: :rest, value_type: 'T.untyped'},
|
117
|
+
{name: :block, arg_type: :block, value_type: 'T.nilable(T.proc.void)'},
|
118
|
+
],
|
119
|
+
ret: "#{@model_class.name}::Relation",
|
120
|
+
}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def populate_named_scope_methods
|
125
|
+
@model_class.methods.sort.each do |method_name|
|
126
|
+
method_obj = @model_class.method(method_name)
|
127
|
+
next unless method_obj.present? && method_obj.source_location.present?
|
128
|
+
# we detect sscopes defined in a model by 2 criteria:
|
129
|
+
# - they don't have an owner name
|
130
|
+
# - they are defined in 'activerecord/lib/active_record/scoping/named.rb'
|
131
|
+
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/scoping/named.rb
|
132
|
+
next unless method_obj.owner.name == nil
|
133
|
+
source_file = method_obj.source_location[0]
|
134
|
+
next unless source_file.include?('lib/active_record/scoping/named.rb')
|
135
|
+
@generated_scope_sigs[method_name] = {
|
136
|
+
args: [ name: :args, arg_type: :rest, value_type: 'T.untyped' ],
|
137
|
+
ret: "#{@model_class.name}::Relation",
|
138
|
+
}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def populate_generated_column_methods
|
143
|
+
@columns_hash.each do |column_name, column_def|
|
144
|
+
if @model_class.defined_enums.has_key?(column_name)
|
145
|
+
# enum attribute is treated differently
|
146
|
+
assignable_type = "T.any(Integer, String, Symbol)"
|
147
|
+
assignable_type = "T.nilable(#{assignable_type})" if column_def.null
|
148
|
+
@generated_sigs.merge!({
|
149
|
+
"#{column_name}" => { ret: "String" },
|
150
|
+
"#{column_name}=" => {
|
151
|
+
args: [ name: :value, arg_type: :req, value_type: assignable_type],
|
152
|
+
},
|
153
|
+
})
|
154
|
+
else
|
155
|
+
column_type = type_for_column_def(column_def)
|
156
|
+
@generated_sigs.merge!({
|
157
|
+
"#{column_name}" => { ret: column_type },
|
158
|
+
"#{column_name}=" => {
|
159
|
+
args: [ name: :value, arg_type: :req, value_type: column_type ],
|
160
|
+
},
|
161
|
+
})
|
162
|
+
end
|
163
|
+
|
164
|
+
if column_def.type == :boolean
|
165
|
+
@generated_sigs["#{column_name}?"] = {
|
166
|
+
ret: "T::Boolean",
|
167
|
+
args: [ name: :args, arg_type: :rest, value_type: 'T.untyped' ],
|
168
|
+
}
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def populate_generated_association_methods
|
174
|
+
@model_class.reflections.each do |assoc_name, reflection|
|
175
|
+
reflection.collection? ?
|
176
|
+
populate_collection_assoc_getter_setter(assoc_name, reflection) :
|
177
|
+
populate_single_assoc_getter_setter(assoc_name, reflection)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def populate_single_assoc_getter_setter(assoc_name, reflection)
|
182
|
+
# TODO allow people to specify the possible values of polymorphic associations
|
183
|
+
assoc_class = assoc_should_be_untyped?(reflection) ? "T.untyped" : reflection.class_name
|
184
|
+
assoc_type = "T.nilable(#{assoc_class})"
|
185
|
+
if reflection.belongs_to?
|
186
|
+
# if this is a belongs_to connection, we may be able to detect whether
|
187
|
+
# this field is required & use a stronger type
|
188
|
+
column_def = @columns_hash[reflection.foreign_key.to_s]
|
189
|
+
if column_def
|
190
|
+
assoc_type = assoc_class if !column_def.null
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
@generated_sigs.merge!({
|
195
|
+
"#{assoc_name}" => { ret: assoc_type },
|
196
|
+
"#{assoc_name}=" => {
|
197
|
+
args: [ name: :value, arg_type: :req, value_type: assoc_type ],
|
198
|
+
},
|
199
|
+
})
|
200
|
+
end
|
201
|
+
|
202
|
+
def populate_collection_assoc_getter_setter(assoc_name, reflection)
|
203
|
+
# TODO allow people to specify the possible values of polymorphic associations
|
204
|
+
assoc_class = assoc_should_be_untyped?(reflection) ? "T.untyped" : reflection.class_name
|
205
|
+
relation_class = relation_should_be_untyped?(reflection) ?
|
206
|
+
"ActiveRecord::Relation[T.untyped]" :
|
207
|
+
"#{assoc_class}::Relation"
|
208
|
+
@generated_sigs.merge!({
|
209
|
+
"#{assoc_name}" => { ret: relation_class },
|
210
|
+
"#{assoc_name}=" => {
|
211
|
+
args: [ name: :value, arg_type: :req, value_type: "T.any(T::Array[#{assoc_class}], #{relation_class})" ],
|
212
|
+
},
|
213
|
+
})
|
214
|
+
end
|
215
|
+
|
216
|
+
def populate_generated_enum_methods
|
217
|
+
@model_class.defined_enums.each do |enum_name, enum_hash|
|
218
|
+
@generated_class_sigs["#{enum_name.pluralize}"] = { ret: "T::Hash[T.any(String, Symbol), Integer]"}
|
219
|
+
enum_hash.keys.each do |enum_val|
|
220
|
+
@generated_sigs["#{enum_val}?"] = { ret: "T::Boolean" }
|
221
|
+
@generated_scope_sigs["#{enum_val}"] = {
|
222
|
+
args: [ name: :args, arg_type: :rest, value_type: 'T.untyped' ],
|
223
|
+
ret: "#{@model_class.name}::Relation",
|
224
|
+
}
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def assoc_should_be_untyped?(reflection)
|
230
|
+
polymorphic_assoc?(reflection) || !Object.const_defined?(reflection.class_name)
|
231
|
+
end
|
232
|
+
|
233
|
+
def relation_should_be_untyped?(reflection)
|
234
|
+
# only type the relation we'll generate
|
235
|
+
assoc_should_be_untyped?(reflection) || !@available_classes.include?(reflection.class_name)
|
236
|
+
end
|
237
|
+
|
238
|
+
def polymorphic_assoc?(reflection)
|
239
|
+
reflection.through_reflection? ?
|
240
|
+
polymorphic_assoc?(reflection.source_reflection) :
|
241
|
+
reflection.polymorphic?
|
242
|
+
end
|
243
|
+
|
244
|
+
def draw_class_header
|
245
|
+
# We define a custom <ModelName>::Relation class so that it can be extended
|
246
|
+
# to contain custom scopes for each models
|
247
|
+
<<~MESSAGE
|
248
|
+
# This is an autogenerated file for dynamic methods in #{@model_class.name}
|
249
|
+
# Please rerun rake rails_rbi:models to regenerate.
|
250
|
+
# typed: strong
|
251
|
+
|
252
|
+
class #{@model_class.name}::Relation < ActiveRecord::Relation
|
253
|
+
include #{@model_class.name}::NamedScope
|
254
|
+
extend T::Generic
|
255
|
+
Elem = type_member(fixed: #{@model_class.name})
|
256
|
+
end
|
257
|
+
|
258
|
+
class #{@model_class.name} < #{@model_class.superclass}
|
259
|
+
extend T::Sig
|
260
|
+
extend T::Generic
|
261
|
+
extend #{@model_class.name}::NamedScope
|
262
|
+
Elem = type_template(fixed: #{@model_class.name})
|
263
|
+
MESSAGE
|
264
|
+
end
|
265
|
+
|
266
|
+
def draw_class_footer
|
267
|
+
"end"
|
268
|
+
end
|
269
|
+
|
270
|
+
def draw_named_scope_header
|
271
|
+
<<~MESSAGE
|
272
|
+
module #{@model_class.name}::NamedScope
|
273
|
+
extend T::Sig
|
274
|
+
MESSAGE
|
275
|
+
end
|
276
|
+
|
277
|
+
def draw_named_scope_footer
|
278
|
+
"end"
|
279
|
+
end
|
280
|
+
|
281
|
+
def generate_column_methods(buffer)
|
282
|
+
@columns_hash.each do |column_name, column_def|
|
283
|
+
buffer << draw_column_methods(column_name, column_def)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def type_for_column_def(column_def)
|
288
|
+
strict_type = RUBY_TO_SORBET_TYPE_MAPPING[column_def.type] || 'T.untyped'
|
289
|
+
if column_def.array?
|
290
|
+
strict_type = "T::Array[#{strict_type}]"
|
291
|
+
end
|
292
|
+
column_def.null ? "T.nilable(#{strict_type})" : strict_type
|
293
|
+
end
|
294
|
+
|
295
|
+
def is_method_autogenerated?(method_obj)
|
296
|
+
# check if this method is autogenerated or overridden
|
297
|
+
# Note: sometimes this is a module, sometimes it's an instance of a class
|
298
|
+
return false unless method_obj.source_location.present? # BaseObject
|
299
|
+
|
300
|
+
owner_name = method_obj.owner.to_s
|
301
|
+
source_file = method_obj.source_location[0]
|
302
|
+
|
303
|
+
(
|
304
|
+
[
|
305
|
+
"ActiveRecord::AttributeMethods::GeneratedAttributeMethods",
|
306
|
+
"GeneratedAssociationMethods",
|
307
|
+
"ActiveRecord::Querying",
|
308
|
+
"ActiveRecord::AttributeMethods::PrimaryKey",
|
309
|
+
].any? { |k| owner_name.include?(k) } ||
|
310
|
+
[
|
311
|
+
"lib/active_record/enum.rb",
|
312
|
+
"lib/active_record/scoping/named.rb",
|
313
|
+
].any? { |k| source_file.include?(k) }
|
314
|
+
)
|
315
|
+
end
|
316
|
+
|
317
|
+
def matched_signature?(method_obj, generated_method_def)
|
318
|
+
# use parameters reflection to find method arguments
|
319
|
+
actual_params = method_obj.parameters
|
320
|
+
expected_args = generated_method_def[:args] || []
|
321
|
+
expected_params = expected_args.map { |arg| [arg[:arg_type], arg[:name]] }
|
322
|
+
actual_params == expected_params
|
323
|
+
end
|
324
|
+
|
325
|
+
def generate_method_sig(method_name, generated_method_def, is_class_method)
|
326
|
+
# generated_method_def:
|
327
|
+
# {
|
328
|
+
# . ret: <return_type>
|
329
|
+
# args: [ name: :value, arg_type: :req, value_type: "T.any(T::Array[#{assoc_class}], ActiveRecord::Relation" ]
|
330
|
+
# }
|
331
|
+
#
|
332
|
+
# Generate something like this
|
333
|
+
#
|
334
|
+
# sig {returns(T.nilable(String))}
|
335
|
+
# .def email; end
|
336
|
+
# sig {params(record: T.nilable(String)).void}
|
337
|
+
# def email=(record); end
|
338
|
+
|
339
|
+
param_sig = ""
|
340
|
+
param_def = ""
|
341
|
+
if generated_method_def[:args]
|
342
|
+
sig_args_string = generated_method_def[:args].map { |arg_def|
|
343
|
+
"#{arg_def[:name]}: #{arg_def[:value_type]}"
|
344
|
+
}.join(", ")
|
345
|
+
param_sig = "params(#{sig_args_string})."
|
346
|
+
|
347
|
+
param_def = generated_method_def[:args].map { |arg_def|
|
348
|
+
prefix = ""
|
349
|
+
prefix = "*" if arg_def[:arg_type] == :rest
|
350
|
+
prefix = "**" if arg_def[:arg_type] == :keyrest
|
351
|
+
|
352
|
+
"#{prefix}#{arg_def[:name]}"
|
353
|
+
}.join(", ")
|
354
|
+
end
|
355
|
+
|
356
|
+
return_type = generated_method_def[:ret] ?
|
357
|
+
"returns(#{generated_method_def[:ret]})" :
|
358
|
+
"void"
|
359
|
+
|
360
|
+
prefix = is_class_method ? "self." : ""
|
361
|
+
|
362
|
+
<<~MESSAGE
|
363
|
+
sig { #{param_sig}#{return_type} }
|
364
|
+
def #{prefix}#{method_name}(#{param_def}); end
|
365
|
+
MESSAGE
|
366
|
+
end
|
367
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "rails"
|
2
|
+
require "sorbet-rails/custom_finder_methods"
|
3
|
+
|
4
|
+
class SorbetRails::Railtie < Rails::Railtie
|
5
|
+
railtie_name "sorbet-rails"
|
6
|
+
|
7
|
+
rake_tasks do
|
8
|
+
path = File.expand_path(__dir__)
|
9
|
+
Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
|
10
|
+
end
|
11
|
+
|
12
|
+
initializer "sorbet-rails.initialize" do
|
13
|
+
ActiveSupport.on_load(:active_record) do
|
14
|
+
ActiveRecord::Base.extend SorbetRails::CustomFinderMethods
|
15
|
+
ActiveRecord::Relation.include SorbetRails::CustomFinderMethods
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# typed: true
|
2
|
+
class RoutesRbiFormatter
|
3
|
+
def initialize
|
4
|
+
@buffer = []
|
5
|
+
end
|
6
|
+
|
7
|
+
def section_title(title)
|
8
|
+
@buffer << "\n# Section #{title}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def section(routes)
|
12
|
+
@buffer << draw_section(routes)
|
13
|
+
end
|
14
|
+
|
15
|
+
def header(routes)
|
16
|
+
end
|
17
|
+
|
18
|
+
def no_routes(routes, filter)
|
19
|
+
@buffer <<
|
20
|
+
if routes.none?
|
21
|
+
<<~MESSAGE
|
22
|
+
You do not have any routes defined!
|
23
|
+
Please add some routes in config/routes.rb.
|
24
|
+
MESSAGE
|
25
|
+
elsif filter.key?(:controller)
|
26
|
+
"No routes were found for this controller."
|
27
|
+
elsif filter.key?(:grep)
|
28
|
+
"No routes were found for this grep pattern."
|
29
|
+
end
|
30
|
+
|
31
|
+
@buffer << "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
|
32
|
+
end
|
33
|
+
|
34
|
+
def result
|
35
|
+
<<~MESSAGE
|
36
|
+
# This is an autogenerated file for routes helper methods
|
37
|
+
|
38
|
+
# typed: strong
|
39
|
+
class ActionController::Base
|
40
|
+
extend T::Sig
|
41
|
+
|
42
|
+
#{@buffer.join("\n").indent(2)}
|
43
|
+
end
|
44
|
+
MESSAGE
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def draw_section(routes)
|
49
|
+
routes.map do |r|
|
50
|
+
if r[:name].present?
|
51
|
+
<<~MESSAGE
|
52
|
+
# Sigs for route #{r[:path]}
|
53
|
+
sig { params(args: T.untyped, kwargs: T.untyped).returns(String) }
|
54
|
+
def #{r[:name]}_path(*args, **kwargs); end
|
55
|
+
sig { params(args: T.untyped, kwargs: T.untyped).returns(String) }
|
56
|
+
def #{r[:name]}_url(*args, **kwargs); end
|
57
|
+
MESSAGE
|
58
|
+
else
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
end.compact
|
62
|
+
end
|
63
|
+
end
|
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sorbet-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chan Zuckerberg Initiative
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-04-18 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: opensource@chanzuckerberg.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- Gemfile
|
20
|
+
- LICENSE
|
21
|
+
- README.md
|
22
|
+
- lib/sorbet-rails.rb
|
23
|
+
- lib/sorbet-rails/custom_finder_methods.rb
|
24
|
+
- lib/sorbet-rails/model_rbi_formatter.rb
|
25
|
+
- lib/sorbet-rails/railtie.rb
|
26
|
+
- lib/sorbet-rails/routes_rbi_formatter.rb
|
27
|
+
homepage: https://github.com/chanzuckerberg/sorbet-rails
|
28
|
+
licenses:
|
29
|
+
- MIT
|
30
|
+
metadata: {}
|
31
|
+
post_install_message:
|
32
|
+
rdoc_options: []
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
requirements: []
|
46
|
+
rubyforge_project:
|
47
|
+
rubygems_version: 2.6.14
|
48
|
+
signing_key:
|
49
|
+
specification_version: 4
|
50
|
+
summary: Set of tools to make Sorbet work with Rails seamlessly.
|
51
|
+
test_files: []
|