tapioca 0.4.27 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
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