tapioca 0.4.27 → 0.5.3

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +15 -15
  3. data/README.md +2 -2
  4. data/Rakefile +5 -7
  5. data/exe/tapioca +2 -2
  6. data/lib/tapioca/cli.rb +172 -2
  7. data/lib/tapioca/compilers/dsl/aasm.rb +122 -0
  8. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +52 -12
  9. data/lib/tapioca/compilers/dsl/action_mailer.rb +6 -9
  10. data/lib/tapioca/compilers/dsl/active_job.rb +8 -12
  11. data/lib/tapioca/compilers/dsl/active_model_attributes.rb +131 -0
  12. data/lib/tapioca/compilers/dsl/active_model_secure_password.rb +101 -0
  13. data/lib/tapioca/compilers/dsl/active_record_associations.rb +33 -54
  14. data/lib/tapioca/compilers/dsl/active_record_columns.rb +10 -105
  15. data/lib/tapioca/compilers/dsl/active_record_enum.rb +8 -10
  16. data/lib/tapioca/compilers/dsl/active_record_fixtures.rb +86 -0
  17. data/lib/tapioca/compilers/dsl/active_record_scope.rb +7 -10
  18. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +5 -8
  19. data/lib/tapioca/compilers/dsl/active_resource.rb +9 -37
  20. data/lib/tapioca/compilers/dsl/active_storage.rb +98 -0
  21. data/lib/tapioca/compilers/dsl/active_support_concern.rb +106 -0
  22. data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +13 -8
  23. data/lib/tapioca/compilers/dsl/base.rb +108 -82
  24. data/lib/tapioca/compilers/dsl/config.rb +111 -0
  25. data/lib/tapioca/compilers/dsl/frozen_record.rb +5 -7
  26. data/lib/tapioca/compilers/dsl/identity_cache.rb +66 -29
  27. data/lib/tapioca/compilers/dsl/mixed_in_class_attributes.rb +74 -0
  28. data/lib/tapioca/compilers/dsl/protobuf.rb +19 -69
  29. data/lib/tapioca/compilers/dsl/sidekiq_worker.rb +25 -12
  30. data/lib/tapioca/compilers/dsl/smart_properties.rb +21 -33
  31. data/lib/tapioca/compilers/dsl/state_machines.rb +56 -78
  32. data/lib/tapioca/compilers/dsl/url_helpers.rb +7 -10
  33. data/lib/tapioca/compilers/dsl_compiler.rb +25 -40
  34. data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +198 -0
  35. data/lib/tapioca/compilers/requires_compiler.rb +2 -2
  36. data/lib/tapioca/compilers/sorbet.rb +25 -5
  37. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +122 -206
  38. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +4 -4
  39. data/lib/tapioca/compilers/symbol_table_compiler.rb +5 -11
  40. data/lib/tapioca/compilers/todos_compiler.rb +1 -1
  41. data/lib/tapioca/config.rb +3 -0
  42. data/lib/tapioca/config_builder.rb +5 -2
  43. data/lib/tapioca/constant_locator.rb +6 -8
  44. data/lib/tapioca/gemfile.rb +14 -11
  45. data/lib/tapioca/generators/base.rb +61 -0
  46. data/lib/tapioca/generators/dsl.rb +362 -0
  47. data/lib/tapioca/generators/gem.rb +345 -0
  48. data/lib/tapioca/generators/init.rb +79 -0
  49. data/lib/tapioca/generators/require.rb +52 -0
  50. data/lib/tapioca/generators/todo.rb +76 -0
  51. data/lib/tapioca/generators.rb +9 -0
  52. data/lib/tapioca/generic_type_registry.rb +25 -98
  53. data/lib/tapioca/helpers/active_record_column_type_helper.rb +98 -0
  54. data/lib/tapioca/internal.rb +2 -10
  55. data/lib/tapioca/loader.rb +11 -31
  56. data/lib/tapioca/rbi_ext/model.rb +166 -0
  57. data/lib/tapioca/reflection.rb +138 -0
  58. data/lib/tapioca/sorbet_ext/fixed_hash_patch.rb +1 -1
  59. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +72 -4
  60. data/lib/tapioca/sorbet_ext/name_patch.rb +1 -1
  61. data/lib/tapioca/version.rb +1 -1
  62. data/lib/tapioca.rb +3 -0
  63. metadata +45 -23
  64. data/lib/tapioca/cli/main.rb +0 -146
  65. data/lib/tapioca/core_ext/class.rb +0 -28
  66. data/lib/tapioca/core_ext/string.rb +0 -18
  67. data/lib/tapioca/generator.rb +0 -633
  68. data/lib/tapioca/rbi/model.rb +0 -405
  69. data/lib/tapioca/rbi/printer.rb +0 -410
  70. data/lib/tapioca/rbi/rewriters/group_nodes.rb +0 -106
  71. data/lib/tapioca/rbi/rewriters/nest_non_public_methods.rb +0 -65
  72. data/lib/tapioca/rbi/rewriters/nest_singleton_methods.rb +0 -42
  73. data/lib/tapioca/rbi/rewriters/sort_nodes.rb +0 -86
  74. data/lib/tapioca/rbi/visitor.rb +0 -21
