rbs_rails 0.12.0 → 0.13.0

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/release.yml +27 -0
  4. data/CHANGELOG.md +64 -0
  5. data/Gemfile +9 -3
  6. data/Gemfile.lock +202 -129
  7. data/README.md +47 -1
  8. data/Rakefile +7 -2
  9. data/Steepfile +5 -0
  10. data/example/rbs_rails.rb +27 -0
  11. data/exe/rbs_rails +6 -0
  12. data/lib/generators/rbs_rails/install_generator.rb +10 -15
  13. data/lib/rbs_rails/active_record/enum.rb +81 -0
  14. data/lib/rbs_rails/active_record.rb +269 -171
  15. data/lib/rbs_rails/cli/configuration.rb +66 -0
  16. data/lib/rbs_rails/cli.rb +173 -0
  17. data/lib/rbs_rails/dependency_builder.rb +29 -8
  18. data/lib/rbs_rails/path_helpers.rb +14 -2
  19. data/lib/rbs_rails/rake_task.rb +39 -41
  20. data/lib/rbs_rails/util/file_writer.rb +22 -0
  21. data/lib/rbs_rails/util.rb +17 -15
  22. data/lib/rbs_rails/version.rb +1 -1
  23. data/lib/rbs_rails.rb +5 -2
  24. data/rbs_collection.lock.yaml +273 -45
  25. data/rbs_collection.yaml +1 -18
  26. data/rbs_rails.gemspec +2 -1
  27. data/sig/{install_generator.rbs → generators/rbs_rails/install_generator.rbs} +2 -0
  28. data/sig/rbs_rails/active_record/enum.rbs +26 -0
  29. data/sig/rbs_rails/active_record.rbs +68 -46
  30. data/sig/rbs_rails/cli/configuration.rbs +37 -0
  31. data/sig/rbs_rails/cli.rbs +35 -0
  32. data/sig/rbs_rails/dependency_builder.rbs +8 -0
  33. data/sig/rbs_rails/path_helpers.rbs +13 -6
  34. data/sig/rbs_rails/rake_task.rbs +8 -7
  35. data/sig/rbs_rails/util/file_writer.rbs +16 -0
  36. data/sig/rbs_rails/util.rbs +7 -2
  37. data/sig/rbs_rails/utils/file_writer.rbs +4 -0
  38. data/sig/rbs_rails/version.rbs +5 -1
  39. data/sig/rbs_rails.rbs +6 -3
  40. metadata +33 -14
  41. data/sig/_internal/activerecord.rbs +0 -4
  42. data/sig/_internal/fileutils.rbs +0 -4
  43. data/sig/_internal/thor.rbs +0 -5
  44. data/sig/parser.rbs +0 -14
  45. data/sig/rake.rbs +0 -6
@@ -1,45 +1,58 @@
1
1
  module RbsRails
2
2
  module ActiveRecord
3
3
 
4
- def self.generatable?(klass)
4
+ # @rbs klass: untyped
5
+ def self.generatable?(klass) #: boolish
5
6
  return false if klass.abstract_class?
6
7
 
7
8
  klass.connection.table_exists?(klass.table_name)
8
9
  end
9
10
 
10
- def self.class_to_rbs(klass, dependencies: [])
11
- Generator.new(klass, dependencies: dependencies).generate
11
+ # @rbs klass: untyped
12
+ # @rbs dependencies: Array[String]
13
+ def self.class_to_rbs(klass) #: untyped
14
+ Generator.new(klass).generate
12
15
  end
13
16
 
14
17
  class Generator
15
- IGNORED_ENUM_KEYS = %i[_prefix _suffix _default _scopes]
18
+ IGNORED_ENUM_KEYS = %i[_prefix _suffix _default _scopes] #: Array[Symbol]
16
19
 
17
- def initialize(klass, dependencies:)
20
+ # @rbs @parse_model_file: nil | Parser::AST::Node
21
+ # @rbs @enum_definitions: Array[Hash[Symbol, untyped]]
22
+ # @rbs @klass_name: String
23
+
24
+ attr_reader :dependencies #: DependencyBuilder
25
+
26
+ # @rbs klass: singleton(ActiveRecord::Base) & Enum
27
+ def initialize(klass) #: untyped
18
28
  @klass = klass
19
- @dependencies = dependencies
20
- @klass_name = Util.module_name(klass)
29
+ @dependencies = DependencyBuilder.new
30
+ @klass_name = Util.module_name(klass, abs: false)
21
31
 
