tapioca 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +25 -1
  3. data/README.md +12 -0
  4. data/Rakefile +15 -4
  5. data/lib/tapioca.rb +2 -0
  6. data/lib/tapioca/cli.rb +24 -2
  7. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +129 -0
  8. data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
  9. data/lib/tapioca/compilers/dsl/active_record_associations.rb +285 -0
  10. data/lib/tapioca/compilers/dsl/active_record_columns.rb +379 -0
  11. data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
  12. data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +213 -0
  13. data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
  14. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +170 -0
  15. data/lib/tapioca/compilers/dsl/active_resource.rb +140 -0
  16. data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +126 -0
  17. data/lib/tapioca/compilers/dsl/base.rb +163 -0
  18. data/lib/tapioca/compilers/dsl/frozen_record.rb +96 -0
  19. data/lib/tapioca/compilers/dsl/protobuf.rb +144 -0
  20. data/lib/tapioca/compilers/dsl/smart_properties.rb +173 -0
  21. data/lib/tapioca/compilers/dsl/state_machines.rb +378 -0
  22. data/lib/tapioca/compilers/dsl/url_helpers.rb +83 -0
  23. data/lib/tapioca/compilers/dsl_compiler.rb +121 -0
  24. data/lib/tapioca/compilers/requires_compiler.rb +67 -0
  25. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +141 -24
  26. data/lib/tapioca/config.rb +11 -6
  27. data/lib/tapioca/config_builder.rb +19 -9
  28. data/lib/tapioca/constant_locator.rb +1 -0
  29. data/lib/tapioca/core_ext/class.rb +23 -0
  30. data/lib/tapioca/generator.rb +187 -21
  31. data/lib/tapioca/loader.rb +20 -9
  32. data/lib/tapioca/sorbet_config_parser.rb +77 -0
  33. data/lib/tapioca/version.rb +1 -1
  34. metadata +29 -51
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6aedd5a0415cca7e554021e114401254561649803acceed6caaa2cb24b9b4c8c
4
- data.tar.gz: 376290c5b3cad6f082e9fc186cbb2ed32a9bb239c5c37bc8f9f464c78a2d951e
3
+ metadata.gz: 91a976cddc1429c3b08568b39d8fae5fe8d4d54a8b8b2b04deebb75f475987e8
4
+ data.tar.gz: 45074066696bd5c838ab09523980fd9c148faeb1b9f6ee800e8752e636edbd1a
5
5
  SHA512:
6
- metadata.gz: e65c8822f8312788075a84f24d5b9c95221bba2ced2dd877f150a377696fd70eff96bc936fdd3cf482fa6d4ac1b2d86a0201ed91c386c1ee1ce0bd19e8defd9c
7
- data.tar.gz: bace8a6fca5ca72789c5fdfa121fb7b30bf415e9f04d6c715f43237f4d022c4507a6d6182161c4928a7258f9759c957a22c2559cc73ebc56d093f5864dcd872f
6
+ metadata.gz: 1646cf3ec4957442bf0895a636bfc684e00f0d88e2252502d119fd3f4b4073e41b773ce0cbe8f725766d0c82bae7f67b92381a32a123b6fa2067ddd6a9e2c8d2
7
+ data.tar.gz: 33f8dae8437b44c2bb2e7d4a1bb483adc11c0ae58322b62fa97a879698ba3d9abab4f198d38b7a8433e57f32f64df5b39ce1b5d8a13ebe37b011dae2d179d926
data/Gemfile CHANGED
@@ -7,5 +7,29 @@ gemspec
7
7
  gem 'rubocop-shopify', require: false
8
8
 
9
9
  group(:deployment, :development) do
10
- gem("rake", "~> 12.3")
10
+ gem("rake")
11
+ end
12
+
13
+ gem("bundler", "~> 1.17")
14
+ gem("pry-byebug")
15
+ gem("minitest")
16
+ gem("minitest-hooks")
17
+ gem("minitest-fork_executor")
18
+ gem("minitest-reporters")
19
+ gem("sorbet")
20
+
21
+ group(:development, :test) do
22
+ gem("smart_properties", ">= 1.15.0", require: false)
23
+ gem("frozen_record", ">= 0.17", require: false)
24
+ gem("sprockets", "~> 3.7", require: false)
25
+ gem("rails", "~> 5.2", require: false)
26
+ gem("state_machines", "~> 0.5.0", require: false)
27
+ gem("activerecord-typedstore", "~> 1.3", require: false)
28
+ gem("sqlite3")
29
+ gem("identity_cache", "~> 1.0", require: false)
30
+ gem('cityhash', git: 'https://github.com/csfrancis/cityhash.git',
31
+ ref: '3cfc7d01f333c01811d5e834f1495eaa29f87c36', require: false)
32
+ gem("activemodel-serializers-xml", "~> 1.0", require: false)
33
+ gem("activeresource", "~> 5.1", require: false)
34
+ gem("google-protobuf", "~>3.12.0", require: false)
11
35
  end
