tapioca 0.5.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +114 -23
  3. data/lib/tapioca/cli.rb +188 -65
  4. data/lib/tapioca/compilers/dsl/active_record_associations.rb +94 -8
  5. data/lib/tapioca/compilers/dsl/active_record_columns.rb +5 -4
  6. data/lib/tapioca/compilers/dsl/active_record_enum.rb +1 -1
  7. data/lib/tapioca/compilers/dsl/active_record_relations.rb +703 -0
  8. data/lib/tapioca/compilers/dsl/active_record_scope.rb +43 -13
  9. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +39 -33
  10. data/lib/tapioca/compilers/dsl/base.rb +26 -42
  11. data/lib/tapioca/compilers/dsl/extensions/frozen_record.rb +29 -0
  12. data/lib/tapioca/compilers/dsl/frozen_record.rb +37 -0
  13. data/lib/tapioca/compilers/dsl/helper/active_record_constants.rb +27 -0
  14. data/lib/tapioca/compilers/dsl/identity_cache.rb +0 -1
  15. data/lib/tapioca/compilers/dsl/param_helper.rb +52 -0
  16. data/lib/tapioca/compilers/dsl/rails_generators.rb +120 -0
  17. data/lib/tapioca/compilers/dsl_compiler.rb +34 -6
  18. data/lib/tapioca/compilers/sorbet.rb +2 -0
  19. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +51 -48
  20. data/lib/tapioca/executor.rb +79 -0
  21. data/lib/tapioca/gemfile.rb +28 -4
  22. data/lib/tapioca/generators/base.rb +11 -18
  23. data/lib/tapioca/generators/dsl.rb +33 -38
  24. data/lib/tapioca/generators/gem.rb +64 -34
  25. data/lib/tapioca/generators/init.rb +41 -16
  26. data/lib/tapioca/generators/todo.rb +6 -6
  27. data/lib/tapioca/helpers/cli_helper.rb +26 -0
  28. data/lib/tapioca/helpers/config_helper.rb +84 -0
  29. data/lib/tapioca/helpers/test/content.rb +51 -0
  30. data/lib/tapioca/helpers/test/isolation.rb +125 -0
  31. data/lib/tapioca/helpers/test/template.rb +34 -0
  32. data/lib/tapioca/internal.rb +3 -2
  33. data/lib/tapioca/rbi_ext/model.rb +13 -10
  34. data/lib/tapioca/reflection.rb +13 -0
  35. data/lib/tapioca/trackers/autoload.rb +70 -0
  36. data/lib/tapioca/trackers/constant_definition.rb +42 -0
  37. data/lib/tapioca/trackers/mixin.rb +78 -0
  38. data/lib/tapioca/trackers.rb +14 -0
  39. data/lib/tapioca/version.rb +1 -1
  40. data/lib/tapioca.rb +28 -2
  41. metadata +37 -13
  42. data/lib/tapioca/config.rb +0 -45
  43. data/lib/tapioca/config_builder.rb +0 -73
  44. data/lib/tapioca/constant_locator.rb +0 -34