@@ -1,23 +1,31 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "parlour"
4
+ require "tapioca/rbi_ext/model"
5
5
 
6
6
  module Tapioca
7
7
  module Compilers
8
8
  module Dsl
9
+ COMPILERS_PATH = T.let(File.expand_path("..", __FILE__).to_s, String)
10
+
9
11
  class Base
10
12
  extend T::Sig
11
13
  extend T::Helpers
12
14
 
15
+ include Reflection
16
+
13
17
  abstract!
14
18
 
15
19
  sig { returns(T::Set[Module]) }
16
20
  attr_reader :processable_constants
17
21
 
22
+ sig { returns(T::Array[String]) }
23
+ attr_reader :errors
24
+
18
25
  sig { void }
19
26
  def initialize
20
27
  @processable_constants = T.let(Set.new(gather_constants), T::Set[Module])
28
+ @errors = T.let([], T::Array[String])
21
29
  end
22
30
 
23
31
  sig { params(constant: Module).returns(T::Boolean) }
@@ -29,65 +37,117 @@ module Tapioca
29
37
  abstract
30
38
  .type_parameters(:T)
31
39
  .params(
32
- root: Parlour::RbiGenerator::Namespace,
40
+ tree: RBI::Tree,
33
41
  constant: T.type_parameter(:T)
34
42
  )
35
43
  .void
36
44
  end
37
- def decorate(root, constant); end
45
+ def decorate(tree, constant); end
38
46
 
39
47
  sig { abstract.returns(T::Enumerable[Module]) }
40
48
  def gather_constants; end
41
49
 
42
- private
50
+ # NOTE: This should eventually accept an `Error` object or `Exception` rather than simply a `String`.
51
+ sig { params(error: String).void }
52
+ def add_error(error)
53
+ @errors << error
54
+ end
43
55
 
