rbs_rails 0.5.0 → 0.8.2

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.dependabot/config.yml +8 -0
  3. data/.github/workflows/ci.yml +29 -0
  4. data/.gitignore +4 -2
  5. data/.gitmodules +0 -3
  6. data/CHANGELOG.md +22 -0
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +61 -0
  9. data/README.md +16 -44
  10. data/Steepfile +3 -0
  11. data/bin/add-type-params.rb +2 -1
  12. data/bin/gem_rbs +94 -0
  13. data/bin/postprocess.rb +1 -1
  14. data/bin/rbs +1 -1
  15. data/bin/rbs-prototype-rb.rb +5 -1
  16. data/bin/setup +1 -0
  17. data/lib/rbs_rails.rb +4 -0
  18. data/lib/rbs_rails/active_record.rb +91 -46
  19. data/lib/rbs_rails/dependency_builder.rb +43 -0
  20. data/lib/rbs_rails/rake_task.rb +72 -0
  21. data/lib/rbs_rails/util.rb +25 -0
  22. data/lib/rbs_rails/version.rb +1 -1
  23. data/rbs_rails.gemspec +2 -2
  24. data/sig/activerecord.rbs +4 -0
  25. data/sig/fileutils.rbs +1 -0
  26. data/sig/rake.rbs +6 -0
  27. data/sig/rbs_rails/active_record.rbs +11 -2
  28. data/sig/rbs_rails/dependency_builder.rbs +9 -0
  29. data/sig/rbs_rails/rake_task.rbs +26 -0
  30. data/sig/rbs_rails/util.rbs +11 -0
  31. metadata +19 -30
  32. data/.travis.yml +0 -11
  33. data/assets/sig/action_mailer.rbs +0 -8
  34. data/assets/sig/builtin.rbs +0 -7
  35. data/assets/sig/capybara.rbs +0 -14
  36. data/assets/sig/concurrent.rbs +0 -4
  37. data/assets/sig/erb.rbs +0 -4
  38. data/assets/sig/erubi.rbs +0 -4
  39. data/assets/sig/i18n.rbs +0 -4
  40. data/assets/sig/libxml.rbs +0 -10
  41. data/assets/sig/minitest.rbs +0 -13
  42. data/assets/sig/nokogiri.rbs +0 -8
  43. data/assets/sig/pg.rbs +0 -5
  44. data/assets/sig/que.rbs +0 -4
  45. data/assets/sig/queue_classic.rbs +0 -4
  46. data/assets/sig/racc.rbs +0 -4
  47. data/assets/sig/rack-test.rbs +0 -6
  48. data/assets/sig/rack.rbs +0 -47
  49. data/assets/sig/rails.rbs +0 -14
  50. data/assets/sig/rdoc.rbs +0 -9
  51. data/assets/sig/sidekiq.rbs +0 -4
  52. data/assets/sig/sneakers.rbs +0 -4
  53. data/assets/sig/stdlib.rbs +0 -24
  54. data/assets/sig/sucker_punch.rbs +0 -4
  55. data/assets/sig/thor.rbs +0 -12
  56. data/assets/sig/tzinfo.rbs +0 -4
data/bin/setup CHANGED
@@ -4,5 +4,6 @@ IFS=$'\n\t'
4
4
  set -vx
5
5
 
6
6
  bundle install
7
+ bin/gem_rbs
7
8
 
8
9
  # Do any other automated setup that you need to do here
data/lib/rbs_rails.rb CHANGED
@@ -1,8 +1,12 @@
1
1
  require 'parser/current'
2
+ require 'rbs'
3
+ require 'stringio'
2
4
 
3
5
  require_relative "rbs_rails/version"
6
+ require_relative "rbs_rails/util"
4
7
  require_relative 'rbs_rails/active_record'
5
8
  require_relative 'rbs_rails/path_helpers'
9
+ require_relative 'rbs_rails/dependency_builder'
6
10
 
7
11
  module RbsRails
8
12
  class Error < StandardError; end
@@ -1,59 +1,97 @@
1
1
  module RbsRails