@@ -0,0 +1,703 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "active_record"
6
+ rescue LoadError
7
+ return
8
+ end
9
+
10
+ require "tapioca/compilers/dsl/helper/active_record_constants"
11
+
12
+ module Tapioca
13
+ module Compilers
14
+ module Dsl
15
+ # `Tapioca::Compilers::Dsl::ActiveRecordRelations` decorates RBI files for subclasses of
16
+ # `ActiveRecord::Base` and adds
17
+ # [relation](http://api.rubyonrails.org/classes/ActiveRecord/Relation.html),
18
+ # [collection proxy](https://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html),
19
+ # [query](http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html),
20
+ # [spawn](http://api.rubyonrails.org/classes/ActiveRecord/SpawnMethods.html),
21
+ # [finder](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html), and
22
+ # [calculation](http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html) methods.
23
+ #
24
+ # The generator defines 3 (synthetic) modules and 3 (synthetic) classes to represent relations properly.
25
+ #
26
+ # For a given model `Model`, we generate the following classes:
27
+ #
28
+ # 1. A `Model::PrivateRelation` that subclasses `ActiveRecord::Relation`. This synthetic class represents
29
+ # a relation on `Model` whose methods which return a relation always return a `Model::PrivateRelation` instance.
30
+ #
31
+ # 2. `Model::PrivateAssocationRelation` that subclasses `ActiveRecord::AssociationRelation`. This synthetic
32
+ # class represents a relation on a singular association of type `Model` (e.g. `foo.model`) whose methods which
33
+ # return a relation will always return a `Model::PrivateAssocationRelation` instance. The difference between this
34
+ # class and the previous one is mainly that an association relation also keeps track of the resource association
35
+ # for this relation.
36
+ #
37
+ # 3. `Model::PrivateCollectionProxy` that subclasses from `ActiveRecord::Associations::CollectionProxy`.
38
+ # This synthetic class represents a relation on a plural association of type `Model` (e.g. `foo.models`)
39
+ # whose methods which return a relation will always return a `Model::PrivateAssocationRelation` instance.
40
+ # This class represents a collection of `Model` instances with some extra methods to `build`, `create`,
41
+ # etc new `Model` instances in the collection.
42
+ #
43
+ # and the following modules:
44
+ #
45
+ # 1. `Model::GeneratedRelationMethods` holds all the relation methods with the return type of
46
+ # `Model::PrivateRelation`. For example, calling `all` on the `Model` class or an instance of
47
+ # `Model::PrivateRelation` class will always return a `Model::PrivateRelation` instance, thus the
48
+ # signature of `all` is defined with that return type in this module.
49
+ #
50
+ # 2. `Model::GeneratedAssociationRelationMethods` holds all the relation methods with the return type
51
+ # of `Model::PrivateAssociationRelation`. For example, calling `all` on an instance of
52
+ # `Model::PrivateAssociationRelation` or an instance of `Model::PrivateCollectionProxy` class will
53
+ # always return a `Model::PrivateAssociationRelation` instance, thus the signature of `all` is defined
54
+ # with that return type in this module.
55
+ #
56
+ # 3. `Model::CommonRelationMethods` holds all the relation methods that do not depend on the type of
57
+ # relation in their return type. For example, `find_by!` will always return the same type (a `Model`
58
+ # instance), regardless of what kind of relation it is called on, and so belongs in this module.
59
+ # This module is used to reduce the replication of methods between the previous two modules.
60
+ #
61
+ # Additionally, the actual `Model` class extends both `Model::CommonRelationMethods` and
62
+ # `Model::PrivateRelation` modules, so that, for example, `find_by` and `all` can be chained off of the
63
+ # `Model` class.
64
+ #
65
+ # **CAUTION**: The generated relation classes are named `PrivateXXX` intentionally to reflect the fact
66
+ # that they represent private subconstants of the Active Record model. As such, these types do not
67
+ # exist at runtime, and their counterparts that do exist at runtime are marked `private_constant` anyway.
68
+ # For that reason, these types cannot be used in user code or in `sig`s inside Ruby files, since that will
69
+ # make the runtime checks fail.
70
+ #
71
+ # For example, with the following `ActiveRecord::Base` subclass:
72
+ #
73
+ # ~~~rb
74
+ # class Post < ApplicationRecord
75
+ # end
76
+ # ~~~
77
+ #
78
+ # this generator will produce the RBI file `post.rbi` with the following content:
79
+ # ~~~rbi
80
+ # # post.rbi
81
+ # # typed: true
82
+ #
83
+ # class Post
84
+ # extend CommonRelationMethods
85
+ # extend GeneratedRelationMethods
86
+ #
87
+ # module CommonRelationMethods
88
+ # sig { params(block: T.nilable(T.proc.params(record: ::Post).returns(T.untyped))).returns(T::Boolean) }
89
+ # def any?(&block); end
90
+ #
91
+ # # ...
92
+ # end
93
+ #
94
+ # module GeneratedAssociationRelationMethods
95
+ # sig { returns(PrivateAssociationRelation) }
96
+ # def all; end
97
+ #
98
+ # # ...
99
+ #
100
+ # sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
101
+ # def where(*args, &blk); end
102
+ # end
103
+ #
104
+ # module GeneratedRelationMethods
105
+ # sig { returns(PrivateRelation) }
106
+ # def all; end
107
+ #
108
+ # # ...
109
+ #
110
+ # sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
111
+ # def where(*args, &blk); end
112
+ # end
113
+ #
114
+ # class PrivateAssociationRelation < ::ActiveRecord::AssociationRelation
115
+ # include CommonRelationMethods
116
+ # include GeneratedAssociationRelationMethods
117
+ #
118
+ # sig { returns(T::Array[::Post]) }
119
+ # def to_ary; end
120
+ #
121
+ # Elem = type_member(fixed: ::Post)
122
+ # end
123
+ #
124
+ # class PrivateCollectionProxy < ::ActiveRecord::Associations::CollectionProxy
125
+ # include CommonRelationMethods
126
+ # include GeneratedAssociationRelationMethods
127
+ #
128
+ # sig do
129
+ # params(records: T.any(::Post, T::Array[::Post], T::Array[PrivateCollectionProxy]))
130
+ # .returns(PrivateCollectionProxy)
131
+ # end
132
+ # def <<(*records); end
133
+ #
134
+ # # ...
135
+ # end
136
+ #
137
+ # class PrivateRelation < ::ActiveRecord::Relation
138
+ # include CommonRelationMethods
139
+ # include GeneratedRelationMethods
140
+ #
141
+ # sig { returns(T::Array[::Post]) }
142
+ # def to_ary; end
143
+ #
144
+ # Elem = type_member(fixed: ::Post)
145
+ # end
146
+ # end
147
+ # ~~~
148
+ class ActiveRecordRelations < Base
149
+ extend T::Sig
150
+
151
+ sig do
152
+ override
153
+ .params(root: RBI::Tree, constant: T.class_of(::ActiveRecord::Base))
154
+ .void
155
+ end
156
+ def decorate(root, constant)
157
+ root.create_path(constant) do |model|
158
+ constant_name = T.must(qualified_name_of(constant))
159
+ RelationGenerator.new(model, constant_name).generate
160
+ end
161
+ end
162
+
163
+ sig { override.returns(T::Enumerable[Module]) }
164
+ def gather_constants
165
+ ActiveRecord::Base.descendants.reject(&:abstract_class?)
166
+ end
167
+
168
+ class RelationGenerator
169
+ extend T::Sig
170
+ include ParamHelper
171
+ include Reflection
172
+ include Helper::ActiveRecordConstants
173
+
174
+ sig do
175
+ params(
176
+ model: RBI::Scope,
177
+ constant_name: String
178
+ ).void
179
+ end
180
+ def initialize(model, constant_name)
181
+ @model = model
182
+ @constant_name = constant_name
183
+ @relation_methods_module = T.let(
184
+ model.create_module(RelationMethodsModuleName),
185
+ RBI::Scope
186
+ )
187
+ @association_relation_methods_module = T.let(
188
+ model.create_module(AssociationRelationMethodsModuleName),
189
+ RBI::Scope
190
+ )
191
+ @common_relation_methods_module = T.let(
192
+ model.create_module(CommonRelationMethodsModuleName),
193
+ RBI::Scope
194
+ )
195
+ end
196
+
197
+ sig { void }
198
+ def generate
199
+ create_classes_and_includes
200
+ create_common_methods
201
+ create_relation_methods
202
+ create_association_relation_methods
203
+ end
204
+
205
+ ASSOCIATION_METHODS = T.let(
206
+ ::ActiveRecord::AssociationRelation.instance_methods -
207
+ ::ActiveRecord::Relation.instance_methods,
208
+ T::Array[Symbol]
209
+ )
210
+ COLLECTION_PROXY_METHODS = T.let(
211
+ ::ActiveRecord::Associations::CollectionProxy.instance_methods -
212
+ ::ActiveRecord::AssociationRelation.instance_methods,
213
+ T::Array[Symbol]
214
+ )
215
+
216
+ QUERY_METHODS = T.let(begin
217
+ # Grab all Query methods
218
+ query_methods = ActiveRecord::QueryMethods.instance_methods(false)
219
+ # Grab all Spawn methods
220
+ query_methods |= ActiveRecord::SpawnMethods.instance_methods(false)
221
+ # Remove the ones we know are private API
222
+ query_methods -= [:arel, :build_subquery, :construct_join_dependency, :extensions, :spawn]
223
+ # Remove "where" which needs a custom return type for WhereChains
224
+ query_methods -= [:where]
225
+ # Remove the methods that ...
226
+ query_methods
227
+ .grep_v(/_clause$/) # end with "_clause"
228
+ .grep_v(/_values?$/) # end with "_value" or "_values"
229
+ .grep_v(/=$/) # end with "=""
230
+ .grep_v(/(?<!uniq)!$/) # end with "!" except for "uniq!"
231
+ end, T::Array[Symbol])
232
+ WHERE_CHAIN_QUERY_METHODS = T.let(
233
+ ActiveRecord::QueryMethods::WhereChain.instance_methods(false),
234
+ T::Array[Symbol]
235
+ )
236
+ FINDER_METHODS = T.let(ActiveRecord::FinderMethods.instance_methods(false), T::Array[Symbol])
237
+ CALCULATION_METHODS = T.let(ActiveRecord::Calculations.instance_methods(false), T::Array[Symbol])
238
+ ENUMERABLE_QUERY_METHODS = T.let([:any?, :many?, :none?, :one?], T::Array[Symbol])
239
+ FIND_OR_CREATE_METHODS = T.let(
240
+ [:find_or_create_by, :find_or_create_by!, :find_or_initialize_by, :create_or_find_by, :create_or_find_by!],
241
+ T::Array[Symbol]
242
+ )
243
+ BUILDER_METHODS = T.let([:new, :build, :create, :create!], T::Array[Symbol])
244
+
245
+ private
246
+
247
+ sig { returns(RBI::Scope) }
248
+ attr_reader :model
249
+
250
+ sig { returns(String) }
251
+ attr_reader :constant_name
252
+
253
+ sig { void }
254
+ def create_classes_and_includes
255
+ model.create_extend(CommonRelationMethodsModuleName)
256
+ # The model always extends the generated relation module
257
+ model.create_extend(RelationMethodsModuleName)
258
+
259
+ # This feature is only available in versions of Sorbet with special support for
260
+ # handling `NilClass` returns from `to_ary`. We should not be typing `to_ary` like
261
+ # this for older versions since it will make all flatten operations be
262
+ # `T::Array[NilClass]`, otherwise.
263
+ if Tapioca::Compilers::Sorbet.supports?(:to_ary_nil_support)
264
+ # Type the `to_ary` method as returning `NilClass` so that flatten stops recursing
265
+ # See https://github.com/sorbet/sorbet/pull/4706 for details
266
+ model.create_method("to_ary", return_type: "NilClass", visibility: RBI::Private.new)
267
+ end
268
+
269
+ create_relation_class
270
+ create_association_relation_class
271
+ create_collection_proxy_class
272
+ end
273
+
274
+ sig { void }
275
+ def create_relation_class
276
+ superclass = "::ActiveRecord::Relation"
277
+
278
+ # The relation subclass includes the generated relation module
279
+ model.create_class(RelationClassName, superclass_name: superclass) do |klass|
280
+ klass.create_include(CommonRelationMethodsModuleName)
281
+ klass.create_include(RelationMethodsModuleName)
282
+ klass.create_constant("Elem", value: "type_member(fixed: #{constant_name})")
283
+
284
+ klass.create_method("to_ary", return_type: "T::Array[#{constant_name}]")
285
+ end
286
+
287
+ create_relation_where_chain_class
288
+ end
289
+
290
+ sig { void }
291
+ def create_association_relation_class
292
+ superclass = "::ActiveRecord::AssociationRelation"
293
+
294
+ # Association subclasses include the generated association relation module
295
+ model.create_class(AssociationRelationClassName, superclass_name: superclass) do |klass|
296
+ klass.create_include(CommonRelationMethodsModuleName)
297
+ klass.create_include(AssociationRelationMethodsModuleName)
298
+ klass.create_constant("Elem", value: "type_member(fixed: #{constant_name})")
299
+
300
+ klass.create_method("to_ary", return_type: "T::Array[#{constant_name}]")
301
+ end
302
+
303
+ create_association_relation_where_chain_class
304
+ end
305
+
306
+ sig { void }
307
+ def create_relation_where_chain_class
308
+ model.create_class(RelationWhereChainClassName, superclass_name: RelationClassName) do |klass|
309
+ create_where_chain_methods(klass, RelationClassName)
310
+ klass.create_constant("Elem", value: "type_member(fixed: #{constant_name})")
311
+ end
312
+ end
313
+
314
+ sig { void }
315
+ def create_association_relation_where_chain_class
316
+ model.create_class(
317
+ AssociationRelationWhereChainClassName,
318
+ superclass_name: AssociationRelationClassName
319
+ ) do |klass|
320
+ create_where_chain_methods(klass, AssociationRelationClassName)
321
+ klass.create_constant("Elem", value: "type_member(fixed: #{constant_name})")
322
+ end
323
+ end
324
+
325
+ sig { params(klass: RBI::Scope, return_type: String).void }
326
+ def create_where_chain_methods(klass, return_type)
327
+ WHERE_CHAIN_QUERY_METHODS.each do |method_name|
328
+ case method_name
329
+ when :not
330
+ klass.create_method(
331
+ method_name.to_s,
332
+ parameters: [
333
+ create_param("opts", type: "T.untyped"),
334
+ create_rest_param("rest", type: "T.untyped"),
335
+ ],
336
+ return_type: return_type
337
+ )
338
+ when :associated, :missing
339
+ klass.create_method(
340
+ method_name.to_s,
341
+ parameters: [
342
+ create_rest_param("args", type: "T.untyped"),
343
+ ],
344
+ return_type: return_type
345
+ )
346
+ end
347
+ end
348
+ end
349
+
350
+ sig { void }
351
+ def create_collection_proxy_class
352
+ superclass = "::ActiveRecord::Associations::CollectionProxy"
353
+
354
+ # The relation subclass includes the generated association relation module
355
+ model.create_class(AssociationsCollectionProxyClassName, superclass_name: superclass) do |klass|
356
+ klass.create_include(CommonRelationMethodsModuleName)
357
+ klass.create_include(AssociationRelationMethodsModuleName)
358
+ klass.create_constant("Elem", value: "type_member(fixed: #{constant_name})")
359
+
360
+ klass.create_method("to_ary", return_type: "T::Array[#{constant_name}]")
361
+ create_collection_proxy_methods(klass)
362
+ end
363
+ end
364
+
365
+ sig { params(klass: RBI::Scope).void }
366
+ def create_collection_proxy_methods(klass)
367
+ # For these cases, it is valid to pass:
368
+ # - a model instance, thus `Model`
369
+ # - a model collection which can be:
370
+ # - an array of models, thus `T::Enumerable[Model]`
371
+ # - an association relation of a model, thus `T::Enumerable[Model]`
372
+ # - a collection proxy of a model, thus, again, a `T::Enumerable[Model]`
373
+ # - a collection of relations or collection proxies, thus `T::Enumerable[T::Enumerable[Model]]`
374
+ # - or, any mix of the above, thus `T::Enumerable[T.any(Model, T::Enumerable[Model])]`
375
+ # which altogether gives us:
376
+ # `T.any(Model, T::Enumerable[T.any(Model, T::Enumerable[Model])])`
377
+ model_collection =
378
+ "T.any(#{constant_name}, T::Enumerable[T.any(#{constant_name}, T::Enumerable[#{constant_name}])])"
379
+
380
+ # For these cases, it is valid to pass the above kind of things, but also:
381
+ # - a model identifier, which can be:
382
+ # - a numeric id, thus `Integer`
383
+ # - a string id, thus `String`
384
+ # - a collection of identifiers
385
+ # - a collection of identifiers, thus `T::Enumerable[T.any(Integer, String)]`
386
+ # which, coupled with the above case, gives us:
387
+ # `T.any(Model, Integer, String, T::Enumerable[T.any(Model, Integer, String, T::Enumerable[Model])])`
388
+ model_or_id_collection =
389
+ "T.any(#{constant_name}, Integer, String" \
390
+ ", T::Enumerable[T.any(#{constant_name}, Integer, String, T::Enumerable[#{constant_name}])])"
391
+
392
+ COLLECTION_PROXY_METHODS.each do |method_name|
393
+ case method_name
394
+ when :<<, :append, :concat, :prepend, :push
395
+ klass.create_method(
396
+ method_name.to_s,
397
+ parameters: [
398
+ create_rest_param("records", type: model_collection),
399
+ ],
400
+ return_type: AssociationsCollectionProxyClassName
401
+ )
402
+ when :clear
403
+ klass.create_method(
404
+ method_name.to_s,
405
+ return_type: AssociationsCollectionProxyClassName
406
+ )
407
+ when :delete, :destroy
408
+ klass.create_method(
409
+ method_name.to_s,
410
+ parameters: [
411
+ create_rest_param("records", type: model_or_id_collection),
412
+ ],
413
+ return_type: "T::Array[#{constant_name}]"
414
+ )
415
+ when :load_target
416
+ klass.create_method(
417
+ method_name.to_s,
418
+ return_type: "T::Array[#{constant_name}]"
419
+ )
420
+ when :replace
421
+ klass.create_method(
422
+ method_name.to_s,
423
+ parameters: [
424
+ create_param("other_array", type: model_collection),
425
+ ],
426
+ return_type: "T::Array[#{constant_name}]"
427
+ )
428
+ when :reset_scope
429
+ # skip
430
+ when :scope
431
+ klass.create_method(
432
+ method_name.to_s,
433
+ return_type: AssociationRelationClassName
434
+ )
435
+ when :target
436
+ klass.create_method(
437
+ method_name.to_s,
438
+ return_type: "T::Array[#{constant_name}]"
439
+ )
440
+ end
441
+ end
442
+ end
443
+
444
+ sig { void }
445
+ def create_relation_methods
446
+ create_relation_method("all")
447
+ create_relation_method(
448
+ "where",
449
+ parameters: [
450
+ create_rest_param("args", type: "T.untyped"),
451
+ create_block_param("blk", type: "T.untyped"),
452
+ ],
453
+ relation_return_type: RelationWhereChainClassName,
454
+ association_return_type: AssociationRelationWhereChainClassName,
455
+ )
456
+
457
+ QUERY_METHODS.each do |method_name|
458
+ create_relation_method(
459
+ method_name,
460
+ parameters: [
461
+ create_rest_param("args", type: "T.untyped"),
462
+ create_block_param("blk", type: "T.untyped"),
463
+ ]
464
+ )
465
+ end
466
+ end
467
+
468
+ sig { void }
469
+ def create_association_relation_methods
470
+ returning_type = "T.nilable(T.any(T::Array[Symbol], FalseClass))"
471
+ unique_by_type = "T.nilable(T.any(T::Array[Symbol], Symbol))"
472
+
473
+ ASSOCIATION_METHODS.each do |method_name|
474
+ case method_name
475
+ when :insert_all, :insert_all!, :upsert_all
476
+ parameters = [
477
+ create_param("attributes", type: "T::Array[Hash]"),
478
+ create_kw_opt_param("returning", type: returning_type, default: "nil"),
479
+ ]
480
+
481
+ # Bang methods don't have the `unique_by` parameter
482
+ unless method_name.end_with?("!")
483
+ parameters << create_kw_opt_param("unique_by", type: unique_by_type, default: "nil")
484
+ end
485
+
486
+ @association_relation_methods_module.create_method(
487
+ method_name.to_s,
488
+ parameters: parameters,
489
+ return_type: "ActiveRecord::Result"
490
+ )
491
+ when :insert, :insert!, :upsert
492
+ parameters = [
493
+ create_param("attributes", type: "Hash"),
494
+ create_kw_opt_param("returning", type: returning_type, default: "nil"),
495
+ ]
496
+
497
+ # Bang methods don't have the `unique_by` parameter
498
+ unless method_name.end_with?("!")
499
+ parameters << create_kw_opt_param("unique_by", type: unique_by_type, default: "nil")
500
+ end
501
+
502
+ @association_relation_methods_module.create_method(
503
+ method_name.to_s,
504
+ parameters: parameters,
505
+ return_type: "ActiveRecord::Result"
506
+ )
507
+ when :proxy_association
508
+ # skip - private method
509
+ end
510
+ end
511
+ end
512
+
513
+ sig { void }
514
+ def create_common_methods
515
+ create_common_method("destroy_all", return_type: "T::Array[#{constant_name}]")
516
+
517
+ FINDER_METHODS.each do |method_name|
518
+ case method_name
519
+ when :exists?
520
+ create_common_method(
521
+ "exists?",
522
+ parameters: [
523
+ create_opt_param("conditions", type: "T.untyped", default: ":none"),
524
+ ],
525
+ return_type: "T::Boolean"
526
+ )
527
+ when :include?, :member?
528
+ create_common_method(
529
+ method_name,
530
+ parameters: [
531
+ create_param("record", type: "T.untyped"),
532
+ ],
533
+ return_type: "T::Boolean"
534
+ )
535
+ when :find, :find_by!
536
+ create_common_method(
537
+ "find",
538
+ parameters: [
539
+ create_rest_param("args", type: "T.untyped"),
540
+ ],
541
+ return_type: "T.untyped"
542
+ )
543
+ when :find_by
544
+ create_common_method(
545
+ "find_by",
546
+ parameters: [
547
+ create_rest_param("args", type: "T.untyped"),
548
+ ],
549
+ return_type: "T.nilable(#{constant_name})"
550
+ )
551
+ when :first, :last, :take
552
+ create_common_method(
553
+ method_name,
554
+ parameters: [
555
+ create_opt_param("limit", type: "T.untyped", default: "nil"),
556
+ ],
557
+ return_type: "T.untyped"
558
+ )
559
+ when :raise_record_not_found_exception!
560
+ # skip
561
+ else
562
+ return_type = if method_name.end_with?("!")
563
+ constant_name
564
+ else
565
+ "T.nilable(#{constant_name})"
566
+ end
567
+
568
+ create_common_method(
569
+ method_name,
570
+ return_type: return_type
571
+ )
572
+ end
573
+ end
574
+
575
+ CALCULATION_METHODS.each do |method_name|
576
+ case method_name
577
+ when :average, :maximum, :minimum
578
+ create_common_method(
579
+ method_name,
580
+ parameters: [
581
+ create_param("column_name", type: "T.any(String, Symbol)"),
582
+ ],
583
+ return_type: "T.untyped"
584
+ )
585
+ when :calculate
586
+ create_common_method(
587
+ "calculate",
588
+ parameters: [
589
+ create_param("operation", type: "Symbol"),
590
+ create_param("column_name", type: "T.any(String, Symbol)"),
591
+ ],
592
+ return_type: "T.untyped"
593
+ )
594
+ when :count
595
+ create_common_method(
596
+ "count",
597
+ parameters: [
598
+ create_opt_param("column_name", type: "T.untyped", default: "nil"),
599
+ ],
600
+ return_type: "T.untyped"
601
+ )
602
+ when :ids
603
+ create_common_method("ids", return_type: "Array")
604
+ when :pick, :pluck
605
+ create_common_method(
606
+ method_name,
607
+ parameters: [
608
+ create_rest_param("column_names", type: "T.untyped"),
609
+ ],
610
+ return_type: "T.untyped"
611
+ )
612
+ when :sum
613
+ create_common_method(
614
+ "sum",
615
+ parameters: [
616
+ create_opt_param("column_name", type: "T.nilable(T.any(String, Symbol))", default: "nil"),
617
+ create_block_param("block", type: "T.nilable(T.proc.params(record: T.untyped).returns(T.untyped))"),
618
+ ],
619
+ return_type: "T.untyped"
620
+ )
621
+ end
622
+ end
623
+
624
+ ENUMERABLE_QUERY_METHODS.each do |method_name|
625
+ block_type = "T.nilable(T.proc.params(record: #{constant_name}).returns(T.untyped))"
626
+ create_common_method(
627
+ method_name,
628
+ parameters: [
629
+ create_block_param("block", type: block_type),
630
+ ],
631
+ return_type: "T::Boolean"
632
+ )
633
+ end
634
+
635
+ FIND_OR_CREATE_METHODS.each do |method_name|
636
+ block_type = "T.nilable(T.proc.params(object: #{constant_name}).void)"
637
+ create_common_method(
638
+ method_name,
639
+ parameters: [
640
+ create_param("attributes", type: "T.untyped"),
641
+ create_block_param("block", type: block_type),
642
+ ],
643
+ return_type: constant_name
644
+ )
645
+ end
646
+
647
+ BUILDER_METHODS.each do |method_name|
648
+ create_common_method(
649
+ method_name,
650
+ parameters: [
651
+ create_opt_param("attributes", type: "T.untyped", default: "nil"),
652
+ create_block_param("block", type: "T.nilable(T.proc.params(object: #{constant_name}).void)"),
653
+ ],
654
+ return_type: constant_name
655
+ )
656
+ end
657
+ end
658
+
659
+ sig do
660
+ params(
661
+ name: T.any(Symbol, String),
662
+ parameters: T::Array[RBI::TypedParam],
663
+ return_type: T.nilable(String)
664
+ ).void
665
+ end
666
+ def create_common_method(name, parameters: [], return_type: nil)
667
+ @common_relation_methods_module.create_method(
668
+ name.to_s,
669
+ parameters: parameters,
670
+ return_type: return_type || "void"
671
+ )
672
+ end
673
+
674
+ sig do
675
+ params(
676
+ name: T.any(Symbol, String),
677
+ parameters: T::Array[RBI::TypedParam],
678
+ relation_return_type: String,
679
+ association_return_type: String,
680
+ ).void
681
+ end
682
+ def create_relation_method(
683
+ name,
684
+ parameters: [],
685
+ relation_return_type: RelationClassName,
686
+ association_return_type: AssociationRelationClassName
687
+ )
688
+ @relation_methods_module.create_method(
689
+ name.to_s,
690
+ parameters: parameters,
691
+ return_type: relation_return_type
692
+ )
693
+ @association_relation_methods_module.create_method(
694
+ name.to_s,
695
+ parameters: parameters,
696
+ return_type: association_return_type
697
+ )
698
+ end
699
+ end
700
+ end
701
+ end
702
+ end
703
+ end