44
- SPECIAL_METHOD_NAMES = T.let(
45
- %w[! ~ +@ ** -@ * / % + - << >> & | ^ < <= => > >= == === != =~ !~ <=> [] []= `].freeze,
46
- T::Array[String]
47
- )
56
+ private
48
57
 
49
- sig { params(name: String).returns(T::Boolean) }
50
- def valid_method_name?(name)
51
- return true if SPECIAL_METHOD_NAMES.include?(name)
52
- !!name.match(/^[a-zA-Z_][[:word:]]*[?!=]?$/)
58
+ sig { returns(T::Enumerable[Class]) }
59
+ def all_classes
60
+ @all_classes = T.let(@all_classes, T.nilable(T::Enumerable[Class]))
61
+ @all_classes ||= T.cast(ObjectSpace.each_object(Class), T::Enumerable[Class]).each
53
62
  end
54
63
 
55
- sig do
56
- params(
57
- namespace: Parlour::RbiGenerator::Namespace,
58
- name: String,
59
- options: T::Hash[T.untyped, T.untyped]
60
- ).void
61
- end
62
- def create_method(namespace, name, options = {})
63
- return unless valid_method_name?(name)
64
- T.unsafe(namespace).create_method(name, **options)
64
+ sig { returns(T::Enumerable[Module]) }
65
+ def all_modules
66
+ @all_modules = T.let(@all_modules, T.nilable(T::Enumerable[Module]))
67
+ @all_modules ||= T.cast(ObjectSpace.each_object(Module), T::Enumerable[Module]).each
65
68
  end
66
69
 
67
- # Create a Parlour method inside `namespace` from its Ruby definition
70
+ # Get the types of each parameter from a method signature
68
71
  sig do
69
72
  params(
70
- namespace: Parlour::RbiGenerator::Namespace,
71
73
  method_def: T.any(Method, UnboundMethod),
72
- class_method: T::Boolean
73
- ).void
74
+ signature: T.untyped # as `T::Private::Methods::Signature` is private
75
+ ).returns(T::Array[String])
74
76
  end
75
- def create_method_from_def(namespace, method_def, class_method: false)
76
- create_method(
77
- namespace,
77
+ def parameters_types_from_signature(method_def, signature)
78
+ params = T.let([], T::Array[String])
79
+
80
+ return method_def.parameters.map { "T.untyped" } unless signature
81
+
82
+ # parameters types
83
+ signature.arg_types.each { |arg_type| params << arg_type[1].to_s }
84
+
85
+ # keyword parameters types
86
+ signature.kwarg_types.each { |_, kwarg_type| params << kwarg_type.to_s }
87
+
88
+ # rest parameter type
89
+ params << signature.rest_type.to_s if signature.has_rest
90
+
91
+ # special case `.void` in a proc
92
+ unless signature.block_name.nil?
93
+ params << signature.block_type.to_s.gsub("returns(<VOID>)", "void")
94
+ end
95
+
96
+ params
97
+ end
98
+
99
+ sig { params(scope: RBI::Scope, method_def: T.any(Method, UnboundMethod), class_method: T::Boolean).void }
100
+ def create_method_from_def(scope, method_def, class_method: false)
101
+ scope.create_method(
78
102
  method_def.name.to_s,
79
- parameters: compile_method_parameters_to_parlour(method_def),
80
- return_type: compile_method_return_type_to_parlour(method_def),
103
+ parameters: compile_method_parameters_to_rbi(method_def),
104
+ return_type: compile_method_return_type_to_rbi(method_def),
81
105
  class_method: class_method
82
106
  )
83
107
  end
84
108
 
85
- # Compile a Ruby method parameters into Parlour parameters
86
- sig do
87
- params(method_def: T.any(Method, UnboundMethod))
88
- .returns(T::Array[Parlour::RbiGenerator::Parameter])
109
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
110
+ def create_param(name, type:)
111
+ create_typed_param(RBI::Param.new(name), type)
112
+ end
113
+
114
+ sig { params(name: String, type: String, default: String).returns(RBI::TypedParam) }
115
+ def create_opt_param(name, type:, default:)
116
+ create_typed_param(RBI::OptParam.new(name, default), type)
117
+ end
118
+
119
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
120
+ def create_rest_param(name, type:)
121
+ create_typed_param(RBI::RestParam.new(name), type)
122
+ end
123
+
124
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
125
+ def create_kw_param(name, type:)
126
+ create_typed_param(RBI::KwParam.new(name), type)
89
127
  end
90
- def compile_method_parameters_to_parlour(method_def)
128
+
129
+ sig { params(name: String, type: String, default: String).returns(RBI::TypedParam) }
130
+ def create_kw_opt_param(name, type:, default:)
131
+ create_typed_param(RBI::KwOptParam.new(name, default), type)
132
+ end
133
+
134
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
135
+ def create_kw_rest_param(name, type:)
136
+ create_typed_param(RBI::KwRestParam.new(name), type)
137
+ end
138
+
139
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
140
+ def create_block_param(name, type:)
141
+ create_typed_param(RBI::BlockParam.new(name), type)
142
+ end
143
+
144
+ sig { params(param: RBI::Param, type: String).returns(RBI::TypedParam) }
145
+ def create_typed_param(param, type)
146
+ RBI::TypedParam.new(param: param, type: type)
147
+ end
148
+
149
+ sig { params(method_def: T.any(Method, UnboundMethod)).returns(T::Array[RBI::TypedParam]) }
150
+ def compile_method_parameters_to_rbi(method_def)
91
151
  signature = T::Private::Methods.signature_for_method(method_def)
92
152
  method_def = signature.nil? ? method_def : signature.method
93
153
  method_types = parameters_types_from_signature(method_def, signature)
@@ -97,72 +157,38 @@ module Tapioca
97
157
 
98
158
  name ||= fallback_arg_name
99
159
  name = name.to_s.gsub(/&|\*/, fallback_arg_name) # avoid incorrect names from `delegate`
100
- method_type = method_types[index]
160
+ method_type = T.must(method_types[index])
101
161
 
102
162
  case type
103
163
  when :req
104
- ::Parlour::RbiGenerator::Parameter.new(name, type: method_type)
164
+ create_param(name, type: method_type)
105
165
  when :opt
106
- ::Parlour::RbiGenerator::Parameter.new(name, type: method_type, default: 'T.unsafe(nil)')
166
+ create_opt_param(name, type: method_type, default: "T.unsafe(nil)")
107
167
  when :rest
108
- ::Parlour::RbiGenerator::Parameter.new("*#{name}", type: method_type)
168
+ create_rest_param(name, type: method_type)
109
169
  when :keyreq
110
- ::Parlour::RbiGenerator::Parameter.new("#{name}:", type: method_type)
170
+ create_kw_param(name, type: method_type)
111
171
  when :key
112
- ::Parlour::RbiGenerator::Parameter.new("#{name}:", type: method_type, default: 'T.unsafe(nil)')
172
+ create_kw_opt_param(name, type: method_type, default: "T.unsafe(nil)")
113
173
  when :keyrest
114
- ::Parlour::RbiGenerator::Parameter.new("**#{name}", type: method_type)
174
+ create_kw_rest_param(name, type: method_type)
115
175
  when :block
116
- ::Parlour::RbiGenerator::Parameter.new("&#{name}", type: method_type)
176
+ create_block_param(name, type: method_type)
117
177
  else
118
178
  raise "Unknown type `#{type}`."
119
179
  end
120
180
  end
121
181
  end
122
182
 
123
- # Compile a Ruby method return type into a Parlour type
124
- sig do
125
- params(method_def: T.any(Method, UnboundMethod))
126
- .returns(T.nilable(String))
127
- end
128
- def compile_method_return_type_to_parlour(method_def)
183
+ sig { params(method_def: T.any(Method, UnboundMethod)).returns(String) }
184
+ def compile_method_return_type_to_rbi(method_def)
129
185
  signature = T::Private::Methods.signature_for_method(method_def)
130
- return_type = signature.nil? ? 'T.untyped' : signature.return_type.to_s
131
- # Map <VOID> to `nil` since `nil` means a `void` return for Parlour
132
- return_type = nil if return_type == "<VOID>"
186
+ return_type = signature.nil? ? "T.untyped" : name_of_type(signature.return_type)
187
+ return_type = "void" if return_type == "<VOID>"
133
188
  # Map <NOT-TYPED> to `T.untyped`
134
189
  return_type = "T.untyped" if return_type == "<NOT-TYPED>"
135
190
  return_type
136
191
  end
137
-
138
- # Get the types of each parameter from a method signature
139
- sig do
140
- params(
141
- method_def: T.any(Method, UnboundMethod),
142
- signature: T.untyped # as `T::Private::Methods::Signature` is private
143
- ).returns(T::Array[String])
144
- end
145
- def parameters_types_from_signature(method_def, signature)
146
- params = T.let([], T::Array[String])
147
-
148
- return method_def.parameters.map { 'T.untyped' } unless signature
149
-
150
- # parameters types
151
- signature.arg_types.each { |arg_type| params << arg_type[1].to_s }
152
-
153
- # keyword parameters types
154
- signature.kwarg_types.each { |_, kwarg_type| params << kwarg_type.to_s }
155
-
156
- # rest parameter type
157
- params << signature.rest_type.to_s if signature.has_rest
158
-
159
- # special case `.void` in a proc
160
- unless signature.block_name.nil?
161
- params << signature.block_type.to_s.gsub('returns(<VOID>)', 'void')
162
- end
163
-
164
- params
165
- end
166
192
  end
167
193
  end
168
194
  end
@@ -0,0 +1,111 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "config"
6
+ rescue LoadError
7
+ return
8
+ end
9
+
10
+ module Tapioca
11
+ module Compilers
12
+ module Dsl
13
+ # `Tapioca::Compilers::Dsl::Config` generates RBI files for classes generated by the
14
+ # [`config`](https://github.com/rubyconfig/config) gem.
15
+ #
16
+ # The gem creates a `Config::Options` instance based on the settings files and/or
17
+ # env variables. It then assigns this instance to a constant with a configurable name,
18
+ # by default `Settings`. Application code uses methods on this constant to read off
19
+ # config values.
20
+ #
21
+ # For a setting file like the following:
22
+ # ```yaml
23
+ # ---
24
+ # github:
25
+ # token: 12345
26
+ # client_id: 54321
27
+ # client_secret: super_secret
28
+ # ```
29
+ # and a `Config` setup like:
30
+ # ```ruby
31
+ # Config.setup do |config|
32
+ # config.const_name = "AppSettings"
33
+ # end
34
+ # ```
35
+ # this generator will produce the following RBI file:
36
+ # ```rbi
37
+ # AppSettings = T.let(T.unsafe(nil), AppSettingsConfigOptions)
38
+ #
39
+ # class AppSettingsConfigOptions < ::Config::Options
40
+ # sig { returns(T.untyped) }
41
+ # def github; end
42
+ #
43
+ # sig { params(value: T.untyped).returns(T.untyped) }
44
+ # def github=(value); end
45
+ # end
46
+ # ```
47
+ class Config < Base
48
+ extend T::Sig
49
+
50
+ CONFIG_OPTIONS_SUFFIX = "ConfigOptions"
51
+
52
+ sig { override.params(root: RBI::Tree, constant: Module).void }
53
+ def decorate(root, constant)
54
+ # The constant we are given is the specialized config options type
55
+ option_class_name = constant.name
56
+ return unless option_class_name
57
+
58
+ # Grab the config constant name and the actual config constant
59
+ config_constant_name = option_class_name
60
+ .gsub(/#{CONFIG_OPTIONS_SUFFIX}$/, "")
61
+ config_constant = Object.const_get(config_constant_name)
62
+
63
+ # Look up method names from the keys of the config constant
64
+ method_names = config_constant.keys
65
+
66
+ return if method_names.empty?
67
+
68
+ root.create_constant(config_constant_name, value: "T.let(T.unsafe(nil), #{option_class_name})")
69
+
70
+ root.create_class(option_class_name, superclass_name: "::Config::Options") do |mod|
71
+ # We need this to be generic only becuase `Config::Options` is an
72
+ # enumerable and, thus, needs to redeclare the `Elem` type member.
73
+ #
74
+ # We declare it as a fixed member of `T.untyped` so that if anyone
75
+ # enumerates the entries, we don't make any assumptions about their
76
+ # types.
77
+ mod.create_extend("T::Generic")
78
+ mod.create_type_member("Elem", value: "type_member(fixed: T.untyped)")
79
+
80
+ method_names.each do |method_name|
81
+ # Create getter method
82
+ mod.create_method(
83
+ method_name.to_s,
84
+ return_type: "T.untyped"
85
+ )
86
+
87
+ # Create setter method
88
+ mod.create_method(
89
+ "#{method_name}=",
90
+ parameters: [create_param("value", type: "T.untyped")],
91
+ return_type: "T.untyped"
92
+ )
93
+ end
94
+ end
95
+ end
96
+
97
+ sig { override.returns(T::Enumerable[Module]) }
98
+ def gather_constants
99
+ name = ::Config.const_name
100
+ return [] unless Object.const_defined?(name)
101
+
102
+ config_object = Object.const_get(name)
103
+ options_class_name = "#{name}#{CONFIG_OPTIONS_SUFFIX}"
104
+ Object.const_set(options_class_name, config_object.singleton_class)
105
+
106
+ Array(config_object.singleton_class)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -1,8 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "parlour"
5
-
6
4
  begin
7
5
  require "frozen_record"
8
6
  rescue LoadError
@@ -67,18 +65,18 @@ module Tapioca
67
65
  class FrozenRecord < Base
68
66
  extend T::Sig
69
67
 
70
- sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(::FrozenRecord::Base)).void }
68
+ sig { override.params(root: RBI::Tree, constant: T.class_of(::FrozenRecord::Base)).void }
71
69
  def decorate(root, constant)
72
70
  attributes = constant.attributes
73
71
  return if attributes.empty?
74
72
 
75
- root.path(constant) do |record|
73
+ root.create_path(constant) do |record|
76
74
  module_name = "FrozenRecordAttributeMethods"
77
75
 
78
76
  record.create_module(module_name) do |mod|
79
77
  attributes.each do |attribute|
80
- create_method(mod, "#{attribute}?", return_type: 'T::Boolean')
81
- create_method(mod, attribute.to_s, return_type: 'T.untyped')
78
+ mod.create_method("#{attribute}?", return_type: "T::Boolean")
79
+ mod.create_method(attribute.to_s, return_type: "T.untyped")
82
80
  end
83
81
  end
84
82
 
@@ -88,7 +86,7 @@ module Tapioca
88
86
 
89
87
  sig { override.returns(T::Enumerable[Module]) }
90
88
  def gather_constants
91
- ::FrozenRecord::Base.descendants.reject(&:abstract_class?)
89
+ descendants_of(::FrozenRecord::Base).reject(&:abstract_class?)
92
90
  end
93
91
  end
94
92
  end
@@ -1,8 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "parlour"
5
-
6
4
  begin
7
5
  require "rails/railtie"
8
6
  require "identity_cache"
@@ -67,23 +65,16 @@ module Tapioca
67
65
 
68
66
  COLLECTION_TYPE = T.let(
69
67
  ->(type) { "T::Array[::#{type}]" },
70
- T.proc.params(type: Module).returns(String)
68
+ T.proc.params(type: T.any(Module, String)).returns(String)
71
69
  )
72
70
 
73
- sig do
74
- override
75
- .params(
76
- root: Parlour::RbiGenerator::Namespace,
77
- constant: T.class_of(::ActiveRecord::Base)
78
- )
79
- .void
80
- end
71
+ sig { override.params(root: RBI::Tree, constant: T.class_of(::ActiveRecord::Base)).void }
81
72
  def decorate(root, constant)
82
73
  caches = constant.send(:all_cached_associations)
83
74
  cache_indexes = constant.send(:cache_indexes)
84
75
  return if caches.empty? && cache_indexes.empty?
85
76
 
86
- root.path(constant) do |model|
77
+ root.create_path(constant) do |model|
87
78
  cache_manys = constant.send(:cached_has_manys)
88
79
  cache_ones = constant.send(:cached_has_ones)
89
80
  cache_belongs = constant.send(:cached_belongs_tos)
@@ -108,7 +99,7 @@ module Tapioca
108
99
 
109
100
  sig { override.returns(T::Enumerable[Module]) }
110
101
  def gather_constants
111
- ::ActiveRecord::Base.descendants.select do |klass|
102
+ descendants_of(::ActiveRecord::Base).select do |klass|
112
103
  klass < ::IdentityCache::WithoutPrimaryIndex
113
104
  end
114
105
  end
@@ -119,8 +110,7 @@ module Tapioca
119
110
  params(
120
111
  field: T.untyped,
121
112
  returns_collection: T::Boolean
122
- )
123
- .returns(String)
113
+ ).returns(String)
124
114
  end