2
2
  module ActiveRecord
3
- def self.class_to_rbs(klass)
4
- Generator.new(klass).generate
3
+
4
+ def self.generatable?(klass)
5
+ return false if klass.abstract_class?
6
+
7
+ klass.connection.table_exists?(klass.table_name)
8
+ end
9
+
10
+ def self.class_to_rbs(klass, dependencies: [])
11
+ Generator.new(klass, dependencies: dependencies).generate
5
12
  end
6
13
 
7
14
  class Generator
8
- def initialize(klass)
15
+ def initialize(klass, dependencies:)
9
16
  @klass = klass
17
+ @dependencies = dependencies
18
+ @klass_name = Util.module_name(klass)
19
+
20
+ namespaces = klass_name.split('::').tap{ |names| names.pop }
21
+ @dependencies << namespaces.join('::') unless namespaces.empty?
10
22
  end
11
23
 
12
24
  def generate
13
- [
14
- klass_decl,
15
- relation_decl,
16
- collection_proxy_decl,
17
- ].join("\n")
25
+ Util.format_rbs klass_decl
18
26
  end
19
27
 
20
28
  private def klass_decl
21
29
  <<~RBS
22
30
  #{header}
23
- extend _ActiveRecord_Relation_ClassMethods[#{klass.name}, #{relation_class_name}]
31
+ extend _ActiveRecord_Relation_ClassMethods[#{klass_name}, #{relation_class_name}, #{pk_type}]
24
32
 
25
- #{columns.indent(2)}
26
- #{associations.indent(2)}
27
- #{enum_instance_methods.indent(2)}
28
- #{enum_scope_methods(singleton: true).indent(2)}
29
- #{scopes(singleton: true).indent(2)}
30
- end
33
+ #{columns}
34
+ #{associations}
35
+ #{enum_instance_methods}
36
+ #{enum_scope_methods(singleton: true)}
37
+ #{scopes(singleton: true)}
38
+
39
+ #{relation_decl}
40
+
41
+ #{collection_proxy_decl}
42
+
43
+ #{footer}
31
44
  RBS
32
45
  end
33
46
 
47
+ private def pk_type
48
+ pk = klass.primary_key
49
+ col = klass.columns.find {|col| col.name == pk }
50
+ sql_type_to_class(col.type)
51
+ end
52
+
34
53
  private def relation_decl
35
54
  <<~RBS
36
55
  class #{relation_class_name} < ActiveRecord::Relation
37
- include _ActiveRecord_Relation[#{klass.name}]
38
- include Enumerable[#{klass.name}]
39
- #{enum_scope_methods(singleton: false).indent(2)}
40
- #{scopes(singleton: false).indent(2)}
56
+ include _ActiveRecord_Relation[#{klass_name}, #{pk_type}]
57
+ include Enumerable[#{klass_name}]
58
+
59
+ #{enum_scope_methods(singleton: false)}
60
+ #{scopes(singleton: false)}
41
61
  end
42
62
  RBS
43
63
  end
44
64
 
45
65
  private def collection_proxy_decl
46
66
  <<~RBS
47
- class #{klass.name}::ActiveRecord_Associations_CollectionProxy < ActiveRecord::Associations::CollectionProxy
67
+ class ActiveRecord_Associations_CollectionProxy < ActiveRecord::Associations::CollectionProxy
48
68
  end
49
69
  RBS
50
70
  end
51
71
 
52
-
53
72
  private def header
54
- # @type var superclass: Class
55
- superclass = _ = klass.superclass
56
- "class #{klass.name} < #{superclass.name}"
73
+ namespace = +''
74
+ klass_name.split('::').map do |mod_name|
75
+ namespace += "::#{mod_name}"
76
+ mod_object = Object.const_get(namespace)
77
+ case mod_object
78
+ when Class
79
+ # @type var superclass: Class
80
+ superclass = _ = mod_object.superclass
81
+ superclass_name = Util.module_name(superclass)
82
+ @dependencies << superclass_name
83
+
84
+ "class #{mod_name} < #{superclass_name}"
85
+ when Module
86
+ "module #{mod_name}"
87
+ else
88
+ raise 'unreachable'
89
+ end
90
+ end.join("\n")
91
+ end
92
+
93
+ private def footer
94
+ "end\n" * klass_name.split('::').size
57
95
  end
58
96
 
59
97
  private def associations
@@ -67,7 +105,7 @@ module RbsRails
67
105
  private def has_many
68
106
  klass.reflect_on_all_associations(:has_many).map do |a|
69
107
  singular_name = a.name.to_s.singularize
70
- type = a.klass.name
108
+ type = Util.module_name(a.klass)
71
109
  collection_type = "#{type}::ActiveRecord_Associations_CollectionProxy"
72
110
  <<~RUBY.chomp
73
111
  def #{a.name}: () -> #{collection_type}
@@ -80,7 +118,7 @@ module RbsRails
80
118
 
81
119
  private def has_one
82
120
  klass.reflect_on_all_associations(:has_one).map do |a|
83
- type = a.polymorphic? ? 'untyped' : a.klass.name
121
+ type = a.polymorphic? ? 'untyped' : Util.module_name(a.klass)
84
122
  type_optional = optional(type)
85
123
  <<~RUBY.chomp
86
124
  def #{a.name}: () -> #{type}
@@ -95,7 +133,7 @@ module RbsRails
95
133
 
96
134
  private def belongs_to
97
135
  klass.reflect_on_all_associations(:belongs_to).map do |a|
98
- type = a.polymorphic? ? 'untyped' : a.klass.name
136
+ type = a.polymorphic? ? 'untyped' : Util.module_name(a.klass)
99
137
  type_optional = optional(type)
100
138
  <<~RUBY.chomp
101
139
  def #{a.name}: () -> #{type}
@@ -208,37 +246,42 @@ module RbsRails
208
246
  next unless body_node.type == :block
209
247
 
210
248
  args = args_to_type(body_node.children[1])
211
- "def #{singleton ? 'self.' : ''}#{name}: (#{args}) -> #{relation_class_name}"
249
+ "def #{singleton ? 'self.' : ''}#{name}: #{args} -> #{relation_class_name}"
212
250
  end.compact.join("\n")
213
251
  end
214
252
 
215
253
  private def args_to_type(args_node)
216
254
  # @type var res: Array[String]
217
255
  res = []
256
+ # @type var block: String?
257
+ block = nil
218
258
  args_node.children.each do |node|
219
259
  case node.type
220
260
  when :arg
221
- res << "untyped"
261
+ res << "untyped #{node.children[0]}"
222
262
  when :optarg
223
- res << "?untyped"
263
+ res << "?untyped #{node.children[0]}"
224
264
  when :kwarg
225
265
  res << "#{node.children[0]}: untyped"
226
266
  when :kwoptarg
227
267
  res << "?#{node.children[0]}: untyped"
268
+ when :restarg
269
+ res << "*untyped #{node.children[0]}"
270
+ when :kwrestarg
271
+ res << "**untyped #{node.children[0]}"
272
+ when :blockarg
273
+ block = " { (*untyped) -> untyped }"
228
274
  else
229
275
  raise "unexpected: #{node}"
230
276
  end
231
277
  end
232
- res.join(", ")
278
+ "(#{res.join(", ")})#{block}"
233
279
  end
234
280
 
235
281
  private def parse_model_file
236
282
  return @parse_model_file if defined?(@parse_model_file)
237
283
 
238
-
239
- # @type var class_name: String
240
- class_name = _ = klass.name
241
- path = Rails.root.join('app/models/', class_name.underscore + '.rb')
284
+ path = Rails.root.join('app/models/', klass_name.underscore + '.rb')
242
285
  return @parse_model_file = nil unless path.exist?
243
286
  return [] unless path.exist?
244
287
 
@@ -259,7 +302,7 @@ module RbsRails
259
302
  end
260
303
 
261
304
  private def relation_class_name
262
- "#{klass.name}::ActiveRecord_Relation"
305
+ "ActiveRecord_Relation"
263
306
  end
264
307
 
265
308
  private def columns
@@ -280,6 +323,12 @@ module RbsRails
280
323
  def #{col.name}_previously_changed?: () -> bool
281
324
  def #{col.name}_previous_change: () -> Array[#{class_name_opt}]?
282
325
  def #{col.name}_previously_was: () -> #{class_name_opt}
326
+ def #{col.name}_before_last_save: () -> #{class_name_opt}
327
+ def #{col.name}_change_to_be_saved: () -> Array[#{class_name_opt}]?
328
+ def #{col.name}_in_database: () -> #{class_name_opt}
329
+ def saved_change_to_#{col.name}: () -> Array[#{class_name_opt}]?
330
+ def saved_change_to_#{col.name}?: () -> bool
331
+ def will_save_change_to_#{col.name}?: () -> bool
283
332
  def restore_#{col.name}!: () -> void
284
333
  def clear_#{col.name}_change: () -> void
285
334
  EOS
@@ -303,29 +352,25 @@ module RbsRails
303
352
  when :string, :text, :citext, :uuid, :binary
304
353
  'String'
305
354
  when :datetime
306
- # TODO
307
- # ActiveSupport::TimeWithZone.name
308
- 'Time'
355
+ 'ActiveSupport::TimeWithZone'
309
356
  when :boolean
310
- "TrueClass | FalseClass"
357
+ "bool"
311
358
  when :jsonb, :json
312
359
  "untyped"
313
360
  when :date
314
- # TODO
315
- # Date.name
316
- 'untyped'
361
+ 'Date'
317
362
  when :time
318
363
  'Time'
319
364
  when :inet
320
365
  "IPAddr"
321
366
  else
322
- raise "unexpected: #{t.inspect}"
367
+ # Unknown column type, give up
368
+ 'untyped'
323
369
  end
324
370
  end
325
371
 
326
372
  private
327
- # @dynamic klass
328
- attr_reader :klass
373
+ attr_reader :klass, :klass_name
329
374
  end
330
375
  end
331
376
  end
@@ -0,0 +1,43 @@
1
+ module RbsRails
2
+ class DependencyBuilder
3
+ attr_reader :deps
4
+
5
+ def initialize
6
+ @deps = []
7
+ end
8
+
9
+ def build
10
+ dep_rbs = +""
11
+ done = Set.new(['ActiveRecord::Base', 'ActiveRecord', 'Object'])
12
+ deps.uniq!
13
+ while dep = deps.shift
14
+ next unless done.add?(dep)
15
+
16
+ case dep_object = Object.const_get(dep)
17
+ when Class
18
+ superclass = dep_object.superclass or raise
19
+ super_name = Util.module_name(superclass)
20
+ deps << super_name
21
+ dep_rbs << "class #{dep} < #{super_name} end\n"
22
+ when Module
23
+ dep_rbs << "module #{dep} end\n"
24
+ else
25
+ raise
26
+ end
27
+
28
+ # push namespaces
29
+ namespaces = dep.split('::')
30
+ namespaces.pop
31
+ namespaces.inject('') do |base, name|
32
+ full_name = base.empty? ? name : [base, name].join('::')
33
+ deps << full_name
34
+ full_name
35
+ end
36
+ end
37
+
38
+ unless dep_rbs.empty?
39
+ Util.format_rbs(dep_rbs)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,72 @@
1
+ require 'rake'
2
+ require 'rake/tasklib'
3
+
4
+ module RbsRails
5
+ class RakeTask < Rake::TaskLib
6
+ attr_accessor :ignore_model_if, :name, :signature_root_dir
7
+
8
+ def initialize(name = :rbs_rails, &block)
9
+ super()
10
+
11
+ @name = name
12
+
13
+ block.call(self) if block
14
+
15
+ setup_signature_root_dir!
16
+
17
+ def_generate_rbs_for_models
18
+ def_generate_rbs_for_path_helpers
19
+ def_all
20
+ end
21
+
22
+ def def_all
23
+ desc 'Run all tasks of rbs_rails'
24
+
25
+ deps = [:"#{name}:generate_rbs_for_models", :"#{name}:generate_rbs_for_path_helpers"]
26
+ task("#{name}:all": deps)
27
+ end
28
+
29
+ def def_generate_rbs_for_models
30
+ desc 'Generate RBS files for Active Record models'
31
+ task("#{name}:generate_rbs_for_models": :environment) do
32
+ require 'rbs_rails'
33
+
34
+ Rails.application.eager_load!
35
+
36
+ dep_builder = DependencyBuilder.new
37
+
38
+ ::ActiveRecord::Base.descendants.each do |klass|
39
+ next unless RbsRails::ActiveRecord.generatable?(klass)
40
+ next if ignore_model_if&.call(klass)
41
+
42
+ path = signature_root_dir / "app/models/#{klass.name.underscore}.rbs"
43
+ path.dirname.mkpath
44
+
45
+ sig = RbsRails::ActiveRecord.class_to_rbs(klass, dependencies: dep_builder.deps)
46
+ path.write sig
47
+ end
48
+
49
+ if dep_rbs = dep_builder.build
50
+ signature_root_dir.join('model_dependencies.rbs').write(dep_rbs)
51
+ end
52
+ end
53
+ end
54
+
55
+ def def_generate_rbs_for_path_helpers
56
+ desc 'Generate RBS files for path helpers'
57
+ task("#{name}:generate_rbs_for_path_helpers": :environment) do
58
+ require 'rbs_rails'
59
+
60
+ out_path = signature_root_dir.join 'path_helpers.rbs'
61
+ rbs = RbsRails::PathHelpers.generate
62
+ out_path.write rbs
63
+ end
64
+ end
65
+
66
+ private def setup_signature_root_dir!
67
+ @signature_root_dir ||= Rails.root / 'sig/rbs_rails'
68
+ @signature_root_dir = Pathname(@signature_root_dir)
69
+ @signature_root_dir.mkpath
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,25 @@
1
+ module RbsRails
2
+ module Util
3
+ MODULE_NAME = Module.instance_method(:name)
4
+
5
+ extend self
6
+
7
+ if '2.7' <= RUBY_VERSION
8
+ def module_name(mod)
9
+ # HACK: RBS doesn't have UnboundMethod#bind_call
10
+ (_ = MODULE_NAME).bind_call(mod)
11
+ end
12
+ else
13
+ def module_name(mod)
14
+ MODULE_NAME.bind(mod).call
15
+ end
16
+ end
17
+
18
+ def format_rbs(rbs)
19
+ decls = RBS::Parser.parse_signature(rbs)
20
+ StringIO.new.tap do |io|
21
+ RBS::Writer.new(out: io).write(decls)
22
+ end.string
23
+ end
24
+ end
25
+ end
@@ -2,5 +2,5 @@ module RbsRails
2
2
  # Because of copy_signatures is defined by lib/rbs_rails.rb
3
3
  # @dynamic self.copy_signatures
4
4
 
5
- VERSION = "0.5.0"
5
+ VERSION = "0.8.2"
6
6
  end
data/rbs_rails.gemspec CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
 
15
15
  spec.metadata["homepage_uri"] = spec.homepage
16
16
  spec.metadata["source_code_uri"] = spec.homepage
17
- # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
17
+ spec.metadata["changelog_uri"] = "https://github.com/pocke/rbs_rails/blob/master/CHANGELOG.md"
18
18
 
19
19
  # Specify which files should be added to the gem when it is released.
20
20
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -26,5 +26,5 @@ Gem::Specification.new do |spec|
26
26
  spec.require_paths = ["lib"]
27
27
 
28
28
  spec.add_runtime_dependency 'parser'
29
- spec.add_runtime_dependency 'rbs', '>= 0.17'
29
+ spec.add_runtime_dependency 'rbs', '>= 1'
30
30
  end
@@ -0,0 +1,4 @@
1
+ # TODO: Define it in gem_rbs_collection repository
2
+ class ActiveRecord::Base
3
+ def self.primary_key: () -> String
4
+ end