tapioca 0.4.0 → 0.4.5

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 +26 -1
  3. data/README.md +16 -0
  4. data/Rakefile +16 -4
  5. data/lib/tapioca.rb +6 -2
  6. data/lib/tapioca/cli.rb +25 -3
  7. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +130 -0
  8. data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
  9. data/lib/tapioca/compilers/dsl/active_record_associations.rb +267 -0
  10. data/lib/tapioca/compilers/dsl/active_record_columns.rb +404 -0
  11. data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
  12. data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +212 -0
  13. data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
  14. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +168 -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 +165 -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 +160 -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 +195 -32
  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/gemfile.rb +32 -9
  31. data/lib/tapioca/generator.rb +200 -24
  32. data/lib/tapioca/loader.rb +30 -9
  33. data/lib/tapioca/version.rb +1 -1
  34. metadata +31 -40
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6aedd5a0415cca7e554021e114401254561649803acceed6caaa2cb24b9b4c8c
4
- data.tar.gz: 376290c5b3cad6f082e9fc186cbb2ed32a9bb239c5c37bc8f9f464c78a2d951e
3
+ metadata.gz: a6ca30be86eac3958354ca4204904993088bfeeef36c72d63b428ccc5ff3eff3
4
+ data.tar.gz: e218a8ed57c6eb6c46b00f423771fa1a4874a8f0a945d8f5683e849fe5ca9e91
5
5
  SHA512:
6
- metadata.gz: e65c8822f8312788075a84f24d5b9c95221bba2ced2dd877f150a377696fd70eff96bc936fdd3cf482fa6d4ac1b2d86a0201ed91c386c1ee1ce0bd19e8defd9c
7
- data.tar.gz: bace8a6fca5ca72789c5fdfa121fb7b30bf415e9f04d6c715f43237f4d022c4507a6d6182161c4928a7258f9759c957a22c2559cc73ebc56d093f5864dcd872f
6
+ metadata.gz: 39b2b67d35896d998f0f11dcdeda386e6e8312e124b12d8e5e252650417ba3c8a452aedfe014059064ca69e465d97089bfad55f1801d6667612902a4edc24903
7
+ data.tar.gz: 0fa4fe00a45d1f0e870f6d2cf985291ee1ed3f78d339c1f27be5653cff99490de31c6907abc462f1c1c7c197d7eb3eba63c2a90764c56a89fd607fd950c4a493
data/Gemfile CHANGED
@@ -7,5 +7,30 @@ 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("yard", "~> 0.9.25")
15
+ gem("pry-byebug")
16
+ gem("minitest")
17
+ gem("minitest-hooks")
18
+ gem("minitest-fork_executor")
19
+ gem("minitest-reporters")
20
+ gem("sorbet")
21
+
22
+ group(:development, :test) do
23
+ gem("smart_properties", ">= 1.15.0", require: false)
24
+ gem("frozen_record", ">= 0.17", require: false)
25
+ gem("sprockets", "~> 3.7", require: false)
26
+ gem("rails", "~> 5.2", require: false)
27
+ gem("state_machines", "~> 0.5.0", require: false)
28
+ gem("activerecord-typedstore", "~> 1.3", require: false)
29
+ gem("sqlite3")
30
+ gem("identity_cache", "~> 1.0", require: false)
31
+ gem('cityhash', git: 'https://github.com/csfrancis/cityhash.git',
32
+ ref: '3cfc7d01f333c01811d5e834f1495eaa29f87c36', require: false)
33
+ gem("activemodel-serializers-xml", "~> 1.0", require: false)
34
+ gem("activeresource", "~> 5.1", require: false)
35
+ gem("google-protobuf", "~>3.12.0", require: false)
11
36
  end
data/README.md CHANGED
@@ -8,6 +8,8 @@ As yet, no gem exports type information in a consumable format and it would be a
8
8
 
9
9
  When you run `tapioca sync` in a project, `tapioca` loads all the gems that are in your dependency list from the Gemfile into memory. It then performs runtime introspection on the loaded types to understand their structure and generates an appropriate RBI file for each gem with a versioned filename.
