Class: RBS::Inline::Annotator::Visitor

Inherits:
Prism::Visitor
  • Object
show all
Defined in:
lib/rbs/inline/annotator/visitor.rb

Instance Method Summary collapse

Constructor Details

#initialize(env:, result:) ⇒ Visitor

Returns a new instance of Visitor.



3
4
5
6
7
8
9
# File 'lib/rbs/inline/annotator/visitor.rb', line 3

def initialize(env:, result:)
  @env = env
  @result = result
  @stack = []
  @kind = :instance
  super()
end

Instance Method Details

#add_rbs_inline_annotation_for_def_node(node:, method_definition:) ⇒ Object



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/rbs/inline/annotator/visitor.rb', line 186

def add_rbs_inline_annotation_for_def_node(node:, method_definition:)
  indent = " " * node.location.start_column

  if method_definition.annotations.any?
    method_definition.annotations.each do |a|
      insert_before(node_range(node), "# @rbs %a{#{a.string}}\n#{indent}")
    end
  end

  if method_definition.overloading
    insert_before(node_range(node), "# @rbs overload\n#{indent}")
    return
  end

  if method_definition.overloads.length > 1
    # multiple overloads
    method_definition.overloads.each_with_index do |overload, index|
      text = if index == 0
               "# @rbs #{overload.method_type}\n#{indent}"
             else
               "#    | #{overload.method_type}\n#{indent}"
             end

      insert_before(node_range(node), text)
    end
  else
    overload = method_definition.overloads.first
    method_type = overload.method_type
    func = method_type.type
    indent = " " * node.location.start_column
    new_annotation = lambda { |name, type|
      insert_before(node_range(node), "# @rbs #{name}: #{type}\n#{indent}")
    }
    for_positional_params = lambda { |orig_sig, orig_ruby|
      sig = orig_sig.dup or break
      ruby = orig_ruby.dup or break
      sig.each do |rbs|
        rb = ruby.shift
        if rbs.name
          name = rbs.name
        else
          if rb.nil?
            s = (node.receiver.is_a?(Prism::SelfNode) || @kind == :singleton) ? "." : "#"
            warn "Parameter mismatch #{type_name}#{s}#{node.name}"
            break
          end
          name = rb.name
        end

        type = rbs.type.to_s
        next if type == "untyped"

        new_annotation.call(name, type)
      end
    }
    for_keyword_params = lambda { |orig_sig, _orig_ruby|
      sig = orig_sig.dup or break
      # ruby = orig_ruby.dup or break
      sig.each do |name, param|
        type = param.type.to_s
        next if type == "untyped"

        new_annotation.call(name, type)
      end
    }
    for_rest_param = lambda { |prefix, sig, ruby|
      name = sig.name || ruby.name
      type = sig.type.to_s
      next if type == "untyped"

      new_annotation.call("#{prefix}#{name}", type)
    }
    for_return_param = lambda { |return_type|
      return_type = return_type.to_s
      break if return_type == "untyped"

      new_annotation.call("return", return_type)
    }
    if node.parameters
      case func
      when RBS::Types::UntypedFunction
        # do nothing
      when RBS::Types::Function
        for_positional_params.call(func.required_positionals, node.parameters.requireds)
        for_positional_params.call(func.optional_positionals, node.parameters.optionals)
        for_rest_param.call("*", func.rest_positionals, node.parameters.rest) if func.rest_positionals
        for_positional_params.call(func.trailing_positionals, node.parameters.posts) if func.trailing_positionals
        for_keyword_params.call(func.required_keywords, node.parameters.keywords.grep(Prism::RequiredKeywordParameterNode))
        for_keyword_params.call(func.optional_keywords, node.parameters.keywords.grep(Prism::OptionalKeywordParameterNode))
        for_rest_param.call("**", func.rest_keywords, node.parameters.keyword_rest) if func.rest_keywords
      end
    end
    if method_type.block
      name = node.parameters&.block&.name
      block_source = method_type.block.location.source
      # "?{ (Integer) -> Integer } -> void"
      # -> "? (Integer) -> Integer"
      block_source = block_source.gsub(/[{}]/, "").strip
      new_annotation.call("&#{name}", block_source)
    end
    for_return_param.call(func.return_type)
  end
end

#add_rbs_inline_annotation_for_trailing(node:, type:) ⇒ Object



462
463
464
465
466
# File 'lib/rbs/inline/annotator/visitor.rb', line 462

def add_rbs_inline_annotation_for_trailing(node:, type:)
  return if type == "untyped"

  insert_after(node_range(node), " #: #{type}")
end

#constant_type(node, type_name) ⇒ Object



425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
# File 'lib/rbs/inline/annotator/visitor.rb', line 425

