rbs_rails 0.2.0 → 0.6.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.gitmodules +3 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +3 -2
  6. data/README.md +33 -35
  7. data/Rakefile +14 -1
  8. data/Steepfile +12 -1
  9. data/assets/sig/action_mailer.rbs +6 -3
  10. data/assets/sig/capybara.rbs +14 -0
  11. data/assets/sig/concurrent.rbs +4 -0
  12. data/assets/sig/erb.rbs +4 -0
  13. data/assets/sig/erubi.rbs +4 -0
  14. data/assets/sig/i18n.rbs +4 -0
  15. data/assets/sig/minitest.rbs +12 -1
  16. data/assets/sig/pg.rbs +5 -0
  17. data/assets/sig/que.rbs +4 -0
  18. data/assets/sig/queue_classic.rbs +4 -0
  19. data/assets/sig/racc.rbs +4 -0
  20. data/assets/sig/rack-test.rbs +6 -0
  21. data/assets/sig/rack.rbs +47 -0
  22. data/assets/sig/rails.rbs +7 -8
  23. data/assets/sig/rdoc.rbs +9 -0
  24. data/assets/sig/sidekiq.rbs +4 -0
  25. data/assets/sig/sneakers.rbs +4 -0
  26. data/assets/sig/stdlib.rbs +15 -5
  27. data/assets/sig/sucker_punch.rbs +4 -0
  28. data/assets/sig/thor.rbs +12 -0
  29. data/assets/sig/tzinfo.rbs +4 -0
  30. data/bin/add-type-params.rb +39 -13
  31. data/bin/postprocess.rb +137 -0
  32. data/bin/rbs +30 -0
  33. data/bin/rbs-prototype-rb.rb +195 -0
  34. data/bin/to-ascii.rb +5 -0
  35. data/lib/rbs_rails/active_record.rb +78 -33
  36. data/lib/rbs_rails/rake_task.rb +75 -0
  37. data/lib/rbs_rails/version.rb +1 -1
  38. data/rbs_rails.gemspec +1 -0
  39. data/sig/fileutils.rbs +1 -0
  40. data/sig/rake.rbs +6 -0
  41. data/sig/rbs_rails/active_record.rbs +4 -4
  42. data/sig/rbs_rails/rake_task.rbs +20 -0
  43. metadata +45 -12
  44. data/assets/sig/action_controller.rbs +0 -44
  45. data/assets/sig/action_view.rbs +0 -3
  46. data/assets/sig/active_record.rbs +0 -130
  47. data/assets/sig/generated/activemodel.rbs +0 -3877
  48. data/assets/sig/generated/activesupport.rbs +0 -11480
  49. data/bin/merge-duplicate-decls.rb +0 -30