125
115
  def type_for_field(field, returns_collection:)
126
116
  cache_type = field.reflection.compute_class(field.reflection.class_name)
@@ -136,10 +126,9 @@ module Tapioca
136
126
  sig do
137
127
  params(
138
128
  field: T.untyped,
139
- klass: Parlour::RbiGenerator::Namespace,
129
+ klass: RBI::Scope,
140
130
  returns_collection: T::Boolean
141
- )
142
- .void
131
+ ).void
143
132
  end
144
133
  def create_fetch_field_methods(field, klass, returns_collection:)
145
134
  name = field.cached_accessor_name.to_s
@@ -156,21 +145,36 @@ module Tapioca
156
145
  sig do
157
146
  params(
158
147
  field: T.untyped,
159
- klass: Parlour::RbiGenerator::Namespace,
148
+ klass: RBI::Scope,
160
149
  constant: T.class_of(::ActiveRecord::Base),
161
- )
162
- .void
150
+ ).void
163
151
  end
164
152
  def create_fetch_by_methods(field, klass, constant)
153
+ is_cache_index = field.instance_variable_defined?(:@attribute_proc)
154
+
155
+ # Both `cache_index` and `cache_attribute` generate aliased methods
156
+ create_aliased_fetch_by_methods(field, klass, constant)
157
+
158
+ # If the method used was `cache_index` a few extra methods are created
159
+ create_index_fetch_by_methods(field, klass, constant) if is_cache_index
160
+ end
161
+
162
+ sig do
163
+ params(
164
+ field: T.untyped,
165
+ klass: RBI::Scope,
166
+ constant: T.class_of(::ActiveRecord::Base),
167
+ ).void
168
+ end
169
+ def create_index_fetch_by_methods(field, klass, constant)
165
170
  field_length = field.key_fields.length