10
10
 
11
+ ## Manual gem requires
12
+
11
13
  For gems that have a normal default `require` and load all of their constants through such a require, everything works seamlessly. However, for gems that are marked as `require: false` in the Gemfile, or for gems that export optionally loaded types via different requires, where a single require does not load the whole gem code into memory, `tapioca` will not be able to load some of the types into memory and, thus, won't be able to generate complete RBIs for them. For this reason, we need to keep a small external file named `sorbet/tapioca/require.rb` that is executed after all the gems in the Gemfile have been required and before generation of gem RBIs have started. This file is responsible for adding the requires for additional files from gems, which are not covered by the default require.
12
14
 
13
15
  For example, suppose you are using the class `BetterHtml::Parser` exported from the `better_html` gem. Just doing a `require "better_html"` (which is the default require) does not load that type:
@@ -31,6 +33,13 @@ In order to make sure that `tapioca` can reflect on that type, we need to add th
31
33
 
32
34
  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
35
 
36
+ You can use the command `tapioca require` to auto-populate the `sorbet/tapioca/require.rb` file with all the requires found
37
+ in your application. Once the file generated, you should review it, remove all unnecessary requires and commit it.
38
+
39
+ ## How does tapioca compare to "srb rbi gems" ?
40
+
41
+ [Please see the detailed answer on our wiki](https://github.com/Shopify/tapioca/wiki/How-does-tapioca-compare-to-%22srb-rbi-gems%22-%3F)
42
+
34
43
  ## Installation
35
44
 
36
45
  Add this line to your application's `Gemfile`:
@@ -49,6 +58,7 @@ Commands:
49
58
  tapioca generate [gem...] # generate RBIs from gems
50
59
  tapioca help [COMMAND] # Describe available commands or one specific command
51
60
  tapioca init # initializes folder structure
61
+ tapioca require # generate the list of files to be required by tapioca
52
62
  tapioca sync # sync RBIs to Gemfile
53
63
  tapioca todo # generate the list of unresolved constants
54
64
 
@@ -87,6 +97,12 @@ Command: `tapioca todo`
87
97
 
88
98
  This will generate the file `sorbet/rbi/todo.rbi` defining all unresolved constants as empty modules.
89
99
 
100
+ ### Generate DSL RBI files
101
+
102
+ Command: `tapioca dsl [constant...]`
103
+
104
+ This will generate DSL RBIs for specified constants (or for all handled constants, if a constant name is not supplied). You can read about DSL RBI generators supplied by `tapioca` in [the manual](manual/generators.md).
105
+
90
106
  ### Flags
91
107
 
92
108
  - `--prerequire [file]`: A file to be required before `Bundler.require` is called.
data/Rakefile CHANGED
@@ -2,11 +2,23 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
+ Dir['tasks/**/*.rake'].each { |t| load t }
5
6
 
6
- begin
7
- require 'rspec/core/rake_task'
8
- RSpec::Core::RakeTask.new(:spec)
9
- rescue LoadError # rubocop:disable Lint/SuppressedException
7
+ Rake.application.options.trace = false
8
+
9
+ Rake::TestTask.new do |t|
10
+ t.libs << "lib"
11
+ t.libs << "spec"
12
+ t.warning = false
13
+ t.test_files = FileList['spec/**/*_spec.rb']
14
+ end
15
+
16
+ task(:spec) do
17
+ begin
18
+ Rake::Task[:test].execute
19
+ rescue RuntimeError
20
+ exit(1)
21
+ end
10
22
  end
11
23
 
12
24
  task(default: :spec)
@@ -4,10 +4,12 @@
4
4
  require "sorbet-runtime"
5
5
 
6
6
  module Tapioca
7
- def self.silence_warnings
7
+ def self.silence_warnings(&blk)
8
8
  original_verbosity = $VERBOSE
9
9
  $VERBOSE = nil
10
- yield
10
+ Gem::DefaultUserInteraction.use_ui(Gem::SilentUI.new) do
11
+ blk.call
12
+ end
11
13
  ensure
12
14
  $VERBOSE = original_verbosity
13
15
  end
@@ -36,8 +38,10 @@ require "tapioca/generator"
36
38
  require "tapioca/cli"
37
39
  require "tapioca/gemfile"
38
40
  require "tapioca/compilers/sorbet"
41
+ require "tapioca/compilers/requires_compiler"
39
42
  require "tapioca/compilers/symbol_table_compiler"
40
43
  require "tapioca/compilers/symbol_table/symbol_generator"
41
44
  require "tapioca/compilers/symbol_table/symbol_loader"
42
45
  require "tapioca/compilers/todos_compiler"
46
+ require "tapioca/compilers/dsl_compiler"
43
47
  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
 
@@ -44,14 +44,21 @@ module Tapioca
44
44
  end
45
45
  create_file(Config::DEFAULT_POSTREQUIRE, skip: true) do
46
46
  <<~CONTENT
47
- # frozen_string_literal: true
48
47
  # typed: false
48
+ # frozen_string_literal: true
49
49
 
50
50
  # Add your extra requires here
51
51
  CONTENT
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,130 @@
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
+ #
47
+ # this generator will produce an RBI file `user_controller.rbi` with the following content:
48
+ #
49
+ # ~~~rbi
50
+ # # user_controller.rbi
51
+ # # typed: strong
52
+ # class UserController
53
+ # sig { returns(UserController::HelperProxy) }
54
+ # def helpers; end
55
+ # end
56
+ #
57
+ # module UserController::HelperMethods
58
+ # include MyHelper
59
+ #
60
+ # sig { params(user: T.untyped).returns(T.untyped) }
61
+ # def age(user); end
62
+ #
63
+ # sig { returns(T.untyped) }
64
+ # def current_user_name; end
65
+ # end
66
+ #
67
+ # class UserController::HelperProxy < ::ActionView::Base
68
+ # include UserController::HelperMethods
69
+ # end
70
+ # ~~~
71
+ class ActionControllerHelpers < Base
72
+ extend T::Sig
73
+
74
+ sig do
75
+ override
76
+ .params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(::ActionController::Base))
77
+ .void
78
+ end
79
+ def decorate(root, constant)
80
+ helper_proxy_name = "#{constant}::HelperProxy"
81
+ helper_methods_name = "#{constant}::HelperMethods"
82
+ proxied_helper_methods = constant._helper_methods.map(&:to_s).map(&:to_sym)
83
+
84
+ # Create helper method module
85
+ root.create_module(helper_methods_name) do |helper_methods|
86
+ helpers_module = constant._helpers
87
+
88
+ gather_includes(helpers_module).each do |ancestor|
89
+ helper_methods.create_include(ancestor)
90
+ end
91
+
92
+ helpers_module.instance_methods(false).each do |method_name|
93
+ method = if proxied_helper_methods.include?(method_name)
94
+ constant.instance_method(method_name)
95
+ else
96
+ helpers_module.instance_method(method_name)
97
+ end
98
+ create_method_from_def(helper_methods, method)
99
+ end
100
+ end
101
+
102
+ # Create helper proxy class
103
+ root.create_class(helper_proxy_name, superclass: "::ActionView::Base") do |proxy|
104
+ proxy.create_include(helper_methods_name)
105
+ end
106
+
107
+ # Define the helpers method
108
+ root.path(constant) do |controller|
109
+ create_method(controller, 'helpers', return_type: helper_proxy_name)
110
+ end
111
+ end
112
+
113
+ sig { override.returns(T::Enumerable[Module]) }
114
+ def gather_constants
115
+ ::ActionController::Base.descendants.reject(&:abstract?)
116
+ end
117
+
118
+ private
119
+
120
+ sig { params(mod: Module).returns(T::Array[String]) }
121
+ def gather_includes(mod)
122
+ mod.ancestors
123
+ .reject { |ancestor| ancestor.is_a?(Class) || ancestor == mod || ancestor.name.nil? }
124
+ .map { |ancestor| T.must(ancestor.name) }
125
+ .reverse
126
+ end
127
+ end
128
+ end
129
+ end
130
+ 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,267 @@
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 = "T.nilable(#{association_class})"
136
+
137
+ create_method(
138
+ klass,
139
+ association_name.to_s,
140
+ return_type: association_type,
141
+ )
142
+ create_method(
143
+ klass,
144
+ "#{association_name}=",
145
+ parameters: [
146
+ Parlour::RbiGenerator::Parameter.new("value", type: association_type),
147
+ ],
148
+ return_type: nil
149
+ )
150
+ create_method(
151
+ klass,
152
+ "reload_#{association_name}",
153
+ return_type: association_type,
154
+ )
155
+ if reflection.constructable?
156
+ create_method(
157
+ klass,
158
+ "build_#{association_name}",
159
+ parameters: [
160
+ Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
161
+ Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
162
+ ],
163
+ return_type: association_type
164
+ )
165
+ create_method(
166
+ klass,
167
+ "create_#{association_name}",
168
+ parameters: [
169
+ Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
170
+ Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
171
+ ],
172
+ return_type: association_type
173
+ )
174
+ create_method(
175
+ klass,
176
+ "create_#{association_name}!",
177
+ parameters: [
178
+ Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped"),
179
+ Parlour::RbiGenerator::Parameter.new("&blk", type: "T.untyped"),
180
+ ],
181
+ return_type: association_type
182
+ )
183
+ end
184
+ end
185
+
186
+ sig do
187
+ params(
188
+ klass: Parlour::RbiGenerator::Namespace,
189
+ constant: T.class_of(ActiveRecord::Base),
190
+ association_name: T.any(String, Symbol),
191
+ reflection: ReflectionType
192
+ ).void
193
+ end
194
+ def populate_collection_assoc_getter_setter(klass, constant, association_name, reflection)
195
+ association_class = type_for(constant, reflection)
196
+ relation_class = relation_type_for(constant, reflection)
197
+
198
+ create_method(
199
+ klass,
200
+ association_name.to_s,
201
+ return_type: relation_class,
202
+ )
203
+ create_method(
204
+ klass,
205
+ "#{association_name}=",
206
+ parameters: [
207
+ Parlour::RbiGenerator::Parameter.new("value", type: "T::Enumerable[#{association_class}]"),
208
+ ],
209
+ return_type: nil,
210
+ )
211
+ create_method(
212
+ klass,
213
+ "#{association_name.to_s.singularize}_ids",
214
+ return_type: "T::Array[T.untyped]"
215
+ )
216
+ create_method(
217
+ klass,
218
+ "#{association_name.to_s.singularize}_ids=",
219
+ parameters: [
220
+ Parlour::RbiGenerator::Parameter.new("ids", type: "T::Array[T.untyped]"),
221
+ ],
222
+ return_type: "T::Array[T.untyped]"
223
+ )
224
+ end
225
+
226
+ sig do
227
+ params(
228
+ constant: T.class_of(ActiveRecord::Base),
229
+ reflection: ReflectionType
230
+ ).returns(String)
231
+ end
232
+ def type_for(constant, reflection)
233
+ return "T.untyped" if !constant.table_exists? || polymorphic_association?(reflection)
234
+
235
+ "::#{reflection.klass.name}"
236
+ end
237
+
238
+ sig do
239
+ params(
240
+ constant: T.class_of(ActiveRecord::Base),
241
+ reflection: ReflectionType
242
+ ).returns(String)
243
+ end
244
+ def relation_type_for(constant, reflection)
245
+ "ActiveRecord::Associations::CollectionProxy" if !constant.table_exists? ||
246
+ polymorphic_association?(reflection)
247
+
248
+ # Change to: "::#{reflection.klass.name}::ActiveRecord_Associations_CollectionProxy"
249
+ "::ActiveRecord::Associations::CollectionProxy[#{reflection.klass.name}]"
250
+ end
251
+
252
+ sig do
253
+ params(
254
+ reflection: ReflectionType
255
+ ).returns(T::Boolean)
256
+ end
257
+ def polymorphic_association?(reflection)
258
+ if reflection.through_reflection?
259
+ polymorphic_association?(reflection.source_reflection)
260
+ else
261
+ !!reflection.polymorphic?
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end