@@ -0,0 +1,137 @@
1
+ #!ruby
2
+
3
+ # TODO: Expose me to user
4
+
5
+ require 'bundler/inline'
6
+
7
+ gemfile do
8
+ source 'https://rubygems.org'
9
+ gem 'rbs', '1.0.0'
10
+ end
11
+
12
+ require 'rbs'
13
+ require 'rbs/cli'
14
+ require 'optparse'
15
+
16
+ def env(options:)
17
+ loader = options.loader
18
+ RBS::Environment.from_loader(loader).resolve_type_names
19
+ end
20
+
21
+ def parse_option(argv)
22
+ opt = OptionParser.new
23
+ options = RBS::CLI::LibraryOptions.new
24
+ options.setup_library_options(opt)
25
+
26
+ return opt.parse(argv), options
27
+ end
28
+
29
+ class FileMatcher
30
+ def initialize(targets:)
31
+ base_dir = Dir.pwd
32
+ @targets = targets + targets.map { |t| File.expand_path(t, base_dir) }
33
+ end
34
+
35
+ def match?(fname)
36
+ @targets.any? { |t| fname.start_with?(t) }
37
+ end
38
+ end
39
+
40
+ def class_method_name(concern)
41
+ RBS::TypeName.new(namespace: concern.name.to_namespace, name: :ClassMethods)
42
+ end
43
+
44
+ def process(decl, env:, builder:, update_targets:)
45
+ concerns = decl.members.select do |m|
46
+ next false unless m.is_a?(RBS::AST::Members::Include)
47
+ next false unless m.name.kind == :class
48
+
49
+ mod_entry = env.class_decls[m.name]
50
+ unless mod_entry
51
+ warn "unknown type: #{m.name}"
52
+ next false
53
+ end
54
+
55
+ a = builder.singleton_ancestors(m.name)
56
+ a.ancestors.any? { |ancestor| ancestor.name.to_s == '::ActiveSupport::Concern' }
57
+ end
58
+
59
+ concerns.each do |concern|
60
+ class_methods_name = class_method_name(concern)
61
+ class_methods_type = env.class_decls[class_methods_name]
62
+ next unless class_methods_type
63
+
64
+ # Skip if the decl already extend ClassMethods
65
+ a = builder.singleton_ancestors(decl.name)
66
+ next if a.ancestors.any? { |ancestor| ancestor.name == class_methods_name }
67
+
68
+ # TODO: Insert `extend class_methods_name` to decl
69
+ update_targets << [decl, concern]
70
+ end
71
+ end
72
+
73
+ def each_decl_descendant(decl:, path: [], &block)
74
+ return unless decl.is_a?(RBS::AST::Declarations::Class) || decl.is_a?(RBS::AST::Declarations::Module)
75
+
76
+ block.call(decl: decl, path: path)
77
+ path = [*path, decl]
78
+ decl.each_decl do |child|
79
+ each_decl_descendant(decl: child, path: path, &block)
80
+ end
81
+ end
82
+
83
+ def may_eql_member?(a, b)
84
+ a.name.to_s.split('::').last == b.name.to_s.split('::').last
85
+ end
86
+
87
+ def update!(update_targets:, only:)
88
+ update_targets.group_by { |decl, _concern| decl.location.name }.each do |fname, target_decls|
89
+ next unless only.match?(fname)
90
+
91
+ tree = RBS::Parser.parse_signature(File.read(fname))
92
+ target_decls.each do |target_decl, concern|
93
+ catch(:break) do
94
+ tree.each do |node|
95
+ each_decl_descendant(decl: node) do |decl:, path:|
96
+ next unless [relative = [*path, decl].map { |p| p.name.to_s }.join('::'), '::' + relative].include?(target_decl.name.to_s)
97
+
98
+ idx = decl.members.index { |m| may_eql_member?(m, concern) } || -1
99
+ extend = RBS::AST::Members::Extend.new(name: class_method_name(concern), args: [], annotations: [], location: nil, comment: nil)
100
+ decl.members.insert(idx + 1, extend)
101
+ throw :break
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ File.open(fname, 'w') do |f|
108
+ RBS::Writer.new(out: f).write(tree)
109
+ end
110
+ end
111
+ end
112
+
113
+ def run(argv)
114
+ targets, options = parse_option(argv)
115
+ env = env(options: options)
116
+ builder = RBS::DefinitionBuilder.new(env: env)
117
+ matcher = FileMatcher.new(targets: targets)
118
+
119
+ only = ENV['ONLY']&.then { Regexp.new(_1) } || //
120
+
121
+ update_targets = []
122
+
123
+ env.class_decls.each do |_name, entry|
124
+ entry.decls.each do |d|
125
+ decl = d.decl
126
+ loc = decl.location
127
+ fname = loc.name
128
+ next unless matcher.match?(fname)
129
+
130
+ process(decl, env: env, builder: builder, update_targets: update_targets)
131
+ end
132
+ end
133
+
134
+ update!(update_targets: update_targets, only: only)
135
+ end
136
+
137
+ run(ARGV)
data/bin/rbs ADDED
@@ -0,0 +1,30 @@
1
+ #!ruby
2
+
3
+ require 'pathname'
4
+ root = Pathname(__dir__) / '../'
5
+
6
+ def v(require)
7
+ if v = ENV['RAILS_VERSION']
8
+ "#{require}:#{v}"
9
+ else
10
+ require
11
+ end
12
+ end
13
+
14
+ def repo
15
+ ENV['RBS_REPO_DIR'] || Pathname(__dir__).join('../gem_rbs/gems').to_s
16
+ end
17
+
18
+ exec(
19
+ 'rbs',
20
+ # Require stdlibs
21
+ '-rlogger', '-rpathname', '-rmutex_m', '-rdate',
22
+ "--repo=#{repo}",
23
+ # Require Rails libraries
24
+ v('-ractivesupport'), v('-ractionpack'), v('-ractivejob'), v('-ractivemodel'), v('-ractionview'), v('-ractiverecord'), v('-rrailties'),
25
+ # Load signatures that are bundled in rbs_rails
26
+ '-I' + root.join('sig').to_s, '-I' + root.join('assets/sig').to_s,
27
+ # Expand arguments
28
+ *ARGV,
29
+ )
30
+
@@ -0,0 +1,195 @@
1
+ #!ruby
2
+
3
+ require 'rbs'
4
+ require 'rbs/cli'
5
+
6
+ using Module.new {
7
+ refine(Object) do
8
+ def const_name(node)
9
+ case node.type
10
+ when :CONST
11
+ node.children[0]
12
+ when :COLON2
13
+ base, name = node.children
14
+ base = const_name(base)
15
+ return unless base
16
+ "#{base}::#{name}"
17
+ end
18
+ end
19
+
20
+ def process_class_methods(node, decls:, comments:, context:)
21
+ return false unless node.type == :ITER
22
+
23
+ fcall = node.children[0]
24
+ return false unless fcall.children[0] == :class_methods
25
+
26
+ name = RBS::TypeName.new(name: :ClassMethods, namespace: RBS::Namespace.empty)
27
+ mod = RBS::AST::Declarations::Module.new(
28
+ name: name,
29
+ type_params: RBS::AST::Declarations::ModuleTypeParams.empty,
30
+ self_types: [],
31
+ members: [],
32
+ annotations: [],
33
+ location: nil,
34
+ comment: comments[node.first_lineno - 1]
35
+ )
36
+
37
+ decls.push mod
38
+
39
+ each_node [node.children[1]] do |child|
40
+ process child, decls: mod.members, comments: comments, context: RBS::Prototype::RB::Context.initial
41
+ end
42
+
43
+ true
44
+ end
45
+
46
+ def process_struct_new(node, decls:, comments:, context:)
47
+ return unless node.type == :CDECL
48
+
49
+ name, *_, rhs = node.children
50
+ fields, body = struct_new(rhs)
51
+ return unless fields
52
+
53
+ type_name = RBS::TypeName.new(name: name, namespace: RBS::Namespace.empty)
54
+ kls = RBS::AST::Declarations::Class.new(
55
+ name: type_name,
56
+ super_class: struct_as_superclass,
57
+ type_params: RBS::AST::Declarations::ModuleTypeParams.empty,
58
+ members: [],
59
+ annotations: [],
60
+ location: nil,
61
+ comment: comments[node.first_lineno - 1],
62
+ )
63
+ decls.push kls
64
+
65
+ fields.children.compact.each do |f|
66
+ case f.type
67
+ when :LIT, :STR
68
+ kls.members << RBS::AST::Members::AttrAccessor.new(
69
+ name: f.children.first,
70
+ type: untyped,
71
+ kind: :instance,
72
+ ivar_name: false,
73
+ annotations: [],
74
+ location: nil,
75
+ comment: nil,
76
+ )
77
+ end
78
+ end
79
+
80
+ if body
81
+ each_node [body] do |child|
82
+ process child, decls: kls.members, comments: comments, context: RBS::Prototype::RB::Context.initial
83
+ end
84
+ end
85
+
86
+ true
87
+ end
88
+
89
+ def process_attr_internal(node, decls:, comments:, context:)
90
+ case node.type
91
+ when :FCALL, :VCALL
92
+ args = node.children[1]&.children || []
93
+
94
+ case node.children[0]
95
+ when :attr_internal_reader
96
+ args.each do |arg|
97
+ if arg && (name = literal_to_symbol(arg))
98
+ decls << RBS::AST::Members::AttrReader.new(
99
+ name: name,
100
+ ivar_name: :"@_#{name}",
101
+ type: RBS::Types::Bases::Any.new(location: nil),
102
+ kind: context.attribute_kind,
103
+ location: nil,
104
+ comment: comments[node.first_lineno - 1],
105
+ annotations: []
106
+ )
107
+ end
108
+ end
109
+ when :attr_internal_writer
110
+ args.each do |arg|
111
+ if arg && (name = literal_to_symbol(arg))
112
+ decls << RBS::AST::Members::AttrWriter.new(
113
+ name: name,
114
+ ivar_name: :"@_#{name}",
115
+ type: RBS::Types::Bases::Any.new(location: nil),
116
+ kind: context.attribute_kind,
117
+ location: nil,
118
+ comment: comments[node.first_lineno - 1],
119
+ annotations: []
120
+ )
121
+ end
122
+ end
123
+ when :attr_internal_accessor, :attr_internal
124
+ args.each do |arg|
125
+ if arg && (name = literal_to_symbol(arg))
126
+ decls << RBS::AST::Members::AttrAccessor.new(
127
+ name: name,
128
+ ivar_name: :"@_#{name}",
129
+ type: RBS::Types::Bases::Any.new(location: nil),
130
+ kind: context.attribute_kind,
131
+ location: nil,
132
+ comment: comments[node.first_lineno - 1],
133
+ annotations: []
134
+ )
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ def class_new_method_to_type(node)
142
+ case node.type
143
+ when :CALL
144
+ recv, name, _args = node.children
145
+ return unless name == :new
146
+
147
+ klass = const_name(recv)
148
+ return unless klass
149
+
150
+ type_name = RBS::TypeName.new(name: klass, namespace: RBS::Namespace.empty)
151
+ RBS::Types::ClassInstance.new(name: type_name, args: [], location: nil)
152
+ end
153
+ end
154
+
155
+ def struct_new(node)
156
+ case node.type
157
+ when :CALL
158
+ # ok
159
+ when :ITER
160
+ call, block = node.children
161
+ return struct_new(call)&.tap do |r|
162
+ r << block
163
+ end
164
+ else
165
+ return
166
+ end
167
+
168
+ recv, method_name, args = node.children
169
+ return unless method_name == :new
170
+ return unless recv.type == :CONST || recv.type == :COLON3
171
+ return unless recv.children.first == :Struct
172
+
173
+ [args]
174
+ end
175
+
176
+ def struct_as_superclass
177
+ name = RBS::TypeName.new(name: 'Struct', namespace: RBS::Namespace.root)
178
+ RBS::AST::Declarations::Class::Super.new(name: name, args: ['untyped'], location: nil)
179
+ end
180
+ end
181
+ }
182
+
183
+ module PrototypeExt
184
+ def process(...)
185
+ process_class_methods(...) || process_struct_new(...) || process_attr_internal(...) || super
186
+ end
187
+
188
+ def literal_to_type(node)
189
+ class_new_method_to_type(node) || super
190
+ end
191
+ end
192
+
193
+ RBS::Prototype::RB.prepend PrototypeExt
194
+
195
+ RBS::CLI.new(stdout: STDOUT, stderr: STDERR).run(ARGV.dup)
@@ -0,0 +1,5 @@
1
+ #!ruby
2
+
3
+ ARGV.each do |p|
4
+ File.write p, File.read(p).gsub(/[^[:ascii:]]+/, '(trim non-ascii characters)')
5
+ end
@@ -1,13 +1,12 @@
1
1
  module RbsRails