def constant_type(node, type_name)
  entry = @env.constant_entry(type_name) or return
  case entry
  when RBS::Environment::ModuleAliasEntry
    case node.value
    when Prism::CallNode
      "module-alias #{entry.decl.old_name}"
    when Prism::ConstantReadNode, Prism::ConstantPathNode
      "module-alias"
    else
      warn "Unsupported node: #{node.value.class}"
    end
  when RBS::Environment::ClassAliasEntry
    case node.value
    when Prism::CallNode
      "class-alias #{entry.decl.old_name}"
    when Prism::ConstantReadNode, Prism::ConstantPathNode
      "class-alias"
    else
      warn "Unsupported node: #{node.value.class}"
    end
  when RBS::Environment::ConstantEntry
    t = entry.decl.type.to_s
    if (node.value.is_a?(Prism::IntegerNode) && t == "Integer" ||
        node.value.is_a?(Prism::FloatNode) && t == "Float" ||
        node.value.is_a?(Prism::TrueNode) && t == "bool" ||
        node.value.is_a?(Prism::FalseNode) && t == "bool" ||
        node.value.is_a?(Prism::StringNode) && t == "String" ||
        node.value.is_a?(Prism::SymbolNode) && t == ":#{node.value.value}")
      # The types of constants may be automatically inferred
      nil
    else
      t
    end
  end
end

#header_node(node) ⇒ Object



80
81
82
83
84
85
86
87
# File 'lib/rbs/inline/annotator/visitor.rb', line 80

def header_node(node)
  case node
  when Prism::ClassNode
    node.superclass ? node.superclass : node.constant_path
  when Prism::ModuleNode
    node.constant_path
  end
end

#insert_after(range, text) ⇒ Object



15
16
17
# File 'lib/rbs/inline/annotator/visitor.rb', line 15

def insert_after(range, text)
  @result.writer.insert_after(range:, text:)
end

#insert_before(range, text) ⇒ Object



11
12
13
# File 'lib/rbs/inline/annotator/visitor.rb', line 11

def insert_before(range, text)
  @result.writer.insert_before(range:, text:)
end

#module_class_entryObject



46
47
48
# File 'lib/rbs/inline/annotator/visitor.rb', line 46

def module_class_entry
  @env.module_class_entry(type_name)
end

#node_range(node) ⇒ Object



27
28
29
30
31
32
# File 'lib/rbs/inline/annotator/visitor.rb', line 27

def node_range(node)
  Range.new(
    node.location.start_character_offset,
    node.location.end_character_offset,
  )
end

#push_type_name(node) ⇒ Object



468
469
470
471
472
473
474
475
# File 'lib/rbs/inline/annotator/visitor.rb', line 468

def push_type_name(node)
  parts = node.constant_path.full_name_parts.dup
  @stack.push(parts)

  yield
ensure
  @stack.pop
end

#remove(range) ⇒ Object



23
24
25
# File 'lib/rbs/inline/annotator/visitor.rb', line 23

def remove(range)
  replace(range, "")
end

#replace(range, text) ⇒ Object



19
20
21
# File 'lib/rbs/inline/annotator/visitor.rb', line 19

def replace(range, text)
  @result.writer.replace(range:, text:)
end

#type_nameObject



477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
# File 'lib/rbs/inline/annotator/visitor.rb', line 477

def type_name
  if @stack.empty?
    nil
  else
    absolute = false
    names = []
    @stack.reverse_each do |parts|
      names.unshift(*parts)
      if names.first == :""
        absolute = true
        names.shift
        break
      end
    end
    *path, name = names.map(&:to_sym)
    RBS::TypeName.new(
      name: name,
      namespace: RBS::Namespace.new(path:, absolute:)
    ).absolute!
  end
end

#visit_call_node(node) ⇒ Object



290
291
292
293
294
295
296
297
298
299
# File 'lib/rbs/inline/annotator/visitor.rb', line 290

def visit_call_node(node)
  return if @stack.empty?

  case node.name
  when :attr_reader, :attr_writer, :attr_accessor
    when_attribute_node(node)
  when :include, :extend, :prepend
    when_mixin_node(node)
  end
end

#visit_class_node(node) ⇒ Object



50
51
52
53
54
55
56
57
58
59
# File 'lib/rbs/inline/annotator/visitor.rb', line 50

def visit_class_node(node)
  push_type_name(node) do
    with_generics(node)
    with_superclass(node.superclass)
    # TODO: Which file should we write to?
    # with_embedding_rbs(header_node(node), node)
    # with_variables(header_node(node), node)
    visit_child_nodes(node)
  end
end

#visit_constant_path_write_node(node) ⇒ Object



407
408
409
410
411
412
413
# File 'lib/rbs/inline/annotator/visitor.rb', line 407