22
- namespaces = klass_name.split('::').tap{ |names| names.pop }
32
+ namespaces = klass_name(abs: false).split('::').tap{ |names| names.pop }
23
33
  @dependencies << namespaces.join('::') unless namespaces.empty?
24
34
  end
25
35
 
26
- def generate
36
+ def generate #: String
27
37
  Util.format_rbs klass_decl
28
38
  end
29
39
 
30
- private def klass_decl
40
+ private def klass_decl #: String
31
41
  <<~RBS
42
+ # resolve-type-names: false
43
+
32
44
  #{header}
33
- extend _ActiveRecord_Relation_ClassMethods[#{klass_name}, #{relation_class_name}, #{pk_type}]
45
+ extend ::ActiveRecord::Base::ClassMethods[#{klass_name}, #{relation_class_name}, #{pk_type}]
34
46
 
35
47
  #{columns}
48
+ #{alias_columns}
36
49
  #{associations}
37
50
  #{generated_association_methods}
38
51
  #{has_secure_password}
39
52
  #{delegated_type_instance}
40
53
  #{delegated_type_scope(singleton: true)}
41
54
  #{enum_instance_methods}
42
- #{enum_scope_methods(singleton: true)}
55
+ #{enum_class_methods(singleton: true)}
43
56
  #{scopes(singleton: true)}
44
57
 
45
58
  #{generated_relation_methods_decl}
@@ -49,80 +62,106 @@ module RbsRails
49
62
  #{collection_proxy_decl}
50
63
 
51
64
  #{footer}
65
+
66
+ #{dependencies.build}
52
67
  RBS
53
68
  end
54
69
 
55
- private def pk_type
70
+ private def pk_type #: String
56
71
  pk = klass.primary_key
57
72
  return 'top' unless pk
58
73
 
59
- col = klass.columns.find {|col| col.name == pk }
60
- sql_type_to_class(col.type)
74
+ case klass.primary_key
75
+ when Array
76
+ types = klass.columns
77
+ .select { |column| klass.primary_key.include?(column.name) }
78
+ .map { |pk| sql_type_to_class(pk.type) }
79
+ "[#{types.join(' , ')}]"
80
+ else
81
+ col = klass.columns.find { |column| column.name == pk }
82
+ sql_type_to_class(col.type)
83
+ end
61
84
  end
62
85
 
63
- private def generated_relation_methods_decl
86
+ private def generated_relation_methods_decl #: String
64
87
  <<~RBS
65
- module GeneratedRelationMethods
66
- #{enum_scope_methods(singleton: false)}
88
+ module #{generated_relation_methods_name}
89
+ #{enum_class_methods(singleton: false)}
67
90
  #{scopes(singleton: false)}
68
91
  #{delegated_type_scope(singleton: false)}
69
92
  end
70
93
  RBS
71
94
  end
72
95
 
73
- private def relation_decl
96
+ private def relation_decl #: String
74
97
  <<~RBS
75
98
  class #{relation_class_name} < ::ActiveRecord::Relation
76
- include GeneratedRelationMethods
77
- include _ActiveRecord_Relation[#{klass_name}, #{pk_type}]
78
- include Enumerable[#{klass_name}]
99
+ include ::Enumerable[#{klass_name}]
100
+ include #{generated_relation_methods_name}
101
+ include ::ActiveRecord::Relation::Methods[#{klass_name}, #{pk_type}]
79
102
  end
80
103
  RBS
81
104
  end
82
105
 
83
- private def collection_proxy_decl
106
+ private def collection_proxy_decl #: String
84
107
  <<~RBS
85
- class ActiveRecord_Associations_CollectionProxy < ::ActiveRecord::Associations::CollectionProxy
86
- include GeneratedRelationMethods
87
- include _ActiveRecord_Relation[#{klass_name}, #{pk_type}]
108
+ class #{klass_name}::ActiveRecord_Associations_CollectionProxy < ::ActiveRecord::Associations::CollectionProxy
109
+ include ::Enumerable[#{klass_name}]
110
+ include #{generated_relation_methods_name}
111
+ include ::ActiveRecord::Relation::Methods[#{klass_name}, #{pk_type}]
112
+
113
+ def build: (?::ActiveRecord::Associations::CollectionProxy::_EachPair attributes) ?{ () -> untyped } -> #{klass_name}
114
+ | (::Array[::ActiveRecord::Associations::CollectionProxy::_EachPair] attributes) ?{ () -> untyped } -> ::Array[#{klass_name}]
115
+ def create: (?::ActiveRecord::Associations::CollectionProxy::_EachPair attributes) ?{ () -> untyped } -> #{klass_name}
116
+ | (::Array[::ActiveRecord::Associations::CollectionProxy::_EachPair] attributes) ?{ () -> untyped } -> ::Array[#{klass_name}]
117
+ def create!: (?::ActiveRecord::Associations::CollectionProxy::_EachPair attributes) ?{ () -> untyped } -> #{klass_name}
118
+ | (::Array[::ActiveRecord::Associations::CollectionProxy::_EachPair] attributes) ?{ () -> untyped } -> ::Array[#{klass_name}]
119
+ def reload: () -> ::Array[#{klass_name}]
120
+
121
+ def replace: (::Array[#{klass_name}]) -> void
122
+ def delete: (*#{klass_name} | #{pk_type}) -> ::Array[#{klass_name}]
123
+ def destroy: (*#{klass_name} | #{pk_type}) -> ::Array[#{klass_name}]
124
+ def <<: (*#{klass_name} | ::Array[#{klass_name}]) -> self
125
+ def prepend: (*#{klass_name} | ::Array[#{klass_name}]) -> self
88
126
  end
89
127
  RBS
90
128
  end
91
129
 
92
- private def header
130
+ private def header #: String
93
131
  namespace = +''
94
- klass_name.split('::').map do |mod_name|
132
+ klass_name(abs: false).split('::').map do |mod_name|
95
133
  namespace += "::#{mod_name}"
96
134
  mod_object = Object.const_get(namespace)
97
135
  case mod_object
98
136
  when Class
99
137
  # @type var superclass: Class
100
138
  superclass = _ = mod_object.superclass
101
- superclass_name = Util.module_name(superclass)
139
+ superclass_name = Util.module_name(superclass, abs: false)
102
140
  @dependencies << superclass_name
103
141
 
104
- "class #{mod_name} < ::#{superclass_name}"
142
+ "class #{namespace} < ::#{superclass_name}"
105
143
  when Module
106
- "module #{mod_name}"
144
+ "module #{namespace}"
107
145
  else
108
146
  raise 'unreachable'
109
147
  end
110
148
  end.join("\n")
111
149
  end
112
150
 
113
- private def footer
114
- "end\n" * klass_name.split('::').size
151
+ private def footer #: String
152
+ "end\n" * klass_name(abs: false).split('::').size
115
153
  end
116
154
 
117
- private def associations
155
+ private def associations #: String
118
156
  [
119
157
  has_many,
158
+ has_and_belongs_to_many,
120
159
  has_one,
121
160
  belongs_to,
122
161
  ].join("\n")
123
162
  end
124
163
 
125
- private def has_many
164
+ private def has_many #: String
126
165
  klass.reflect_on_all_associations(:has_many).map do |a|
127
166
  @dependencies << a.klass.name
128
167
 
@@ -133,14 +172,32 @@ module RbsRails
133
172
 
134
173
  <<~RUBY.chomp
135
174
  def #{a.name}: () -> #{collection_type}
136
- def #{a.name}=: (#{collection_type} | Array[#{type}]) -> (#{collection_type} | Array[#{type}])
137
- def #{singular_name}_ids: () -> Array[Integer]
138
- def #{singular_name}_ids=: (Array[Integer]) -> Array[Integer]
175
+ def #{a.name}=: (#{collection_type} | ::Array[#{type}]) -> (#{collection_type} | ::Array[#{type}])
176
+ def #{singular_name}_ids: () -> ::Array[::Integer]
177
+ def #{singular_name}_ids=: (::Array[::Integer]) -> ::Array[::Integer]
178
+ RUBY
179
+ end.join("\n")
180
+ end
181
+
182
+ private def has_and_belongs_to_many #: String
183
+ klass.reflect_on_all_associations(:has_and_belongs_to_many).map do |a|
184
+ @dependencies << a.klass.name
185
+
186
+ singular_name = a.name.to_s.singularize
187
+ type = Util.module_name(a.klass)
188
+ collection_type = "#{type}::ActiveRecord_Associations_CollectionProxy"
189
+ @dependencies << collection_type
190
+
191
+ <<~RUBY.chomp
192
+ def #{a.name}: () -> #{collection_type}
193
+ def #{a.name}=: (#{collection_type} | ::Array[#{type}]) -> (#{collection_type} | ::Array[#{type}])
194
+ def #{singular_name}_ids: () -> ::Array[::Integer]
195
+ def #{singular_name}_ids=: (::Array[::Integer]) -> ::Array[::Integer]
139
196
  RUBY
140
197
  end.join("\n")
141
198
  end
142
199
 
143
- private def has_one
200
+ private def has_one #: String
144
201
  klass.reflect_on_all_associations(:has_one).map do |a|
145
202
  @dependencies << a.klass.name unless a.polymorphic?
146
203
 
@@ -149,15 +206,15 @@ module RbsRails
149
206
  <<~RUBY.chomp
150
207
  def #{a.name}: () -> #{type_optional}
151
208
  def #{a.name}=: (#{type_optional}) -> #{type_optional}
152
- def build_#{a.name}: (untyped) -> #{type}
153
- def create_#{a.name}: (untyped) -> #{type}
154
- def create_#{a.name}!: (untyped) -> #{type}
209
+ def build_#{a.name}: (?untyped) -> #{type}
210
+ def create_#{a.name}: (?untyped) -> #{type}
211
+ def create_#{a.name}!: (?untyped) -> #{type}
155
212
  def reload_#{a.name}: () -> #{type_optional}
156
213
  RUBY
157
214
  end.join("\n")
158
215
  end
159
216
 
160
- private def belongs_to
217
+ private def belongs_to #: String
161
218
  klass.reflect_on_all_associations(:belongs_to).map do |a|
162
219
  @dependencies << a.klass.name unless a.polymorphic?
163
220
 
@@ -179,42 +236,43 @@ module RbsRails
179
236
  end.join("\n")
180
237
  end
181
238
 
182
- private def generated_association_methods
239
+ private def generated_association_methods #: String
183
240
  # @type var sigs: Array[String]
184
241
  sigs = []
185
242
 
186
243
  # Needs to require "active_storage/engine"
187
244
  if klass.respond_to?(:attachment_reflections)
188
- sigs << "module GeneratedAssociationMethods"
245
+ sigs << "module #{klass_name}::GeneratedAssociationMethods"
189
246
  sigs << klass.attachment_reflections.map do |name, reflection|
190
247
  case reflection.macro
191
248
  when :has_one_attached
192
249
  <<~EOS
193
- def #{name}: () -> ActiveStorage::Attached::One
194
- def #{name}=: (ActionDispatch::Http::UploadedFile) -> ActionDispatch::Http::UploadedFile
195
- | (Rack::Test::UploadedFile) -> Rack::Test::UploadedFile
196
- | (ActiveStorage::Blob) -> ActiveStorage::Blob
197
- | (String) -> String
198
- | ({ io: IO, filename: String, content_type: String? }) -> { io: IO, filename: String, content_type: String? }
250
+ def #{name}: () -> ::ActiveStorage::Attached::One
251
+ def #{name}=: (::ActionDispatch::Http::UploadedFile) -> ::ActionDispatch::Http::UploadedFile
252
+ | (::Rack::Test::UploadedFile) -> ::Rack::Test::UploadedFile
253
+ | (::ActiveStorage::Blob) -> ::ActiveStorage::Blob
254
+ | (::String) -> ::String
255
+ | ({ io: ::IO, filename: ::String, content_type: ::String? }) -> { io: ::IO, filename: ::String, content_type: ::String? }
199
256
  | (nil) -> nil
200
257
  EOS
201
258
  when :has_many_attached
202
259
  <<~EOS
203
- def #{name}: () -> ActiveStorage::Attached::Many
260
+ def #{name}: () -> ::ActiveStorage::Attached::Many
204
261
  def #{name}=: (untyped) -> untyped
205
262
  EOS
206
263
  else
207
- raise
264
+ raise "unknown macro: #{reflection.macro}"
208
265
  end
209
266
  end.join("\n")
210
267
  sigs << "end"
211
- sigs << "include GeneratedAssociationMethods"
268
+ sigs << "include #{klass_name}::GeneratedAssociationMethods"
212
269
  end
213
270
 
214
271
  sigs.join("\n")
215
272
  end
216
273
 
217
- private def delegated_type_scope(singleton:)
274
+ # @rbs singleton: bool
275
+ private def delegated_type_scope(singleton:) #: String
218
276
  definitions = delegated_type_definitions
219
277
  return "" unless definitions
220
278
  definitions.map do |definition|
@@ -225,14 +283,14 @@ module RbsRails
225
283
  end.flatten.join("\n")
226
284
  end
227
285
 
228
- private def delegated_type_instance
286
+ private def delegated_type_instance #: String
229
287
  definitions = delegated_type_definitions
230
288
  return "" unless definitions
231
289
  # @type var methods: Array[String]
232
290
  methods = []
233
291
  definitions.each do |definition|
234
- methods << "def #{definition[:role]}_class: () -> Class"
235
- methods << "def #{definition[:role]}_name: () -> String"
292
+ methods << "def #{definition[:role]}_class: () -> ::Class"
293
+ methods << "def #{definition[:role]}_name: () -> ::String"
236
294
  methods << definition[:types].map do |type|
237
295
  scope_name = type.tableize.gsub("/", "_")
238
296
  singular = scope_name.singularize
@@ -246,7 +304,7 @@ module RbsRails
246
304
  methods.join("\n")
247
305
  end
248
306
 
249
- private def delegated_type_definitions
307
+ private def delegated_type_definitions #: Array[{ role: Symbol, types: Array[String] }]?
250
308
  ast = parse_model_file
251
309
  return unless ast
252
310
 
@@ -285,7 +343,7 @@ module RbsRails
285
343
  end.compact
286
344
  end
287
345
 
288
- private def has_secure_password
346
+ private def has_secure_password #: String?
289
347
  ast = parse_model_file
290
348
  return unless ast
291
349
 
@@ -303,99 +361,47 @@ module RbsRails
303
361
  end
304
362
 
305
363
  <<~EOS
306
- module ActiveModel_SecurePassword_InstanceMethodsOnActivation_#{attribute}
307
- attr_reader #{attribute}: String?
308
- def #{attribute}=: (String) -> String
309
- def #{attribute}_confirmation=: (String) -> String
310
- def authenticate_#{attribute}: (String) -> (#{klass_name} | false)
364
+ module #{klass_name}::ActiveModel_SecurePassword_InstanceMethodsOnActivation_#{attribute}
365
+ attr_reader #{attribute}: ::String?
366
+ def #{attribute}=: (::String) -> ::String
367
+ def #{attribute}_confirmation=: (::String) -> ::String
368
+ def authenticate_#{attribute}: (::String) -> (#{klass_name} | false)
311
369
  #{attribute == :password ? "alias authenticate authenticate_password" : ""}
312
370
  end
313
- include ActiveModel_SecurePassword_InstanceMethodsOnActivation_#{attribute}
371
+ include #{klass_name}::ActiveModel_SecurePassword_InstanceMethodsOnActivation_#{attribute}
314
372
  EOS
315
373
  end.compact.join("\n")
316
374
  end
317
375
 
318
- private def enum_instance_methods
376
+ private def enum_instance_methods #: String
319
377
  # @type var methods: Array[String]
320
378
  methods = []
321
- enum_definitions.each do |hash|
322
- hash.each do |name, values|
323
- next if IGNORED_ENUM_KEYS.include?(name)
324
-
325
- values.each do |label, value|
326
- value_method_name = enum_method_name(hash, name, label)
327
- methods << "def #{value_method_name}!: () -> bool"
328
- methods << "def #{value_method_name}?: () -> bool"
329
- end
330
- end
379
+ klass.enum_definitions.each do |_, method_name|
380
+ methods << "def #{method_name}!: () -> bool"
381
+ methods << "def #{method_name}?: () -> bool"
331
382
  end
332
383
 
333
384
  methods.join("\n")
334
385
  end
335
386
 
336
- private def enum_scope_methods(singleton:)
387
+ # @rbs singleton: untyped
388
+ private def enum_class_methods(singleton:) #: String
337
389
  # @type var methods: Array[String]
338
390
  methods = []
339
- enum_definitions.each do |hash|
340
- hash.each do |name, values|
341
- next if IGNORED_ENUM_KEYS.include?(name)
342
-
343
- values.each do |label, value|
344
- value_method_name = enum_method_name(hash, name, label)
345
- methods << "def #{singleton ? 'self.' : ''}#{value_method_name}: () -> #{relation_class_name}"
346
- end
347
- end
348
- end
349
- methods.join("\n")
350
- end
351
-
352
- private def enum_definitions
353
- @enum_definitions ||= build_enum_definitions
354
- end
355
-
356
- # We need static analysis to detect enum.
357
- # ActiveRecord has `defined_enums` method,
358
- # but it does not contain _prefix and _suffix information.
359
- private def build_enum_definitions
360
- ast = parse_model_file
361
- return [] unless ast
362
-
363
- traverse(ast).map do |node|
364
- # @type block: nil | Hash[untyped, untyped]
365
- next unless node.type == :send
366
- next unless node.children[0].nil?
367
- next unless node.children[1] == :enum
368
-
369
- definitions = node.children[2]
370
- next unless definitions
371
- next unless definitions.type == :hash
372
- next unless traverse(definitions).all? { |n| [:str, :sym, :int, :hash, :pair, :true, :false].include?(n.type) }
373
-
374
- code = definitions.loc.expression.source
375
- code = "{#{code}}" if code[0] != '{'
376
- eval(code)
377
- end.compact
378
- end
379
-
380
- private def enum_method_name(hash, name, label)
381
- enum_prefix = hash[:_prefix]
382
- enum_suffix = hash[:_suffix]
383
-
384
- if enum_prefix == true
385
- prefix = "#{name}_"
386
- elsif enum_prefix
387
- prefix = "#{enum_prefix}_"
391
+ klass.enum_definitions.map(&:first).uniq.each do |name|
392
+ column = klass.columns_hash[name.to_s] || klass.columns_hash[klass.attribute_aliases[name.to_s]]
393
+ class_name = sql_type_to_class(column.type)
394
+ methods << "def #{singleton ? 'self.' : ''}#{name.to_s.pluralize}: () -> ::ActiveSupport::HashWithIndifferentAccess[::String, #{class_name}]"
388
395
  end
389
- if enum_suffix == true
390
- suffix = "_#{name}"
391
- elsif enum_suffix
392
- suffix = "_#{enum_suffix}"
396
+ klass.enum_definitions.each do |_, method_name|
397
+ methods << "def #{singleton ? 'self.' : ''}#{method_name}: () -> #{relation_class_name}"
398
+ methods << "def #{singleton ? 'self.' : ''}not_#{method_name}: () -> #{relation_class_name}"
393
399
  end
394
-
395
- "#{prefix}#{label}#{suffix}"
400
+ methods.join("\n")
396
401
  end
397
402
 
398
- private def scopes(singleton:)
403
+ # @rbs singleton: untyped
404
+ private def scopes(singleton:) #: untyped
399
405
  ast = parse_model_file
400
406
  return '' unless ast
401
407
 
@@ -429,7 +435,8 @@ module RbsRails
429
435
  sigs.join("\n")
430
436
  end
431
437
 
432
- private def args_to_type(args_node)
438
+ # @rbs args_node: untyped
439
+ private def args_to_type(args_node) #: untyped
433
440
  # @type var res: Array[String]
434
441
  res = []
435
442
  # @type var block: String?
@@ -457,97 +464,188 @@ module RbsRails
457
464
  "(#{res.join(", ")})#{block}"
458
465
  end
459
466
 
460
- private def parse_model_file
467
+ private def parse_model_file #: untyped
461
468
  return @parse_model_file if defined?(@parse_model_file)
462
469
 
463
- path = Rails.root.join('app/models/', klass_name.underscore + '.rb')
464
- return @parse_model_file = nil unless path.exist?
465
- return [] unless path.exist?
470
+ path, _line = Object.const_source_location(klass.name) rescue nil
471
+ return @parse_model_file = nil unless path
466
472
 
467
- ast = Parser::CurrentRuby.parse path.read
468
- return @parse_model_file = nil unless path.exist?
473
+ begin
474
+ @parse_model_file = parser_class.parse File.read(path)
475
+ rescue => e
476
+ @parse_model_file = nil
477
+ end
478
+ end
469
479
 
470
- @parse_model_file = ast
480
+ private def parser_class #: untyped
481
+ case RUBY_VERSION
482
+ when /\A3\.2\./
483
+ # backward campatibility
484
+ require 'parser/ruby32'
485
+ Parser::Ruby32
486
+ when /\A3\.3\./
487
+ Prism::Translation::Parser33 # steep:ignore
488
+ when /\A3\.4\./
489
+ Prism::Translation::Parser34 # steep:ignore
490
+ else
491
+ # For Prism v1.5.0+, Prism::Translation::ParserCurrent should be used instead.
492
+ Prism::Translation::Parser34 # steep:ignore
493
+ end
471
494
  end
472
495
 
496
+ #: (Parser::AST::Node) { (Parser::AST::Node) -> untyped } -> untyped
497
+ #: (Parser::AST::Node) -> Enumerator[Parser::AST::Node, untyped]
473
498
  private def traverse(node, &block)
474
- return to_enum(__method__ || raise, node) unless block_given?
499
+ return to_enum(__method__ || raise, node) unless block
475
500
 
476
- # @type var block: ^(Parser::AST::Node) -> untyped
477
501
  block.call node
478
502
  node.children.each do |child|
479
503
  traverse(child, &block) if child.is_a?(Parser::AST::Node)
480
504
  end
481
505
  end
482
506
 
483
- private def relation_class_name
484
- "ActiveRecord_Relation"
507
+ private def relation_class_name #: String
508
+ "#{klass_name}::ActiveRecord_Relation"
509
+ end
510
+
511
+ # @rbs abs: boolish
512
+ private def klass_name(abs: true) #: String
513
+ abs ? "::#{@klass_name}" : @klass_name
514
+ end
515
+
516
+ private def generated_relation_methods_name #: String
517
+ "#{klass_name}::GeneratedRelationMethods"
485
518
  end
486
519
 
487
- private def columns
488
- mod_sig = +"module GeneratedAttributeMethods\n"
520
+
521
+ private def columns #: untyped
522
+ mod_sig = +"module #{klass_name}::GeneratedAttributeMethods\n"
489
523
  mod_sig << klass.columns.map do |col|
490
- class_name = if enum_definitions.any? { |hash| hash.key?(col.name) || hash.key?(col.name.to_sym) }
491
- 'String'
492
- else
493
- sql_type_to_class(col.type)
494
- end
495
- class_name_opt = optional(class_name)
524
+ # NOTE:
525
+ # `klass.attribute_types[col.name].try(:coder)` is for Rails 6.0 and before
526
+ # `klass.attribute_types[col.name]&.instance_variable_get(:@coder)` is for Rails 6.1 and after
527
+ col_serializer = klass.attribute_types[col.name].try(:coder) ||
528
+ klass.attribute_types[col.name]&.instance_variable_get(:@coder)
529
+ # e.g. ActiveRecord::Coders::JSON
530
+ # if your model has `serialize ..., JSON`
531
+ # e.g. #<ActiveRecord::Coders::YAMLColumn:0x0000aaaafdc54970 @attr_name=..., @object_class=Array>
532
+ # if your model has `serialize ..., Array`
533
+ # etc.
534
+ col_serialize_to = col_serializer.try(:object_class)&.name
535
+ if col_serializer.is_a?(Class) && col_serializer.name == 'ActiveRecord::Coders::JSON'
536
+ class_name = 'untyped' # JSON
537
+ elsif col_serialize_to == 'Array'
538
+ class_name = '::Array[untyped]' # Array
539
+ elsif col_serialize_to == 'Hash'
540
+ class_name = '::Hash[untyped, untyped]' # Hash
541
+ else
542
+ class_name = if klass.enum_definitions.any? { |name, _| name == col.name.to_sym }
543
+ '::String'
544
+ else
545
+ sql_type_to_class(col.type)
546
+ end
547
+ end
548
+ sql_class_name = col.type == :datetime ? '::Time' : sql_type_to_class(col.type)
549
+ # If the DB says the column can be null, we need `<type>?`
550
+ # ...but if the type is already `untyped` there's no point in writing `untyped?`
551
+ class_name_opt = (class_name == 'untyped') ? 'untyped' : optional(class_name)
496
552
  column_type = col.null ? class_name_opt : class_name
553
+ sql_column_type = col.null ? optional(sql_class_name) : sql_class_name
497
554
  sig = <<~EOS
498
555
  def #{col.name}: () -> #{column_type}
499
556
  def #{col.name}=: (#{column_type}) -> #{column_type}
500
557
  def #{col.name}?: () -> bool
501
- def #{col.name}_changed?: () -> bool
558
+ def #{col.name}_changed?: (?from: #{class_name_opt}, ?to: #{class_name_opt}) -> bool
502
559
  def #{col.name}_change: () -> [#{class_name_opt}, #{class_name_opt}]
503
560
  def #{col.name}_will_change!: () -> void
504
561
  def #{col.name}_was: () -> #{class_name_opt}
505
- def #{col.name}_previously_changed?: () -> bool
506
- def #{col.name}_previous_change: () -> Array[#{class_name_opt}]?
562
+ def #{col.name}_previously_changed?: (?from: #{class_name_opt}, ?to: #{class_name_opt}) -> bool
563
+ def #{col.name}_previous_change: () -> ::Array[#{class_name_opt}]?
507
564
  def #{col.name}_previously_was: () -> #{class_name_opt}
508
565
  def #{col.name}_before_last_save: () -> #{class_name_opt}
509
- def #{col.name}_change_to_be_saved: () -> Array[#{class_name_opt}]?
566
+ def #{col.name}_change_to_be_saved: () -> ::Array[#{class_name_opt}]?
510
567
  def #{col.name}_in_database: () -> #{class_name_opt}
511
- def saved_change_to_#{col.name}: () -> Array[#{class_name_opt}]?
568
+ def saved_change_to_#{col.name}: () -> ::Array[#{class_name_opt}]?
512
569
  def saved_change_to_#{col.name}?: () -> bool
513
570
  def will_save_change_to_#{col.name}?: () -> bool
514
571
  def restore_#{col.name}!: () -> void
515
572
  def clear_#{col.name}_change: () -> void
573
+ def #{col.name}_before_type_cast: () -> #{sql_column_type}
574
+ def #{col.name}_for_database: () -> #{sql_column_type}
575
+ EOS
576
+ sig << "\n"
577
+ sig
578
+ end.join("\n")
579
+ mod_sig << "\nend\n"
580
+ mod_sig << "include #{klass_name}::GeneratedAttributeMethods"
581
+ mod_sig
582
+ end
583
+
584
+ private def alias_columns
585
+ attribute_aliases = klass.attribute_aliases.dup
586
+ attribute_aliases["id_value"] ||= "id" if klass.attribute_names.include?("id")
587
+
588
+ mod_sig = +"module #{klass_name}::GeneratedAliasAttributeMethods\n"
589
+ mod_sig << "include #{klass_name}::GeneratedAttributeMethods\n"
590
+ mod_sig << attribute_aliases.map do |col|
591
+ sig = <<~EOS
592
+ alias #{col[0]} #{col[1]}
593
+ alias #{col[0]}= #{col[1]}=
594
+ alias #{col[0]}? #{col[1]}?
595
+ alias #{col[0]}_changed? #{col[1]}_changed?
596
+ alias #{col[0]}_change #{col[1]}_change
597
+ alias #{col[0]}_will_change! #{col[1]}_will_change!
598
+ alias #{col[0]}_was #{col[1]}_was
599
+ alias #{col[0]}_previously_changed? #{col[1]}_previously_changed?
600
+ alias #{col[0]}_previous_change #{col[1]}_previous_change
601
+ alias #{col[0]}_previously_was #{col[1]}_previously_was
602
+ alias #{col[0]}_before_last_save #{col[1]}_before_last_save
603
+ alias #{col[0]}_change_to_be_saved #{col[1]}_change_to_be_saved
604
+ alias #{col[0]}_in_database #{col[1]}_in_database
605
+ alias saved_change_to_#{col[0]} saved_change_to_#{col[1]}
606
+ alias saved_change_to_#{col[0]}? saved_change_to_#{col[1]}?
607
+ alias will_save_change_to_#{col[0]}? will_save_change_to_#{col[1]}?
608
+ alias restore_#{col[0]}! restore_#{col[1]}!
609
+ alias clear_#{col[0]}_change clear_#{col[1]}_change
610
+ alias #{col[0]}_before_type_cast #{col[1]}_before_type_cast
611
+ alias #{col[0]}_for_database #{col[1]}_for_database
516
612
  EOS
517
613
  sig << "\n"
518
614
  sig
519
615
  end.join("\n")
520
616
  mod_sig << "\nend\n"
521
- mod_sig << "include GeneratedAttributeMethods"
617
+ mod_sig << "include #{klass_name}::GeneratedAliasAttributeMethods"
522
618
  mod_sig
523
619
  end
524
620
 
525
- private def optional(class_name)
621
+ # @rbs class_name: String
622
+ private def optional(class_name) #: String
526
623
  class_name.include?("|") ? "(#{class_name})?" : "#{class_name}?"
527
624
  end
528
625
 
529
- private def sql_type_to_class(t)
626
+ # @rbs t: untyped
627
+ private def sql_type_to_class(t) #: untyped
530
628
  case t
531
629
  when :integer
532
- 'Integer'
630
+ '::Integer'
533
631
  when :float
534
- 'Float'
632
+ '::Float'
535
633
  when :decimal
536
- 'BigDecimal'
634
+ '::BigDecimal'
537
635
  when :string, :text, :citext, :uuid, :binary
538
- 'String'
636
+ '::String'
539
637
  when :datetime
540
- 'ActiveSupport::TimeWithZone'
638
+ '::ActiveSupport::TimeWithZone'
541
639
  when :boolean
542
640
  "bool"
543
641
  when :jsonb, :json
544
642
  "untyped"
545
643
  when :date
546
- 'Date'
644
+ '::Date'
547
645
  when :time
548
- 'Time'
646
+ '::Time'
549
647
  when :inet
550
- "IPAddr"
648
+ "::IPAddr"
551
649
  else
552
650
  # Unknown column type, give up
553
651
  'untyped'
@@ -555,7 +653,7 @@ module RbsRails
555
653
  end
556
654
 
557
655
  private
558
- attr_reader :klass, :klass_name
656
+ attr_reader :klass #: singleton(ActiveRecord::Base) & Enum
559
657
  end
560
658
  end
561
659
  end