Class: RBS::InlineParser::Parser

Inherits:
Prism::Visitor
  • Object
show all
Includes:
AST::Ruby::Helpers::ConstantHelper, AST::Ruby::Helpers::LocationHelper
Defined in:
lib/rbs/inline_parser.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from AST::Ruby::Helpers::LocationHelper

#rbs_location

Methods included from AST::Ruby::Helpers::ConstantHelper

constant_as_type_name

Constructor Details

#initialize(result) ⇒ Parser

Returns a new instance of Parser.



63
64
65
66
67
# File 'lib/rbs/inline_parser.rb', line 63

def initialize(result)
  @result = result
  @module_nesting = []
  @comments = CommentAssociation.build(result.buffer, result.prism_result)
end

Instance Attribute Details

#commentsObject (readonly)

Returns the value of attribute comments.



58
59
60
# File 'lib/rbs/inline_parser.rb', line 58

def comments
  @comments
end

#module_nestingObject (readonly)

Returns the value of attribute module_nesting.



58
59
60
# File 'lib/rbs/inline_parser.rb', line 58

def module_nesting
  @module_nesting
end

#resultObject (readonly)

Returns the value of attribute result.



58
59
60
# File 'lib/rbs/inline_parser.rb', line 58

def result
  @result
end

Instance Method Details

#bufferObject



69
70
71
# File 'lib/rbs/inline_parser.rb', line 69

def buffer
  result.buffer
end

#current_moduleObject



73
74
75
# File 'lib/rbs/inline_parser.rb', line 73

def current_module
  module_nesting.last
end

#current_module!Object



77
78
79
# File 'lib/rbs/inline_parser.rb', line 77

def current_module!
  current_module || raise("#current_module is nil")
end

#diagnosticsObject



81
82
83
# File 'lib/rbs/inline_parser.rb', line 81

def diagnostics
  result.diagnostics
end

#insert_declaration(decl) ⇒ Object



472
473
474
475
476
477
478
# File 'lib/rbs/inline_parser.rb', line 472

def insert_declaration(decl)
  if current_module
    current_module.members << decl
  else
    result.declarations << decl
  end
end

#parse_attribute_call(node) ⇒ Object



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
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/rbs/inline_parser.rb', line 315

def parse_attribute_call(node)
  # Get the name nodes (arguments to attr_*)
  unless node.arguments && !node.arguments.arguments.empty?
    return # No arguments, nothing to do
  end

  name_nodes = [] #: Array[Prism::SymbolNode]
  node.arguments.arguments.each do |arg|
    case arg
    when Prism::SymbolNode
      name_nodes << arg
    else
      # Non-symbol argument, report error
      diagnostics << Diagnostic::AttributeNonSymbolName.new(
        rbs_location(arg.location),
        "Attribute name must be a symbol"
      )
    end
  end

  return if name_nodes.empty?

  # Look for leading comment block
  leading_block = comments.leading_block!(node)

  # Look for trailing type annotation (#: Type)
  trailing_block = comments.trailing_block!(node.location)
  type_annotation = nil

  if trailing_block
    case annotation = trailing_block.trailing_annotation([])
    when AST::Ruby::Annotations::NodeTypeAssertion
      type_annotation = annotation
    when AST::Ruby::CommentBlock::AnnotationSyntaxError
      diagnostics << Diagnostic::AnnotationSyntaxError.new(
        annotation.location, "Syntax error: " + annotation.error.error_message
      )
    end
  end

  # Report unused leading annotations since @rbs annotations are not used for attributes
  if leading_block
    report_unused_block(leading_block)
  end

  # Create the appropriate member type
  member = case node.name
  when :attr_reader
    AST::Ruby::Members::AttrReaderMember.new(buffer, node, name_nodes, leading_block, type_annotation)
  when :attr_writer
    AST::Ruby::Members::AttrWriterMember.new(buffer, node, name_nodes, leading_block, type_annotation)
  when :attr_accessor
    AST::Ruby::Members::AttrAccessorMember.new(buffer, node, name_nodes, leading_block, type_annotation)
  else
    raise "Unexpected attribute method: #{node.name}"
  end

  current_module!.members << member
end

#parse_constant_declaration(node) ⇒ Object



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
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
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
461
462
463
464
465
466
467
468
469
470
# File 'lib/rbs/inline_parser.rb', line 375