2
2
  module ActiveRecord
3
- def self.class_to_rbs(klass, mode:)
4
- Generator.new(klass, mode: mode).generate
3
+ def self.class_to_rbs(klass)
4
+ Generator.new(klass).generate
5
5
  end
6
6
 
7
7
  class Generator
8
- def initialize(klass, mode:)
8
+ def initialize(klass)
9
9
  @klass = klass
10
- @mode = mode
11
10
  end
12
11
 
13
12
  def generate
@@ -36,7 +35,7 @@ module RbsRails
36
35
  <<~RBS
37
36
  class #{relation_class_name} < ActiveRecord::Relation
38
37
  include _ActiveRecord_Relation[#{klass.name}]
39
- include Enumerable[#{klass.name}, self]
38
+ include Enumerable[#{klass.name}]
40
39
  #{enum_scope_methods(singleton: false).indent(2)}
41
40
  #{scopes(singleton: false).indent(2)}
42
41
  end
@@ -52,16 +51,9 @@ module RbsRails
52
51
 
53
52
 
54
53
  private def header
55
- case mode
56
- when :extension
57
- "extension #{klass.name} (RbsRails)"
58
- when :class
59
- # @type var superclass: Class
60
- superclass = _ = klass.superclass
61
- "class #{klass.name} < #{superclass.name}"
62
- else
63
- raise "unexpected mode: #{mode}"
64
- end
54
+ # @type var superclass: Class
55
+ superclass = _ = klass.superclass
56
+ "class #{klass.name} < #{superclass.name}"
65
57
  end
66
58
 
67
59
  private def associations
@@ -74,21 +66,45 @@ module RbsRails
74
66
 
75
67
  private def has_many
76
68
  klass.reflect_on_all_associations(:has_many).map do |a|
77
- "def #{a.name}: () -> #{a.klass.name}::ActiveRecord_Associations_CollectionProxy"
69
+ singular_name = a.name.to_s.singularize
70
+ type = a.klass.name
71
+ collection_type = "#{type}::ActiveRecord_Associations_CollectionProxy"
72
+ <<~RUBY.chomp
73
+ def #{a.name}: () -> #{collection_type}
74
+ def #{a.name}=: (#{collection_type} | Array[#{type}]) -> (#{collection_type} | Array[#{type}])
75
+ def #{singular_name}_ids: () -> Array[Integer]
76
+ def #{singular_name}_ids=: (Array[Integer]) -> Array[Integer]
77
+ RUBY
78
78
  end.join("\n")
79
79
  end
80
80
 
81
81
  private def has_one
82
82
  klass.reflect_on_all_associations(:has_one).map do |a|
83
83
  type = a.polymorphic? ? 'untyped' : a.klass.name
84
- "def #{a.name}: () -> #{type}"
84
+ type_optional = optional(type)
85
+ <<~RUBY.chomp
86
+ def #{a.name}: () -> #{type}
87
+ def #{a.name}=: (#{type_optional}) -> #{type_optional}
88
+ def build_#{a.name}: (untyped) -> #{type}
89
+ def create_#{a.name}: (untyped) -> #{type}
90
+ def create_#{a.name}!: (untyped) -> #{type}
91
+ def reload_#{a.name}: () -> #{type_optional}
92
+ RUBY
85
93
  end.join("\n")
86
94
  end
87
95
 
88
96
  private def belongs_to
89
97
  klass.reflect_on_all_associations(:belongs_to).map do |a|
90
98
  type = a.polymorphic? ? 'untyped' : a.klass.name
91
- "def #{a.name}: () -> #{type}"
99
+ type_optional = optional(type)
100
+ <<~RUBY.chomp
101
+ def #{a.name}: () -> #{type}
102
+ def #{a.name}=: (#{type_optional}) -> #{type_optional}
103
+ def build_#{a.name}: (untyped) -> #{type}
104
+ def create_#{a.name}: (untyped) -> #{type}
105
+ def create_#{a.name}!: (untyped) -> #{type}
106
+ def reload_#{a.name}: () -> #{type_optional}
107
+ RUBY
92
108
  end.join("\n")
93
109
  end
94
110
 
@@ -138,6 +154,7 @@ module RbsRails
138
154
  return [] unless ast
139
155
 
140
156
  traverse(ast).map do |node|
157
+ # @type block: nil | Hash[untyped, untyped]
141
158
  next unless node.type == :send
142
159
  next unless node.children[0].nil?
143
160
  next unless node.children[1] == :enum
@@ -176,6 +193,7 @@ module RbsRails
176
193
  return '' unless ast
177
194
 
178
195
  traverse(ast).map do |node|
196
+ # @type block: nil | String
179
197
  next unless node.type == :send
180
198
  next unless node.children[0].nil?
181
199
  next unless node.children[1] == :scope
@@ -246,38 +264,65 @@ module RbsRails
246
264
 
247
265
  private def columns
248
266
  klass.columns.map do |col|
249
- "attr_accessor #{col.name} (): #{sql_type_to_class(col.type)}"
267
+ class_name = if enum_definitions.any? { |hash| hash.key?(col.name) || hash.key?(col.name.to_sym) }
268
+ 'String'
269
+ else
270
+ sql_type_to_class(col.type)
271
+ end
272
+ class_name_opt = optional(class_name)
273
+ column_type = col.null ? class_name_opt : class_name
274
+ sig = <<~EOS
275
+ attr_accessor #{col.name} (): #{column_type}
276
+ def #{col.name}_changed?: () -> bool
277
+ def #{col.name}_change: () -> [#{class_name_opt}, #{class_name_opt}]
278
+ def #{col.name}_will_change!: () -> void
279
+ def #{col.name}_was: () -> #{class_name_opt}
280
+ def #{col.name}_previously_changed?: () -> bool
281
+ def #{col.name}_previous_change: () -> Array[#{class_name_opt}]?
282
+ def #{col.name}_previously_was: () -> #{class_name_opt}
283
+ def restore_#{col.name}!: () -> void
284
+ def clear_#{col.name}_change: () -> void
285
+ EOS
286
+ sig << "attr_accessor #{col.name}? (): #{class_name}\n" if col.type == :boolean
287
+ sig
250
288
  end.join("\n")
251
289
  end
252
290
 
291
+ private def optional(class_name)
292
+ class_name.include?("|") ? "(#{class_name})?" : "#{class_name}?"
293
+ end
294
+
253
295
  private def sql_type_to_class(t)
254
296
  case t
255
297
  when :integer
256
- Integer.name
298
+ 'Integer'
257
299
  when :float
258
- Float.name
259
- when :string, :text, :uuid, :binary
260
- String.name
300
+ 'Float'
301
+ when :decimal
302
+ 'BigDecimal'
303
+ when :string, :text, :citext, :uuid, :binary
304
+ 'String'
261
305
  when :datetime
262
- # TODO
263
- # ActiveSupport::TimeWithZone.name
264
- Time.name
306
+ 'ActiveSupport::TimeWithZone'
265
307
  when :boolean
266
- "TrueClass | FalseClass"
308
+ "bool"
267
309
  when :jsonb, :json
268
310
  "untyped"
269
311
  when :date
270
- # TODO
271
- # Date.name
272
- 'untyped'
312
+ 'Date'
313
+ when :time
314
+ 'Time'
315
+ when :inet
316
+ "IPAddr"
273
317
  else
274
- raise "unexpected: #{t.inspect}"
318
+ # Unknown column type, give up
319
+ 'untyped'
275
320
  end
276
321
  end
277
322
 
278
323
  private
279
- # @dynamic klass, mode
280
- attr_reader :klass, :mode
324
+ # @dynamic klass
325
+ attr_reader :klass
281
326
  end
282
327
  end
283
328
  end