def visit_constant_path_write_node(node)
  constant_type_name = RBS::TypeName.parse(node.target.full_name).then do |c|
    type_name ? type_name + c : c.absolute!
  end
  type = constant_type(node, constant_type_name) or return
  add_rbs_inline_annotation_for_trailing(node:, type:)
end

#visit_constant_write_node(node) ⇒ Object



415
416
417
418
419
420
421
422
423
# File 'lib/rbs/inline/annotator/visitor.rb', line 415

def visit_constant_write_node(node)
  namespace = type_name ? type_name.to_namespace : RBS::Namespace.root
  constant_type_name = RBS::TypeName.new(
    name: node.name,
    namespace:
  )
  type = constant_type(node, constant_type_name) or return
  add_rbs_inline_annotation_for_trailing(node:, type:)
end

#visit_def_node(node) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/rbs/inline/annotator/visitor.rb', line 163

def visit_def_node(node)
  module_class_entry&.each_decl do |decl|
    decl.members.each do |member|
      case member
      when RBS::AST::Members::MethodDefinition
        next unless node.name == member.name

        if node.receiver.nil? && @kind == :instance
          # def foo
          next unless member.instance?
        elsif node.receiver.is_a?(Prism::SelfNode) || @kind == :singleton
          # def self.foo
          next unless member.singleton?
        else
          next
        end

        add_rbs_inline_annotation_for_def_node(node:, method_definition: member)
      end
    end
  end
end

#visit_module_node(node) ⇒ Object



61
62
63
64
65
66
67
68
69
70
# File 'lib/rbs/inline/annotator/visitor.rb', line 61

def visit_module_node(node)
  push_type_name(node) do
    with_generics(node)
    with_module_self(node)
    # TODO: Which file should we write to?
    # with_embedding_rbs(header_node(node), node)
    # with_variables(header_node(node), node)
    visit_child_nodes(node)
  end
end

#visit_singleton_class_node(node) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
# File 'lib/rbs/inline/annotator/visitor.rb', line 34

def visit_singleton_class_node(node)
  if @kind == :singleton
    warn "nested singleton class detected"
    return
  end

  @kind = :singleton
  visit_child_nodes(node)
ensure
  @kind = :instance
end

#when_attribute_node(node) ⇒ Object



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/rbs/inline/annotator/visitor.rb', line 301

def when_attribute_node(node)
  case node.receiver
  when nil, Prism::SelfNode
    if node.arguments.arguments.length == 1
      first_arg_node = node.arguments.arguments.first
      return unless first_arg_node.is_a?(Prism::SymbolNode)

      value = first_arg_node.value
      module_class_entry&.each_decl do |decl|
        decl.members.each do |member|
          next unless (node.name == :attr_reader && member.is_a?(RBS::AST::Members::AttrReader)) ||
                      (node.name == :attr_writer && member.is_a?(RBS::AST::Members::AttrWriter)) ||
                      (node.name == :attr_accessor && member.is_a?(RBS::AST::Members::AttrAccessor))
          next unless member.name == value.to_sym
          next unless member.kind == @kind

          type = member.type.to_s
          next if type == "untyped"

          insert_after(node_range(node), " #: #{type}")
        end
      end
    else
      replaced_count = 0
      node.arguments.arguments.each do |arg|
        next unless arg.is_a?(Prism::SymbolNode)

        value = arg.value
        module_class_entry&.each_decl do |decl|
          decl.members.each do |member|
            next unless (node.name == :attr_reader && member.is_a?(RBS::AST::Members::AttrReader)) ||
                        (node.name == :attr_writer && member.is_a?(RBS::AST::Members::AttrWriter)) ||
                        (node.name == :attr_accessor && member.is_a?(RBS::AST::Members::AttrAccessor))
            next unless member.name == value.to_sym
            next unless member.kind == @kind

            indent = replaced_count == 0 ? "" : " " * node.location.start_column
            insert_before(node_range(node), "#{indent}#{node.name} :#{value} #: #{member.type}\n")
            replaced_count += 1
          end
        end
      end

      if replaced_count == node.arguments.arguments.length
        range = node_range(node)
        # Remove attr_*
        remove(range)
        # Remove the last newline
        remove(Range.new(range.end, range.end + 1))
      end
    end
  end
end

#when_mixin_node(node) ⇒ Object



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/rbs/inline/annotator/visitor.rb', line 355