def parse_constant_declaration(node)
  # Create TypeName for the constant
  unless constant_name = constant_as_type_name(node)
    location =
      case node
      when Prism::ConstantWriteNode
        node.name_loc
      when Prism::ConstantPathWriteNode
        node.target.location
      end

    diagnostics << Diagnostic::NonConstantConstantDeclaration.new(
      rbs_location(location),
      "Constant name must be a constant"
    )
    return
  end

  # Look for leading comment block
  leading_block = comments.leading_block!(node)
  report_unused_block(leading_block) if leading_block

  # Look for trailing type annotation (#: Type)
  trailing_block = comments.trailing_block!(node.location)
  type_annotation = nil
  alias_annotation = nil

  if trailing_block
    case annotation = trailing_block.trailing_annotation([])
    when AST::Ruby::Annotations::NodeTypeAssertion
      type_annotation = annotation
    when AST::Ruby::Annotations::ClassAliasAnnotation, AST::Ruby::Annotations::ModuleAliasAnnotation
      alias_annotation = annotation
    when AST::Ruby::CommentBlock::AnnotationSyntaxError
      diagnostics << Diagnostic::AnnotationSyntaxError.new(
        annotation.location, "Syntax error: " + annotation.error.error_message
      )
    end
  end

  # Handle class/module alias declarations
  if alias_annotation
    # Try to infer the old name from the right-hand side
    infered_old_name = constant_as_type_name(node.value)

    # Check if we have either an explicit type name or can infer one
    if alias_annotation.type_name.nil? && infered_old_name.nil?
      message =
        if alias_annotation.is_a?(AST::Ruby::Annotations::ClassAliasAnnotation)
          "Class name is missing in class alias declaration"
        else
          "Module name is missing in module alias declaration"
        end

      diagnostics << Diagnostic::ClassModuleAliasDeclarationMissingTypeName.new(
        alias_annotation.location,
        message
      )
      return
    end

    # Create class/module alias declaration
    alias_decl = AST::Ruby::Declarations::ClassModuleAliasDecl.new(
      buffer,
      node,
      constant_name,
      infered_old_name,
      leading_block,
      alias_annotation
    )

    # Insert the alias declaration appropriately

    if current_module
      current_module.members << alias_decl
    else
      result.declarations << alias_decl
    end
  else
    # Create regular constant declaration
    constant_decl = AST::Ruby::Declarations::ConstantDecl.new(
      buffer,
      constant_name,
      node,
      leading_block,
      type_annotation
    )

    # Insert the constant declaration appropriately
    if current_module
      current_module.members << constant_decl
    else
      result.declarations << constant_decl
    end
  end
end

#parse_mixin_call(node) ⇒ Object



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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/rbs/inline_parser.rb', line 259

def parse_mixin_call(node)
  # Check for multiple arguments
  if node.arguments && node.arguments.arguments.length > 1
    diagnostics << Diagnostic::MixinMultipleArguments.new(
      rbs_location(node.location),
      "Mixing multiple modules with one call is not supported"
    )
    return
  end

  # Check for missing arguments
  unless node.arguments && node.arguments.arguments.length == 1
    # This shouldn't happen in valid Ruby code, but handle it gracefully
    return
  end

  first_arg = node.arguments.arguments.first

  # Check if the argument is a constant
  unless module_name = constant_as_type_name(first_arg)
    diagnostics << Diagnostic::MixinNonConstantModule.new(
      rbs_location(first_arg.location),
      "Module name must be a constant"
    )
    return
  end

  # Look for type application annotation in trailing comments
  # For single-line calls like "include Bar #[String]", the annotation is trailing
  trailing_block = comments.trailing_block!(node.location)
  annotation = nil

  if trailing_block
    case trailing_annotation = trailing_block.trailing_annotation([])
    when AST::Ruby::Annotations::TypeApplicationAnnotation
      annotation = trailing_annotation
    else
      report_unused_annotation(trailing_annotation)
    end
  end

  # Create the appropriate member based on the method name
  member = case node.name
  when :include
    AST::Ruby::Members::IncludeMember.new(buffer, node, module_name, annotation)
  when :extend
    AST::Ruby::Members::ExtendMember.new(buffer, node, module_name, annotation)
  when :prepend
    AST::Ruby::Members::PrependMember.new(buffer, node, module_name, annotation)
  else
    raise "Unexpected mixin method: #{node.name}"
  end

  current_module!.members << member
