tapioca 0.3.1 → 0.4.4

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +25 -1
  3. data/README.md +23 -2
  4. data/Rakefile +15 -4
  5. data/lib/tapioca.rb +8 -2
  6. data/lib/tapioca/cli.rb +32 -3
  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 +267 -0
  10. data/lib/tapioca/compilers/dsl/active_record_columns.rb +393 -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 +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 +92 -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/sorbet.rb +34 -0
  26. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +171 -26
  27. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +1 -20
  28. data/lib/tapioca/compilers/todos_compiler.rb +32 -0
  29. data/lib/tapioca/config.rb +14 -6
  30. data/lib/tapioca/config_builder.rb +22 -9
  31. data/lib/tapioca/constant_locator.rb +1 -0
  32. data/lib/tapioca/core_ext/class.rb +23 -0
  33. data/lib/tapioca/gemfile.rb +32 -9
  34. data/lib/tapioca/generator.rb +231 -23
  35. data/lib/tapioca/loader.rb +30 -9
  36. data/lib/tapioca/version.rb +1 -1
  37. metadata +32 -39
@@ -0,0 +1,393 @@
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::ActiveRecordColumns` 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 attribute methods that would be created for the columns 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
+ # end
25
+ # ~~~
26
+ #
27
+ # and the following database schema:
28
+ #
29
+ # ~~~rb
30
+ # # db/schema.rb
31
+ # create_table :posts do |t|
32
+ # t.string :title, null: false
33
+ # t.string :body
34
+ # t.boolean :published
35
+ # t.timestamps
36
+ # end
37
+ # ~~~
38
+ #
39
+ # this generator will produce the following methods in the RBI file
40
+ # `post.rbi`:
41
+ #
42
+ # ~~~rbi
43
+ # # post.rbi
44
+ # # typed: true
45
+ # class Post
46
+ # sig { returns(T.nilable(::String)) }
47
+ # def body; end
48
+ #
49
+ # sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
50
+ # def body=; end
51
+ #
52
+ # sig { params(args: T.untyped).returns(T::Boolean) }
53
+ # def body?; end
54
+ #
55
+ # sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
56
+ # def created_at; end
57
+ #
58
+ # sig { params(value: ::ActiveSupport::TimeWithZone).returns(::ActiveSupport::TimeWithZone) }
59
+ # def created_at=; end
60
+ #
61
+ # sig { params(args: T.untyped).returns(T::Boolean) }
62
+ # def created_at?; end
63
+ #
64
+ # sig { returns(T.nilable(T::Boolean)) }
65
+ # def published; end
66
+ #
67
+ # sig { params(value: T::Boolean).returns(T::Boolean) }
68
+ # def published=; end
69
+ #
70
+ # sig { params(args: T.untyped).returns(T::Boolean) }
71
+ # def published?; end
72
+ #
73
+ # sig { returns(::String) }
74
+ # def title; end
75
+ #
76
+ # sig { params(value: ::String).returns(::String) }
77
+ # def title=(value); end
78
+ #
79
+ # sig { params(args: T.untyped).returns(T::Boolean) }
80
+ # def title?(*args); end
81
+ #
82
+ # sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
83
+ # def updated_at; end
84
+ #
85
+ # sig { params(value: ::ActiveSupport::TimeWithZone).returns(::ActiveSupport::TimeWithZone) }
86
+ # def updated_at=; end
87
+ #
88
+ # sig { params(args: T.untyped).returns(T::Boolean) }
89
+ # def updated_at?; end
90
+ #
91
+ # ## Also the methods added by https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Dirty.html
92
+ # ## Also the methods added by https://api.rubyonrails.org/classes/ActiveModel/Dirty.html
93
+ # ## Also the methods added by https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html
94
+ # end
95
+ # ~~~
96
+ class ActiveRecordColumns < Base
97
+ extend T::Sig
98
+
99
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(ActiveRecord::Base)).void }
100
+ def decorate(root, constant)
101
+ return unless constant.table_exists?
102
+
103
+ module_name = "#{constant}::GeneratedAttributeMethods"
104
+ root.create_module(module_name) do |mod|
105
+ constant.columns_hash.each_key do |column_name|
106
+ column_name = column_name.to_s
107
+ add_methods_for_attribute(mod, constant, column_name)
108
+ end
109
+
110
+ constant.attribute_aliases.each do |attribute_name, column_name|
111
+ attribute_name = attribute_name.to_s
112
+ column_name = column_name.to_s
113
+ new_method_names = constant.attribute_method_matchers.map { |m| m.method_name(attribute_name) }
114
+ old_method_names = constant.attribute_method_matchers.map { |m| m.method_name(column_name) }
115
+ methods_to_add = new_method_names - old_method_names
116
+
117
+ add_methods_for_attribute(mod, constant, column_name, attribute_name, methods_to_add)
118
+ end
119
+ end
120
+
121
+ root.path(constant) do |klass|
122
+ klass.create_include(module_name)
123
+ end
124
+ end
125
+
126
+ sig { override.returns(T::Enumerable[Module]) }
127
+ def gather_constants
128
+ ActiveRecord::Base.descendants.reject(&:abstract_class?)
129
+ end
130
+
131
+ private
132
+
133
+ sig do
134
+ params(
135
+ klass: Parlour::RbiGenerator::Namespace,
136
+ name: String,
137
+ methods_to_add: T.nilable(T::Array[String]),
138
+ return_type: T.nilable(String),
139
+ parameters: T::Array[[String, String]]
140
+ ).void
141
+ end
142
+ def add_method(klass, name, methods_to_add, return_type: nil, parameters: [])
143
+ create_method(
144
+ klass,
145
+ name,
146
+ parameters: parameters.map do |param, type|
147
+ Parlour::RbiGenerator::Parameter.new(param, type: type)
148
+ end,
149
+ return_type: return_type
150
+ ) if methods_to_add.nil? || methods_to_add.include?(name)
151
+ end
152
+
153
+ sig do
154
+ params(
155
+ klass: Parlour::RbiGenerator::Namespace,
156
+ constant: T.class_of(ActiveRecord::Base),
157
+ column_name: String,
158
+ attribute_name: String,
159
+ methods_to_add: T.nilable(T::Array[String])
160
+ ).void
161
+ end
162
+ def add_methods_for_attribute(klass, constant, column_name, attribute_name = column_name, methods_to_add = nil)
163
+ getter_type, setter_type = type_for(constant, column_name)
164
+
165
+ # Added by ActiveRecord::AttributeMethods::Read
166
+ #
167
+ add_method(
168
+ klass,
169
+ attribute_name.to_s,
170
+ methods_to_add,
171
+ return_type: getter_type
172
+ )
173
+
174
+ # Added by ActiveRecord::AttributeMethods::Write
175
+ #
176
+ add_method(
177
+ klass,
178
+ "#{attribute_name}=",
179
+ methods_to_add,
180
+ parameters: [["value", setter_type]],
181
+ return_type: setter_type
182
+ )
183
+
184
+ # Added by ActiveRecord::AttributeMethods::Query
185
+ #
186
+ add_method(
187
+ klass,
188
+ "#{attribute_name}?",
189
+ methods_to_add,
190
+ return_type: "T::Boolean"
191
+ )
192
+
193
+ # Added by ActiveRecord::AttributeMethods::Dirty
194
+ #
195
+ add_method(
196
+ klass,
197
+ "#{attribute_name}_before_last_save",
198
+ methods_to_add,
199
+ return_type: as_nilable_type(getter_type)
200
+ )
201
+ add_method(
202
+ klass,
203
+ "#{attribute_name}_change_to_be_saved",
204
+ methods_to_add,
205
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])"
206
+ )
207
+ add_method(
208
+ klass,
209
+ "#{attribute_name}_in_database",
210
+ methods_to_add,
211
+ return_type: as_nilable_type(getter_type)
212
+ )
213
+ add_method(
214
+ klass,
215
+ "saved_change_to_#{attribute_name}",
216
+ methods_to_add,
217
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])"
218
+ )
219
+ add_method(
220
+ klass,
221
+ "saved_change_to_#{attribute_name}?",
222
+ methods_to_add,
223
+ return_type: "T::Boolean"
224
+ )
225
+ add_method(
226
+ klass,
227
+ "will_save_change_to_#{attribute_name}?",
228
+ methods_to_add,
229
+ return_type: "T::Boolean"
230
+ )
231
+
232
+ # Added by ActiveModel::Dirty
233
+ #
234
+ add_method(
235
+ klass,
236
+ "#{attribute_name}_change",
237
+ methods_to_add,
238
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])"
239
+ )
240
+ add_method(
241
+ klass,
242
+ "#{attribute_name}_changed?",
243
+ methods_to_add,
244
+ return_type: "T::Boolean"
245
+ )
246
+ add_method(
247
+ klass,
248
+ "#{attribute_name}_will_change!",
249
+ methods_to_add
250
+ )
251
+ add_method(
252
+ klass,
253
+ "#{attribute_name}_was",
254
+ methods_to_add,
255
+ return_type: as_nilable_type(getter_type)
256
+ )
257
+ add_method(
258
+ klass,
259
+ "#{attribute_name}_previous_change",
260
+ methods_to_add,
261
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])"
262
+ )
263
+ add_method(
264
+ klass,
265
+ "#{attribute_name}_previously_changed?",
266
+ methods_to_add,
267
+ return_type: "T::Boolean"
268
+ )
269
+ add_method(
270
+ klass,
271
+ "#{attribute_name}_previously_was",
272
+ methods_to_add,
273
+ return_type: as_nilable_type(getter_type)
274
+ )
275
+ add_method(
276
+ klass,
277
+ "restore_#{attribute_name}!",
278
+ methods_to_add
279
+ )
280
+
281
+ # Added by ActiveRecord::AttributeMethods::BeforeTypeCast
282
+ #
283
+ add_method(
284
+ klass,
285
+ "#{attribute_name}_before_type_cast",
286
+ methods_to_add,
287
+ return_type: "T.untyped"
288
+ )
289
+ add_method(
290
+ klass,
291
+ "#{attribute_name}_came_from_user?",
292
+ methods_to_add,
293
+ return_type: "T::Boolean"
294
+ )
295
+ end
296
+
297
+ sig do
298
+ params(
299
+ constant: T.class_of(ActiveRecord::Base),
300
+ column_name: String
301
+ ).returns([String, String])
302
+ end
303
+ def type_for(constant, column_name)
304
+ return ["T.untyped", "T.untyped"] if do_not_generate_strong_types?(constant)
305
+
306
+ column_type = constant.attribute_types[column_name]
307
+
308
+ getter_type =
309
+ case column_type
310
+ when ActiveRecord::Type::Integer
311
+ "::Integer"
312
+ when ActiveRecord::Type::String
313
+ "::String"
314
+ when ActiveRecord::Type::Date
315
+ "::Date"
316
+ when ActiveRecord::Type::Decimal
317
+ "::BigDecimal"
318
+ when ActiveRecord::Type::Float
319
+ "::Float"
320
+ when ActiveRecord::Type::Boolean
321
+ "T::Boolean"
322
+ when ActiveRecord::Type::DateTime, ActiveRecord::Type::Time
323
+ "::DateTime"
324
+ when ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter
325
+ "::ActiveSupport::TimeWithZone"
326
+ else
327
+ handle_unknown_type(column_type)
328
+ end
329
+
330
+ column = constant.columns_hash[column_name]
331
+ setter_type = getter_type
332
+
333
+ if column&.null
334
+ return ["T.nilable(#{getter_type})", "T.nilable(#{setter_type})"]
335
+ end
336
+
337
+ if column_name == constant.primary_key ||
338
+ column_name == "created_at" ||
339
+ column_name == "updated_at"
340
+ getter_type = "T.nilable(#{getter_type})"
341
+ end
342
+
343
+ [getter_type, setter_type]
344
+ end
345
+
346
+ sig { params(constant: Module).returns(T::Boolean) }
347
+ def do_not_generate_strong_types?(constant)
348
+ Object.const_defined?(:StrongTypeGeneration) &&
349
+ !(constant.singleton_class < Object.const_get(:StrongTypeGeneration))
350
+ end
351
+
352
+ sig { params(column_type: Object).returns(String) }
353
+ def handle_unknown_type(column_type)
354
+ return "T.untyped" unless ActiveModel::Type::Value === column_type
355
+
356
+ lookup_return_type_of_method(column_type, :deserialize) ||
357
+ lookup_return_type_of_method(column_type, :cast) ||
358
+ lookup_arg_type_of_method(column_type, :serialize) ||
359
+ "T.untyped"
360
+ end
361
+
362
+ sig { params(column_type: ActiveModel::Type::Value, method: Symbol).returns(T.nilable(String)) }
363
+ def lookup_return_type_of_method(column_type, method)
364
+ signature = T::Private::Methods.signature_for_method(column_type.method(method))
365
+ return unless signature
366
+
367
+ return_type = signature.return_type
368
+ return if T::Types::Simple === return_type && T::Generic === return_type.raw_type
369
+ return if return_type == T::Private::Types::Void || return_type == T::Private::Types::NotTyped
370
+
371
+ return_type.to_s
372
+ end
373
+
374
+ sig { params(column_type: ActiveModel::Type::Value, method: Symbol).returns(T.nilable(String)) }
375
+ def lookup_arg_type_of_method(column_type, method)
376
+ signature = T::Private::Methods.signature_for_method(column_type.method(method))
377
+ return unless signature
378
+
379
+ arg_type = signature.arg_types.first.last
380
+ return if T::Types::Simple === arg_type && T::Generic === arg_type.raw_type
381
+
382
+ arg_type.to_s
383
+ end
384
+
385
+ sig { params(type: String).returns(String) }
386
+ def as_nilable_type(type)
387
+ return type if type.start_with?("T.nilable(")
388
+ "T.nilable(#{type})"
389
+ end
390
+ end
391
+ end
392
+ end
393
+ end
@@ -0,0 +1,112 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "active_record"
8
+ rescue LoadError
9
+ # means ActiveRecord is not installed,
10
+ # so let's not even define the generator.
11
+ return
12
+ end
13
+
14
+ module Tapioca
15
+ module Compilers
16
+ module Dsl
17
+ # `Tapioca::Compilers::Dsl::ActiveRecordEnum` decorates RBI files for subclasses of
18
+ # `ActiveRecord::Base` which declare `enum` fields
19
+ # (see https://api.rubyonrails.org/classes/ActiveRecord/Enum.html).
20
+ #
21
+ # For example, with the following `ActiveRecord::Base` subclass:
22
+ #
23
+ # ~~~rb
24
+ # class Post < ApplicationRecord
25
+ # enum title_type: %i(book all web), _suffix: :title
26
+ # end
27
+ # ~~~
28
+ #
29
+ # this generator will produce the RBI file `post.rbi` with the following content:
30
+ #
31
+ # ~~~rbi
32
+ # # post.rbi
33
+ # # typed: true
34
+ # class Post
35
+ # sig { void }
36
+ # def all_title!; end
37
+ #
38
+ # sig { returns(T::Boolean) }
39
+ # def all_title?; end
40
+ #
41
+ # sig { returns(T::Hash[T.any(String, Symbol), Integer]) }
42
+ # def self.title_types; end
43
+ #
44
+ # sig { void }
45
+ # def book_title!; end
46
+ #
47
+ # sig { returns(T::Boolean) }
48
+ # def book_title?; end
49
+ #
50
+ # sig { void }
51
+ # def web_title!; end
52
+ #
53
+ # sig { returns(T::Boolean) }
54
+ # def web_title?; end
55
+ # end
56
+ # ~~~
57
+ class ActiveRecordEnum < Base
58
+ extend T::Sig
59
+
60
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(::ActiveRecord::Base)).void }
61
+ def decorate(root, constant)
62
+ return if constant.defined_enums.empty?
63
+
64
+ module_name = "#{constant}::EnumMethodsModule"
65
+ root.create_module(module_name) do |mod|
66
+ generate_instance_methods(constant, mod)
67
+ end
68
+
69
+ root.path(constant) do |k|
70
+ k.create_include(module_name)
71
+
72
+ constant.defined_enums.each do |name, enum_map|
73
+ type = type_for_enum(enum_map)
74
+ create_method(k, name.pluralize, class_method: true, return_type: type)
75
+ end
76
+ end
77
+ end
78
+
79
+ sig { override.returns(T::Enumerable[Module]) }
80
+ def gather_constants
81
+ ::ActiveRecord::Base.descendants.reject(&:abstract_class?)
82
+ end
83
+
84
+ private
85
+
86
+ sig { params(enum_map: T::Hash[T.untyped, T.untyped]).returns(String) }
87
+ def type_for_enum(enum_map)
88
+ value_type = enum_map.values.map { |v| v.class.name }.uniq
89
+ value_type = if value_type.length == 1
90
+ value_type.first
91
+ else
92
+ "T.any(#{value_type.join(', ')})"
93
+ end
94
+
95
+ "T::Hash[T.any(String, Symbol), #{value_type}]"
96
+ end
97
+
98
+ sig { params(constant: T.class_of(::ActiveRecord::Base), klass: Parlour::RbiGenerator::Namespace).void }
99
+ def generate_instance_methods(constant, klass)
100
+ methods = constant.send(:_enum_methods_module).instance_methods
101
+
102
+ methods.each do |method|
103
+ method = method.to_s
104
+ return_type = "T::Boolean" if method.end_with?("?")
105
+
106
+ create_method(klass, method, return_type: return_type)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end