166
171
  fields_name = field.key_fields.join("_and_")
167
-
172
+ name = "fetch_by_#{fields_name}"
168
173
  parameters = field.key_fields.map do |arg|
169
- Parlour::RbiGenerator::Parameter.new(arg.to_s, type: "T.untyped")
174
+ create_param(arg.to_s, type: "T.untyped")
170
175
  end
171
- parameters << Parlour::RbiGenerator::Parameter.new("includes:", default: "nil", type: "T.untyped")
176
+ parameters << create_kw_opt_param("includes", default: "nil", type: "T.untyped")
172
177
 
173
- name = "fetch_by_#{fields_name}"
174
178
  if field.unique
175
179
  klass.create_method(
176
180
  "#{name}!",
@@ -195,18 +199,51 @@ module Tapioca
195
199
  end
196
200
 
197
201
  if field_length == 1
198
- name = "fetch_multi_by_#{fields_name}"
199
202
  klass.create_method(
200
- name,
203
+ "fetch_multi_by_#{fields_name}",
201
204
  class_method: true,
202
205
  parameters: [
203
- Parlour::RbiGenerator::Parameter.new("index_values", type: "T.untyped"),
204
- Parlour::RbiGenerator::Parameter.new("includes:", default: "nil", type: "T.untyped"),
206
+ create_param("index_values", type: "T::Enumerable[T.untyped]"),
207
+ create_kw_opt_param("includes", default: "nil", type: "T.untyped"),
205
208
  ],
206
209
  return_type: COLLECTION_TYPE.call(constant)
207
210
  )
208
211
  end
209
212
  end
213
+
214
+ sig do
215
+ params(
216
+ field: T.untyped,
217
+ klass: RBI::Scope,
218
+ constant: T.class_of(::ActiveRecord::Base),
219
+ ).void
220
+ end
221
+ def create_aliased_fetch_by_methods(field, klass, constant)
222
+ type, _ = ActiveRecordColumnTypeHelper.new(constant).type_for(field.alias_name.to_s)
223
+ multi_type = type.delete_prefix("T.nilable(").delete_suffix(")").delete_prefix("::")
224
+ length = field.key_fields.length
225
+ suffix = field.send(:fetch_method_suffix)
226
+
227
+ parameters = field.key_fields.map do |arg|
228
+ create_param(arg.to_s, type: "T.untyped")
229
+ end
230
+
231
+ klass.create_method(
232
+ "fetch_#{suffix}",
233
+ class_method: true,
234
+ parameters: parameters,
235
+ return_type: type
236
+ )
237
+
238
+ if length == 1
239
+ klass.create_method(
240
+ "fetch_multi_#{suffix}",
241
+ class_method: true,
242
+ parameters: [create_param("keys", type: "T::Enumerable[T.untyped]")],
243
+ return_type: COLLECTION_TYPE.call(multi_type)
244
+ )
245
+ end
246
+ end
210
247
  end
211
248
  end
212
249
  end
@@ -0,0 +1,74 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "active_support/core_ext/class/attribute"
6
+ rescue LoadError
7
+ return
8
+ end
9
+
10
+ module Tapioca
11
+ module Compilers
12
+ module Dsl
13
+ # `Tapioca::Compilers::Dsl::MixedInClassAttributes` generates RBI files for modules that dynamically use
14
+ # `class_attribute` on classes.
15
+ #
16
+ # For example, given the following concern
17
+ #
18
+ # ~~~rb
19
+ # module Taggeable
20
+ # extend ActiveSupport::Concern
21
+ #
22
+ # included do
23
+ # class_attribute :tag
24
+ # end
25
+ # end
26
+ # ~~~
27
+ #
28
+ # this generator will produce the RBI file `taggeable.rbi` with the following content:
29
+ #
30
+ # ~~~rbi
31
+ # # typed: strong
32
+ #
33
+ # module Taggeable
34
+ # include GeneratedInstanceMethods
35
+ #
36
+ # mixes_in_class_methods GeneratedClassMethods
37
+ #
38
+ # module GeneratedClassMethods
39
+ # def tag; end
40
+ # def tag=(value); end
41
+ # def tag?; end
42
+ # end
43
+ #
44
+ # module GeneratedInstanceMethods
45
+ # def tag; end
46
+ # def tag=(value); end
47
+ # def tag?; end
48
+ # end
49
+ # end
50
+ # ~~~
51
+ class MixedInClassAttributes < Base
52
+ extend T::Sig
53
+
54
+ sig { override.params(root: RBI::Tree, constant: Module).void }
55
+ def decorate(root, constant)
56
+ mixin_compiler = DynamicMixinCompiler.new(constant)
57
+ return if mixin_compiler.empty_attributes?
58
+
59
+ root.create_path(constant) do |mod|
60
+ mixin_compiler.compile_class_attributes(mod)
61
+ end
62
+ end
63
+
64
+ sig { override.returns(T::Enumerable[Module]) }
65
+ def gather_constants
66
+ # Select all non-anonymous modules that have overridden Module.included
67
+ all_modules.select do |mod|
68
+ !mod.is_a?(Class) && name_of(mod) && Tapioca::Reflection.method_of(mod, :included).owner != Module
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end