end

#parse_super_class(super_class_expr, inheritance_operator_loc) ⇒ Object



508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/rbs/inline_parser.rb', line 508

def parse_super_class(super_class_expr, inheritance_operator_loc)
  # Check if the superclass is a constant
  unless super_class_name = constant_as_type_name(super_class_expr)
    diagnostics << Diagnostic::NonConstantSuperClassName.new(
      rbs_location(super_class_expr.location),
      "Super class name must be a constant"
    )
    return nil
  end

  # Look for type application annotation in trailing comments
  # For example: class StringArray < Array #[String]
  trailing_block = comments.trailing_block!(super_class_expr.location)
  type_annotation = nil

  if trailing_block
    case annotation = trailing_block.trailing_annotation([])
    when AST::Ruby::Annotations::TypeApplicationAnnotation
      type_annotation = annotation
    else
      report_unused_annotation(annotation)
    end
  end

  # Create SuperClass object
  AST::Ruby::Declarations::ClassDecl::SuperClass.new(
    rbs_location(super_class_expr.location),
    rbs_location(inheritance_operator_loc),
    super_class_name,
    type_annotation
  )
end

#push_module_nesting(mod) ⇒ Object



85
86
87
88
89
90
# File 'lib/rbs/inline_parser.rb', line 85

def push_module_nesting(mod)
  module_nesting.push(mod)
  yield
ensure
  module_nesting.pop()
end

#report_unused_annotation(*annotations) ⇒ Object



480
481
482
483
484
485
486
487
488
489
490
491
492
493
# File 'lib/rbs/inline_parser.rb', line 480

def report_unused_annotation(*annotations)
  annotations.each do |annotation|
    case annotation
    when AST::Ruby::CommentBlock::AnnotationSyntaxError
      diagnostics << Diagnostic::AnnotationSyntaxError.new(
        annotation.location, "Syntax error: " + annotation.error.error_message
      )
    when AST::Ruby::Annotations::Base
      diagnostics << Diagnostic::UnusedInlineAnnotation.new(
        annotation.location, "Unused inline rbs annotation"
      )
    end
  end
end

#report_unused_block(block) ⇒ Object



495
496
497
498
499
500
501
502
503
504
505
506
# File 'lib/rbs/inline_parser.rb', line 495

def report_unused_block(block)
  return unless block.leading?

  block.each_paragraph([]) do |paragraph|
    case paragraph
    when Location
      # noop
    else
      report_unused_annotation(paragraph)
    end
  end
end

#skip_node?(node) ⇒ Boolean

Returns:

  • (Boolean)


92
93
94
95
96
97
98
99
100
101
# File 'lib/rbs/inline_parser.rb', line 92

def skip_node?(node)
  if ref = comments.leading_block(node)
    if ref.block.each_paragraph([]).any? { _1.is_a?(AST::Ruby::Annotations::SkipAnnotation) }
      ref.associate!
      return true
    end
  end

  false
end

#visit_call_node(node) ⇒ Object



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
# File 'lib/rbs/inline_parser.rb', line 217

def visit_call_node(node)
  return unless node.receiver.nil? # Only handle top-level calls like include, extend, prepend, attr_*

  case node.name
  when :include, :extend, :prepend
    return if skip_node?(node)

    case current = current_module
    when AST::Ruby::Declarations::ClassDecl, AST::Ruby::Declarations::ModuleDecl
      parse_mixin_call(node)
    end
  when :attr_reader, :attr_writer, :attr_accessor
    return if skip_node?(node)

    case current = current_module
    when AST::Ruby::Declarations::ClassDecl, AST::Ruby::Declarations::ModuleDecl
      parse_attribute_call(node)
    when nil
      # Top-level attribute definition
      diagnostics << Diagnostic::TopLevelAttributeDefinition.new(
        rbs_location(node.message_loc || node.location),
        "Top-level attribute definition is not supported"
      )
    end
  else
    visit_child_nodes(node)
  end
end

#visit_class_node(node) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/rbs/inline_parser.rb', line 103

