rbs_rails 0.2.0 → 0.6.0

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