data/README.md CHANGED
@@ -31,6 +31,13 @@ In order to make sure that `tapioca` can reflect on that type, we need to add th
31
31
 
32
32
  If you ever run into a case, where you add a gem or update the version of a gem and run `tapioca sync` but don't have some types you expect in the generated gem RBI files, you will need to make sure you have added the necessary requires to the `sorbet/tapioca/require.rb` file.
33
33
 
34
+ You can use the command `tapioca require` to auto-populate the `sorbet/tapioca/require.rb` file with all the requires found
35
+ in your application. Once the file generated, you should review it, remove all unnecessary requires and commit it.
36
+
37
+ ## How does tapioca compare to "srb rbi gems" ?
38
+
39
+ [Please see the detailed answer on our wiki](https://github.com/Shopify/tapioca/wiki/How-does-tapioca-compare-to-%22srb-rbi-gems%22-%3F)
40
+
34
41
  ## Installation
35
42
 
36
43
  Add this line to your application's `Gemfile`:
@@ -49,6 +56,7 @@ Commands:
49
56
  tapioca generate [gem...] # generate RBIs from gems
50
57
  tapioca help [COMMAND] # Describe available commands or one specific command
51
58
  tapioca init # initializes folder structure
59
+ tapioca require # generate the list of files to be required by tapioca
52
60
  tapioca sync # sync RBIs to Gemfile
53
61
  tapioca todo # generate the list of unresolved constants
54
62
 
@@ -95,6 +103,10 @@ This will generate the file `sorbet/rbi/todo.rbi` defining all unresolved consta
95
103
  - `--generate-command [command]`: The command to run to regenerate RBI files (used in header comment of the RBI files), defaults to the current command.
96
104
  - `--typed-overrides [gem:level]`: Overrides typed sigils for generated gem RBIs for gem `gem` to level `level` (`level` can be one of `ignore`, `false`, `true`, `strict`, or `strong`, see [the Sorbet docs](https://sorbet.org/docs/static#file-level-granularity-strictness-levels) for more details).
97
105
 
106
+ ### Strong typing option for ActiveRecord column methods
107
+
108
+ `tapioca` gives you the option to generate stricter type signatures for your ActiveRecord column types. By default, methods generated for columns that are defined in the schema have signatures of T.untyped. However, if the object extends a module with name StrongTypeGeneration, tapioca will generate stricter signatures that follow closely with the types defined in the schema. Expectation is the StrongTypeGeneration module you define in your application won't allow objects to be initialized with "bad state". It will check all the attributes that are not nillable to ensure they are not nil.
109
+
98
110
  ## Contributing
99
111
 
100
112
  Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/tapioca. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](https://github.com/Shopify/tapioca/blob/master/CODE_OF_CONDUCT.md) code of conduct.
data/Rakefile CHANGED
@@ -3,10 +3,21 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
5
 
6
- begin
7
- require 'rspec/core/rake_task'
8
- RSpec::Core::RakeTask.new(:spec)
9
- rescue LoadError # rubocop:disable Lint/SuppressedException
6
+ Rake.application.options.trace = false
7
+
8
+ Rake::TestTask.new do |t|
9
+ t.libs << "lib"
10
+ t.libs << "spec"
11
+ t.warning = false
12
+ t.test_files = FileList['spec/**/*_spec.rb']
13
+ end
14
+
15
+ task(:spec) do
16
+ begin
17
+ Rake::Task[:test].execute
18
+ rescue RuntimeError
19
+ exit(1)
20
+ end
10
21
  end
11
22
 
12
23
  task(default: :spec)
@@ -36,8 +36,10 @@ require "tapioca/generator"
36
36
  require "tapioca/cli"
37
37
  require "tapioca/gemfile"
38
38
  require "tapioca/compilers/sorbet"
39
+ require "tapioca/compilers/requires_compiler"
39
40
  require "tapioca/compilers/symbol_table_compiler"
40
41
  require "tapioca/compilers/symbol_table/symbol_generator"
41
42
  require "tapioca/compilers/symbol_table/symbol_loader"
42
43
  require "tapioca/compilers/todos_compiler"
44
+ require "tapioca/compilers/dsl_compiler"
43
45
  require "tapioca/version"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- # typed: false
2
+ # typed: true
3
3
 
4
4
  require 'thor'
5
5
 
@@ -52,6 +52,13 @@ module Tapioca
52
52
  end
53
53
  end
54
54
 
55
+ desc "require", "generate the list of files to be required by tapioca"
56
+ def require
57
+ Tapioca.silence_warnings do
58
+ generator.build_requires
59
+ end
60
+ end
61
+
55
62
  desc "todo", "generate the list of unresolved constants"
56
63
  def todo
57
64
  Tapioca.silence_warnings do
@@ -59,6 +66,18 @@ module Tapioca
59
66
  end
60
67
  end
61
68
 
69
+ desc "dsl [constant...]", "generate RBIs for dynamic methods"
70
+ option :generators,
71
+ type: :array,
72
+ aliases: ["--gen", "-g"],
73
+ banner: "generator [generator ...]",
74
+ desc: "Only run supplied DSL generators"
75
+ def dsl(*constants)
76
+ Tapioca.silence_warnings do
77
+ generator.build_dsl(constants)
78
+ end
79
+ end
80
+
62
81
  desc "generate [gem...]", "generate RBIs from gems"
63
82
  def generate(*gems)
64
83
  Tapioca.silence_warnings do
@@ -79,7 +98,10 @@ module Tapioca
79
98
  end
80
99
 
81
100
  def generator
82
- @generator ||= Generator.new(ConfigBuilder.from_options(options))
101
+ current_command = T.must(current_command_chain.first)
102
+ @generator ||= Generator.new(
103
+ ConfigBuilder.from_options(current_command, options)
104
+ )
83
105
  end
84
106
  end
85
107
  end
@@ -0,0 +1,129 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "action_controller"
8
+ rescue LoadError
9
+ return
10
+ end
11
+
12
+ module Tapioca
13
+ module Compilers
14
+ module Dsl
15
+ # `Tapioca::Compilers::Dsl::ActionControllerHelpers` decorates RBI files for all
16
+ # subclasses of `::ActionController::Base`
17
+ # to add helper methods (see https://api.rubyonrails.org/classes/ActionController/Helpers.html).
18
+ #
19
+ # For example, with the following `MyHelper` module:
20
+ #
21
+ # ~~~rb
22
+ # module MyHelper
23
+ # def greet(user)
24
+ # # ...
25
+ # end
26
+ #
27
+ # def localized_time
28
+ # # ...
29
+ # end
30
+ # end
31
+ # ~~~
32
+ #
33
+ # and the following controller:
34
+ #
35
+ # ~~~rb
36
+ # class UserController < ActionController::Base
37
+ # helper MyHelper
38
+ # helper { def age(user) "99" end }
39
+ # helper_method :current_user_name
40
+ #
41
+ # def current_user_name
42
+ # # ...
43
+ # end
44
+ # end
45
+ #
46
+ # this generator will produce an RBI file `user_controller.rbi` with the following content:
47
+ #
48
+ # ~~~rbi
49
+ # # user_controller.rbi
50
+ # # typed: strong
51
+ # class UserController
52
+ # sig { returns(UserController::HelperProxy) }
53
+ # def helpers; end
54
+ # end
55
+ #
56
+ # module UserController::HelperMethods
57
+ # include MyHelper
58
+ #
59
+ # sig { params(user: T.untyped).returns(T.untyped) }
60
+ # def age(user); end
61
+ #
62
+ # sig { returns(T.untyped) }
63
+ # def current_user_name; end
64
+ # end
65
+ #
66
+ # class UserController::HelperProxy < ::ActionView::Base
67
+ # include UserController::HelperMethods
68
+ # end
69
+ # ~~~
70
+ class ActionControllerHelpers < Base
71
+ extend T::Sig
72
+
73
+ sig do
74
+ override
75
+ .params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(::ActionController::Base))
76
+ .void
77
+ end
78
+ def decorate(root, constant)
79
+ helper_proxy_name = "#{constant}::HelperProxy"
80
+ helper_methods_name = "#{constant}::HelperMethods"
81
+ proxied_helper_methods = constant._helper_methods.map(&:to_s).map(&:to_sym)
82
+
83
+ # Create helper method module
84
+ root.create_module(helper_methods_name) do |helper_methods|
85
+ helpers_module = constant._helpers
86
+
87
+ gather_includes(helpers_module).each do |ancestor|
88
+ helper_methods.create_include(ancestor)
89
+ end
90
+
91
+ helpers_module.instance_methods(false).each do |method_name|
92
+ method = if proxied_helper_methods.include?(method_name)
93
+ constant.instance_method(method_name)
94
+ else
95
+ helpers_module.instance_method(method_name)
96
+ end
97
+ create_method_from_def(helper_methods, method)
98
+ end
99
+ end
100
+
101
+ # Create helper proxy class
102
+ root.create_class(helper_proxy_name, superclass: "::ActionView::Base") do |proxy|
103
+ proxy.create_include(helper_methods_name)
104
+ end
105
+
106
+ # Define the helpers method
107
+ root.path(constant) do |controller|
108
+ create_method(controller, 'helpers', return_type: helper_proxy_name)
109
+ end
110
+ end
111
+
112
+ sig { override.returns(T::Enumerable[Module]) }
113
+ def gather_constants
114
+ ::ActionController::Base.descendants.reject(&:abstract?)
115
+ end
116
+
117
+ private
118
+
119
+ sig { params(mod: Module).returns(T::Array[String]) }
120
+ def gather_includes(mod)
121
+ mod.ancestors
122
+ .reject { |ancestor| ancestor.is_a?(Class) || ancestor == mod || ancestor.name.nil? }
123
+ .map { |ancestor| T.must(ancestor.name) }
124
+ .reverse
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,65 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "action_mailer"
8
+ rescue LoadError
9
+ return
10
+ end
11
+
12
+ module Tapioca
13
+ module Compilers
14
+ module Dsl
15
+ # `Tapioca::Compilers::Dsl::ActionMailer` generates RBI files for subclasses of `ActionMailer::Base`
16
+ # (see https://api.rubyonrails.org/classes/ActionMailer/Base.html).
17
+ #
18
+ # For example, with the following `ActionMailer` subclass:
19
+ #
20
+ # ~~~rb
21
+ # class NotifierMailer < ActionMailer::Base
22
+ # def notify_customer(customer_id)
23
+ # # ...
24
+ # end
25
+ # end
26
+ # ~~~
27
+ #
28
+ # this generator will produce the RBI file `notifier_mailer.rbi` with the following content:
29
+ #
30
+ # ~~~rbi
31
+ # # notifier_mailer.rbi
32
+ # # typed: true
33
+ # class NotifierMailer
34
+ # sig { params(customer_id: T.untyped).returns(::ActionMailer::MessageDelivery) }
35
+ # def self.notify_customer(customer_id); end
36
+ # end
37
+ # ~~~
38
+ class ActionMailer < Base
39
+ extend T::Sig
40
+
41
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(::ActionMailer::Base)).void }
42
+ def decorate(root, constant)
43
+ root.path(constant) do |k|
44
+ constant.action_methods.to_a.each do |mailer_method|
45
+ method_def = constant.instance_method(mailer_method)
46
+ parameters = compile_method_parameters_to_parlour(method_def)
47
+ create_method(
48
+ k,
49
+ mailer_method,
50
+ parameters: parameters,
51
+ return_type: '::ActionMailer::MessageDelivery',
52
+ class_method: true
53
+ )
54
+ end
55
+ end
56
+ end
57
+
58
+ sig { override.returns(T::Enumerable[Module]) }
59
+ def gather_constants
60
+ ::ActionMailer::Base.descendants.reject(&:abstract?)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,285 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "active_record"
8
+ rescue LoadError
9
+ return
10
+ end
11
+
12
+ module Tapioca
13
+ module Compilers
14
+ module Dsl
15
+ # `Tapioca::Compilers::Dsl::ActiveRecordAssociations` refines RBI files for subclasses of `ActiveRecord::Base`
16
+ # (see https://api.rubyonrails.org/classes/ActiveRecord/Base.html). This generator is only
17
+ # responsible for defining the methods that would be created for the association that
18
+ # are defined in the Active Record model.
19
+ #
20
+ # For example, with the following model class:
21
+ #
22
+ # ~~~rb
23
+ # class Post < ActiveRecord::Base
24
+ # belongs_to :category
25
+ # has_many :comments
26
+ # has_one :author, class_name: "User"
27
+ # end
28
+ # ~~~
29
+ #
30
+ # this generator will produce the following methods in the RBI file
31
+ # `post.rbi`:
32
+ #
33
+ # ~~~rbi
34
+ # # post.rbi
35
+ # # typed: true
36
+ #
37
+ # class Post
38
+ # include Post::GeneratedAssociationMethods
39
+ # end
40
+ #
41
+ # module Post::GeneratedAssociationMethods
42
+ # sig { returns(T.nilable(::User)) }
43
+ # def author; end
44
+ #
45
+ # sig { params(value: T.nilable(::User)).void }
46
+ # def author=(value); end
47
+ #
48
+ # sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::User)) }
49
+ # def build_author(*args, &blk); end
50
+ #
51
+ # sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::Category)) }
52
+ # def build_category(*args, &blk); end
53
+ #
54
+ # sig { returns(T.nilable(::Category)) }
55
+ # def category; end
56
+ #
57
+ # sig { params(value: T.nilable(::Category)).void }
58
+ # def category=(value); end
59
+ #
60
+ # sig { returns(T::Array[T.untyped]) }
61
+ # def comment_ids; end
62
+ #
63
+ # sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
64
+ # def comment_ids=(ids); end
65
+ #
66
+ # sig { returns(::ActiveRecord::Associations::CollectionProxy[Comment]) }
67
+ # def comments; end
68
+ #
69
+ # sig { params(value: T::Enumerable[::Comment]).void }
70
+ # def comments=(value); end
71
+ #
72
+ # sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::User)) }
73
+ # def create_author(*args, &blk); end
74
+ #
75
+ # sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::User)) }
76
+ # def create_author!(*args, &blk); end
77
+ #
78
+ # sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::Category)) }
79
+ # def create_category(*args, &blk); end
80
+ #
81
+ # sig { params(args: T.untyped, blk: T.untyped).returns(T.nilable(::Category)) }
82
+ # def create_category!(*args, &blk); end
83
+ #
84
+ # sig { returns(T.nilable(::User)) }
85
+ # def reload_author; end
86
+ #
87
+ # sig { returns(T.nilable(::Category)) }
88
+ # def reload_category; end
89
+ # end
90
+ # ~~~
91
+ class ActiveRecordAssociations < Base
92
+ extend T::Sig
93
+
94
+ ReflectionType = T.type_alias do
95
+ T.any(::ActiveRecord::Reflection::ThroughReflection, ::ActiveRecord::Reflection::AssociationReflection)
96
+ end
97
+
98
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(ActiveRecord::Base)).void }
99
+ def decorate(root, constant)
100
+ return if constant.reflections.empty?
101
+
102
+ module_name = "#{constant}::GeneratedAssociationMethods"
103
+ root.create_module(module_name) do |mod|
104
+ constant.reflections.each do |association_name, reflection|
105
+ if reflection.collection?
106
+ populate_collection_assoc_getter_setter(mod, constant, association_name, reflection)
107
+ else
108
+ populate_single_assoc_getter_setter(mod, constant, association_name, reflection)
109
+ end
110
+ end
111
+ end
112
+
113
+ root.path(constant) do |klass|
114
+ klass.create_include(module_name)
115
+ end
116
+ end
117
+
118
+ sig { override.returns(T::Enumerable[Module]) }
119
+ def gather_constants
120
+ ActiveRecord::Base.descendants.reject(&:abstract_class?)
121
+ end
122
+
123
+ private
124
+
125
+ sig do
126
+ params(
127
+ klass: Parlour::RbiGenerator::Namespace,
128
+ constant: T.class_of(ActiveRecord::Base),
129
+ association_name: T.any(String, Symbol),
130
+ reflection: ReflectionType
131
+ ).void
132
+ end
133
+ def populate_single_assoc_getter_setter(klass, constant, association_name, reflection)
134
+ association_class = type_for(constant, reflection)
135
+ association_type = if belongs_to_and_required?(constant, reflection)
136
+ association_class
137
+ else
138
+ "T.nilable(#{association_class})"
139
+ end
140
+
141
+ create_method(
142
+ klass,
143
+ association_name.to_s,
144
+ return_type: association_type,
145
+ )
146
+ create_method(
147
+ klass,
148
+ "#{association_name}=",
149
+ parameters: [
150
+ Parlour::RbiGenerator::Parameter.new("value", type: association_type),
151
+ ],
152
+ return_type: nil
153
+ )
154
+ create_method(
155
+ klass,
156
+ "reload_#{association_name}",
157
+ return_type: association_type,
158
+ )
159
+ if reflection.constructable?
160
+ create_method(
161
+ klass,
162
+ "build_#{association_name}",
163
+ parameters: [
164
+ Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
165
+ Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
166
+ ],
167
+ return_type: association_type
168
+ )
169
+ create_method(
170
+ klass,
171
+ "create_#{association_name}",
172
+ parameters: [
173
+ Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
174
+ Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
175
+ ],
176
+ return_type: association_type
177
+ )
178
+ create_method(
179
+ klass,
180
+ "create_#{association_name}!",
181
+ parameters: [
182
+ Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
183
+ Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
184
+ ],
185
+ return_type: association_type
186
+ )
187
+ end
188
+ end
189
+
190
+ sig do
191
+ params(
192
+ klass: Parlour::RbiGenerator::Namespace,
193
+ constant: T.class_of(ActiveRecord::Base),
194
+ association_name: T.any(String, Symbol),
195
+ reflection: ReflectionType
196
+ ).void
197
+ end
198
+ def populate_collection_assoc_getter_setter(klass, constant, association_name, reflection)
199
+ association_class = type_for(constant, reflection)
200
+ relation_class = relation_type_for(constant, reflection)
201
+
202
+ create_method(
203
+ klass,
204
+ association_name.to_s,
205
+ return_type: relation_class,
206
+ )
207
+ create_method(
208
+ klass,
209
+ "#{association_name}=",
210
+ parameters: [
211
+ Parlour::RbiGenerator::Parameter.new("value", type: "T::Enumerable[#{association_class}]"),
212
+ ],
213
+ return_type: nil,
214
+ )
215
+ create_method(
216
+ klass,
217
+ "#{association_name.to_s.singularize}_ids",
218
+ return_type: "T::Array[T.untyped]"
219
+ )
220
+ create_method(
221
+ klass,
222
+ "#{association_name.to_s.singularize}_ids=",
223
+ parameters: [
224
+ Parlour::RbiGenerator::Parameter.new("ids", type: "T::Array[T.untyped]"),
225
+ ],
226
+ return_type: "T::Array[T.untyped]"
227
+ )
228
+ end
229
+
230
+ sig do
231
+ params(
232
+ constant: T.class_of(ActiveRecord::Base),
233
+ reflection: ReflectionType
234
+ ).returns(T::Boolean)
235
+ end
236
+ def belongs_to_and_required?(constant, reflection)
237
+ return false unless constant.table_exists?
238
+ return false unless reflection.belongs_to?
239
+ column_definition = constant.columns_hash[reflection.foreign_key.to_s]
240
+
241
+ !column_definition.nil? && !column_definition.null
242
+ end
243
+
244
+ sig do
245
+ params(
246
+ constant: T.class_of(ActiveRecord::Base),
247
+ reflection: ReflectionType
248
+ ).returns(String)
249
+ end
250
+ def type_for(constant, reflection)
251
+ return "T.untyped" if !constant.table_exists? || polymorphic_association?(reflection)
252
+
253
+ "::#{reflection.klass.name}"
254
+ end
255
+
256
+ sig do
257
+ params(
258
+ constant: T.class_of(ActiveRecord::Base),
259
+ reflection: ReflectionType
260
+ ).returns(String)
261
+ end
262
+ def relation_type_for(constant, reflection)
263
+ "ActiveRecord::Associations::CollectionProxy" if !constant.table_exists? ||
264
+ polymorphic_association?(reflection)
265
+
266
+ # Change to: "::#{reflection.klass.name}::ActiveRecord_Associations_CollectionProxy"
267
+ "::ActiveRecord::Associations::CollectionProxy[#{reflection.klass.name}]"
268
+ end
269
+
270
+ sig do
271
+ params(
272
+ reflection: ReflectionType
273
+ ).returns(T::Boolean)
274
+ end
275
+ def polymorphic_association?(reflection)
276
+ if reflection.through_reflection?
277
+ polymorphic_association?(reflection.source_reflection)
278
+ else
279
+ !!reflection.polymorphic?
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end
285
+ end