def when_mixin_node(node)
  case node.receiver
  when nil, Prism::SelfNode
    if node.arguments.arguments.length == 1
      first_arg_node = node.arguments.arguments.first
      return unless first_arg_node.is_a?(Prism::ConstantReadNode)

      name = first_arg_node.name
      module_class_entry&.each_decl do |decl|
        decl.members.each do |member|
          next unless (node.name == :include && member.is_a?(RBS::AST::Members::Include)) ||
                      (node.name == :extend && member.is_a?(RBS::AST::Members::Extend)) ||
                      (node.name == :prepend && member.is_a?(RBS::AST::Members::Prepend))
          next unless member.name.to_s == name.to_s
          next unless member.args.any?

          type = member.args.join(", ")
          insert_after(node_range(node), " #[#{type}]")
        end
      end
    else
      replaced_count = 0
      node.arguments.arguments.each do |arg|
        next unless arg.is_a?(Prism::ConstantReadNode)

        name = arg.name
        module_class_entry&.each_decl do |decl|
          decl.members.each do |member|
            next unless (node.name == :include && member.is_a?(RBS::AST::Members::Include)) ||
                        (node.name == :extend && member.is_a?(RBS::AST::Members::Extend)) ||
                        (node.name == :prepend && member.is_a?(RBS::AST::Members::Prepend))
            next unless member.name.to_s == name.to_s
            next unless member.args.any?

            indent = replaced_count == 0 ? "" : " " * node.location.start_column
            insert_before(node_range(node), "#{indent}#{node.name} #{name} #[#{member.args.join(", ")}]\n")
            replaced_count += 1
          end
        end
      end

      if replaced_count > 0 && replaced_count == node.arguments.arguments.length
        range = node_range(node)
        # Remove include *
        remove(range)
        # Remove the last newline
        remove(Range.new(range.end, range.end + 1))
      end
    end
  end
end

#with_embedding_rbs(header_node, node) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/rbs/inline/annotator/visitor.rb', line 89

def with_embedding_rbs(header_node, node)
  entry = module_class_entry or return
  indent = " " * (node.body ? node.body.location.start_column : node.location.start_column + 2)
  embedding_rbs = []
  entry.each_decl do |decl|
    decl.members.each do |member|
      case member
      when RBS::AST::Declarations::Interface, RBS::AST::Declarations::TypeAlias
        embedding_rbs << member
      end
    end
  end

  if embedding_rbs.any?
    header_range = node_range(header_node)
    insert_after(header_range, "\n#{indent}# @rbs!\n")
    embedding_rbs.each do |member|
      case member
      when RBS::AST::Declarations::Interface
        insert_after(header_range, "#{indent}#   interface #{member.name}\n")
        member.members.each do |m|
          insert_after(header_range, "#{indent}#     #{m.location.source.strip}\n")
        end
        insert_after(header_range, "#{indent}#   end\n")
      when RBS::AST::Declarations::TypeAlias
        insert_after(header_range, "#{indent}#   type #{member.name} = #{member.type}\n")
      end
    end
  end
end

#with_generics(node) ⇒ Object



72
73
74
75
76
77
78
# File 'lib/rbs/inline/annotator/visitor.rb', line 72

def with_generics(node)
  entry = module_class_entry or return
  indent = " " * node.location.start_column
  entry.primary_decl.type_params.each do |type_param|
    insert_before(node_range(node), "# @rbs generic #{type_param}\n#{indent}")
  end
end

#with_module_self(node) ⇒ Object



154
155
156
157
158
159
160
161
# File 'lib/rbs/inline/annotator/visitor.rb', line 154

def with_module_self(node)
  entry = module_class_entry or return
  self_types = entry.primary_decl.self_types
  if self_types.any?
    indent = " " * node.location.start_column
    insert_before(node_range(node), "# @rbs module-self #{self_types.join(", ")}\n#{indent}")
  end
end

#with_superclass(node) ⇒ Object



120
121
122
123
124
125
126
127
128
# File 'lib/rbs/inline/annotator/visitor.rb', line 120

def with_superclass(node)
  node or return
  entry = module_class_entry or return
  super_class_decl = entry.primary_decl.super_class or return
  return unless super_class_decl.args.any?

  args = super_class_decl.args.join(", ")
  insert_after(node_range(node), " #[#{args}]")
end

#with_variables(header_node, node) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/rbs/inline/annotator/visitor.rb', line 130

def with_variables(header_node, node)
  indent = " " * (node.body ? node.body.location.start_column : node.location.start_column + 2)
  entry = module_class_entry or return
  added = false
  entry.each_decl do |decl|
    decl.members.each do |member|
      case member
      when RBS::AST::Members::InstanceVariable
        insert_after(node_range(header_node), "\n#{indent}# @rbs #{member.name}: #{member.type}")
        added = true
      when RBS::AST::Members::ClassVariable
        insert_after(node_range(header_node), "\n#{indent}# @rbs #{member.name}: #{member.type}")
        added = true
      when RBS::AST::Members::ClassInstanceVariable
        insert_after(node_range(header_node), "\n#{indent}# @rbs #{member.name}: #{member.type}")
        added = true
      end
    end
  end
  if added && node.body&.body&.first
    insert_before(node_range(node.body.body.first), "\n#{indent}")
  end
end