tapioca 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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