def visit_class_node(node)
  return if skip_node?(node)

  unless class_name = constant_as_type_name(node.constant_path)
    diagnostics << Diagnostic::NonConstantClassName.new(
      rbs_location(node.constant_path.location),
      "Class name must be a constant"
    )
    return
  end

  # Parse super class if present
  super_class = if node.superclass
    node.inheritance_operator_loc or raise
    parse_super_class(node.superclass, node.inheritance_operator_loc)
  end

  class_decl = AST::Ruby::Declarations::ClassDecl.new(buffer, class_name, node, super_class)
  insert_declaration(class_decl)
  push_module_nesting(class_decl) do
    visit_child_nodes(node)

    node.child_nodes.each do |child_node|
      if child_node
        comments.each_enclosed_block(child_node) do |block|
          report_unused_block(block)
        end
      end
    end
  end

  comments.each_enclosed_block(node) do |block|
    unused_annotations = [] #: Array[AST::Ruby::CommentBlock::AnnotationSyntaxError | AST::Ruby::Annotations::leading_annotation]

    block.each_paragraph([]) do |paragraph|
      case paragraph
      when AST::Ruby::Annotations::InstanceVariableAnnotation
        class_decl.members << AST::Ruby::Members::InstanceVariableMember.new(buffer, paragraph)
      when Location
        # Skip
      when AST::Ruby::CommentBlock::AnnotationSyntaxError
        unused_annotations << paragraph
      else
        unused_annotations << paragraph
      end
    end

    report_unused_annotation(*unused_annotations)
  end

  class_decl.members.sort_by! { _1.location.start_line }
end

#visit_constant_path_write_node(node) ⇒ Object



253
254
255
256
257
# File 'lib/rbs/inline_parser.rb', line 253

def visit_constant_path_write_node(node)
  return if skip_node?(node)

  parse_constant_declaration(node)
end

#visit_constant_write_node(node) ⇒ Object



246
247
248
249
250
251
# File 'lib/rbs/inline_parser.rb', line 246

def visit_constant_write_node(node)
  return if skip_node?(node)

  # Parse constant declaration (both top-level and in classes/modules)
  parse_constant_declaration(node)
end

#visit_def_node(node) ⇒ Object



178
179
180
181
182
183
184
185
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
# File 'lib/rbs/inline_parser.rb', line 178

def visit_def_node(node)
  return if skip_node?(node)

  if node.receiver
    diagnostics << Diagnostic::NotImplementedYet.new(
      rbs_location(node.receiver.location),
      "Singleton method definition is not supported yet"
    )
    return
  end

  case current = current_module
  when AST::Ruby::Declarations::ClassDecl, AST::Ruby::Declarations::ModuleDecl
    leading_block = comments.leading_block!(node)

    if node.end_keyword_loc
      # Not an end-less def
      end_loc = node.rparen_loc || node.parameters&.location || node.name_loc
      trailing_block = comments.trailing_block!(end_loc)
    end

    method_type, leading_unuseds, trailing_unused = AST::Ruby::Members::MethodTypeAnnotation.build(leading_block, trailing_block, [], node)
    report_unused_annotation(trailing_unused, *leading_unuseds)

    defn = AST::Ruby::Members::DefMember.new(buffer, node.name, node, method_type, leading_block)
    current.members << defn

    # Skip other comments in `def` node
    comments.each_enclosed_block(node) do |block|
      report_unused_block(block)
    end
  else
    diagnostics << Diagnostic::TopLevelMethodDefinition.new(
      rbs_location(node.name_loc),
      "Top-level method definition is not supported"
    )
  end
end

#visit_module_node(node) ⇒ Object



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/rbs/inline_parser.rb', line 156

def visit_module_node(node)
  return if skip_node?(node)

  unless module_name = constant_as_type_name(node.constant_path)
    diagnostics << Diagnostic::NonConstantModuleName.new(
      rbs_location(node.constant_path.location),
      "Module name must be a constant"
    )
    return
  end

  module_decl = AST::Ruby::Declarations::ModuleDecl.new(buffer, module_name, node)
  insert_declaration(module_decl)
  push_module_nesting(module_decl) do
    visit_child_nodes(node)
  end

  comments.each_enclosed_block(node) do |block|
    report_unused_block(block)
  end
end