Class: Rfmt::PrismBridge

Inherits:
Object
  • Object
show all
Extended by:
PrismNodeExtractor
Defined in:
lib/rfmt/prism_bridge.rb

Overview

PrismBridge provides the Ruby-side integration with the Prism parser It parses Ruby source code and converts the AST to a JSON format that can be consumed by the Rust formatter

Defined Under Namespace

Classes: ParseError

Class Method Summary collapse

Methods included from PrismNodeExtractor

extract_class_or_module_name, extract_literal_value, extract_message_name, extract_node_name, extract_parameter_count, extract_string_content, extract_superclass_name

Class Method Details

.convert_node(node) ⇒ Object

Convert a Prism node to our internal representation



87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/rfmt/prism_bridge.rb', line 87

def self.convert_node(node)
  return nil if node.nil?

  {
    node_type: node_type_name(node),
    location: extract_location(node),
    children: extract_children(node),
    metadata: (node),
    comments: extract_comments(node),
    formatting: extract_formatting(node)
  }
end

.extract_children(node) ⇒ Object

Extract child nodes



175
176
177
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
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
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
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
354
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
406
407
408
# File 'lib/rfmt/prism_bridge.rb', line 175

def self.extract_children(node)
  children = []

  begin
    # Different node types have different child accessors
    children = case node
               when Prism::ProgramNode
                 node.statements ? node.statements.body : []
               when Prism::StatementsNode
                 node.body || []
               when Prism::ClassNode
                 [
                   node.constant_path,
                   node.superclass,
                   node.body
                 ].compact
               when Prism::ModuleNode
                 [
                   node.constant_path,
                   node.body
                 ].compact
               when Prism::DefNode
                 params = if node.parameters
                            node.parameters.child_nodes.compact
                          else
                            []
                          end
                 params + [node.body].compact
               when Prism::CallNode
                 result = []
                 result << node.receiver if node.receiver
                 result.concat(node.arguments.child_nodes.compact) if node.arguments
                 result << node.block if node.block
                 result
               when Prism::IfNode, Prism::UnlessNode
                 [
                   node.predicate,
                   node.statements,
                   node.consequent
                 ].compact
               when Prism::ElseNode
                 [node.statements].compact
               when Prism::ArrayNode
                 node.elements || []
               when Prism::HashNode
                 node.elements || []
               when Prism::BlockNode
                 params = if node.parameters
                            node.parameters.child_nodes.compact
                          else
                            []
                          end
                 params + [node.body].compact
               when Prism::BeginNode
                 [
                   node.statements,
                   node.rescue_clause,
                   node.else_clause,
                   node.ensure_clause
                 ].compact
               when Prism::EnsureNode
                 [node.statements].compact
               when Prism::LambdaNode
                 params = if node.parameters
                            node.parameters.child_nodes.compact
                          else
                            []
                          end
                 params + [node.body].compact
               when Prism::RescueNode
                 result = []
                 result.concat(node.exceptions) if node.exceptions
                 result << node.reference if node.reference
                 result << node.statements if node.statements
                 result << node.subsequent if node.subsequent
                 result
               when Prism::SymbolNode, Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode
                 []
               when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode
                 [node.value].compact
               when Prism::ReturnNode
                 node.arguments ? node.arguments.child_nodes.compact : []
               when Prism::OrNode
                 [node.left, node.right].compact
               when Prism::AssocNode
                 [node.key, node.value].compact
               when Prism::KeywordHashNode
                 node.elements || []
               when Prism::InterpolatedStringNode
                 node.parts || []
               when Prism::EmbeddedStatementsNode
                 [node.statements].compact
               when Prism::CaseNode
                 [node.predicate, *node.conditions, node.else_clause].compact
               when Prism::WhenNode
                 [*node.conditions, node.statements].compact
               when Prism::WhileNode, Prism::UntilNode
                 [node.predicate, node.statements].compact
               when Prism::ForNode
                 [node.index, node.collection, node.statements].compact
               when Prism::BreakNode, Prism::NextNode
                 node.arguments ? node.arguments.child_nodes.compact : []
               when Prism::RedoNode, Prism::RetryNode
                 []
               when Prism::YieldNode
                 node.arguments ? node.arguments.child_nodes.compact : []
               when Prism::SuperNode
                 result = []
                 result.concat(node.arguments.child_nodes.compact) if node.arguments
                 result << node.block if node.block
                 result
               when Prism::ForwardingSuperNode
                 node.block ? [node.block] : []
               when Prism::RescueModifierNode
                 [node.expression, node.rescue_expression].compact
               when Prism::RangeNode
                 [node.left, node.right].compact
               when Prism::RegularExpressionNode
                 []
               when Prism::SplatNode
                 [node.expression].compact
               when Prism::AndNode
                 [node.left, node.right].compact
               when Prism::InterpolatedRegularExpressionNode, Prism::InterpolatedSymbolNode,
                    Prism::InterpolatedXStringNode
                 node.parts || []
               when Prism::XStringNode
                 []
               when Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode, Prism::SelfNode
                 []
               when Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode
                 [node.value].compact
               when Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
                    Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
                    Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
                    Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
                    Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode
                 [node.value].compact
               when Prism::ClassVariableOperatorWriteNode, Prism::GlobalVariableOperatorWriteNode,
                    Prism::LocalVariableOperatorWriteNode, Prism::InstanceVariableOperatorWriteNode,
                    Prism::ConstantOperatorWriteNode
                 [node.value].compact
               when Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode,
                    Prism::ConstantPathOperatorWriteNode
                 [node.target, node.value].compact
               when Prism::ConstantPathWriteNode
                 [node.target, node.value].compact
               when Prism::CaseMatchNode
                 [node.predicate, *node.conditions, node.else_clause].compact
               when Prism::InNode
                 [node.pattern, node.statements].compact
               when Prism::MatchPredicateNode, Prism::MatchRequiredNode
                 [node.value, node.pattern].compact
               when Prism::ParenthesesNode
                 [node.body].compact
               when Prism::DefinedNode
                 [node.value].compact
               when Prism::SingletonClassNode
                 [node.expression, node.body].compact
               when Prism::AliasMethodNode
                 [node.new_name, node.old_name].compact
               when Prism::AliasGlobalVariableNode
                 [node.new_name, node.old_name].compact
               when Prism::UndefNode
                 node.names || []
               when Prism::AssocSplatNode
                 [node.value].compact
               when Prism::BlockArgumentNode
                 [node.expression].compact
               when Prism::MultiWriteNode
                 [*node.lefts, node.rest, *node.rights, node.value].compact
               when Prism::MultiTargetNode
                 [*node.lefts, node.rest, *node.rights].compact
               when Prism::SourceFileNode, Prism::SourceLineNode, Prism::SourceEncodingNode
                 []
               when Prism::PreExecutionNode, Prism::PostExecutionNode
                 [node.statements].compact
               # Numeric literals
               when Prism::RationalNode, Prism::ImaginaryNode
                 [node.numeric].compact
               # String interpolation
               when Prism::EmbeddedVariableNode
                 [node.variable].compact
               # Pattern matching patterns
               when Prism::ArrayPatternNode
                 [*node.requireds, node.rest, *node.posts].compact
               when Prism::HashPatternNode
                 [*node.elements, node.rest].compact
               when Prism::FindPatternNode
                 [node.left, *node.requireds, node.right].compact
               when Prism::CapturePatternNode
                 [node.value, node.target].compact
               when Prism::AlternationPatternNode
                 [node.left, node.right].compact
               when Prism::PinnedExpressionNode
                 [node.expression].compact
               when Prism::PinnedVariableNode
                 [node.variable].compact
               # Forwarding and special parameters
               when Prism::ForwardingArgumentsNode, Prism::ForwardingParameterNode,
                    Prism::NoKeywordsParameterNode
                 []
               # References
               when Prism::BackReferenceReadNode, Prism::NumberedReferenceReadNode
                 []
               # Call/Index compound assignment
               when Prism::CallAndWriteNode, Prism::CallOrWriteNode, Prism::CallOperatorWriteNode
                 [node.receiver, node.value].compact
               when Prism::IndexAndWriteNode, Prism::IndexOrWriteNode, Prism::IndexOperatorWriteNode
                 [node.receiver, node.arguments, node.value].compact
               # Match
               when Prism::MatchWriteNode
                 [node.call, *node.targets].compact
               when Prism::MatchLastLineNode, Prism::InterpolatedMatchLastLineNode
                 []
               # Other
               when Prism::FlipFlopNode
                 [node.left, node.right].compact
               when Prism::ImplicitNode
                 [node.value].compact
               when Prism::ImplicitRestNode
                 []
               else
                 # For unknown types, try to get child nodes if they exist
                 []
               end
  rescue StandardError => e
    # Log warning in debug mode but continue processing
    warn "Warning: Failed to extract children from #{node.class}: #{e.message}" if $DEBUG
    children = []
  end

  children.compact.map { |child| convert_node(child) }
end

.extract_comments(_node) ⇒ Object

Extract comments associated with the node



479
480
481
482
483
# File 'lib/rfmt/prism_bridge.rb', line 479

def self.extract_comments(_node)
  # Prism attaches comments to the parse result, not individual nodes
  # For Phase 1, we'll return empty array and implement in Phase 2
  []
end

.extract_formatting(node) ⇒ Object

Extract formatting information



486
487
488
489
490
491
492
493
494
495
496
# File 'lib/rfmt/prism_bridge.rb', line 486

def self.extract_formatting(node)
  loc = node.location
  {
    indent_level: 0, # Will be calculated during formatting
    needs_blank_line_before: false,
    needs_blank_line_after: false,
    preserve_newlines: false,
    multiline: loc.start_line != loc.end_line,
    original_formatting: nil # Can store original text if needed
  }
end

.extract_location(node) ⇒ Object

Extract location information from node



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
# File 'lib/rfmt/prism_bridge.rb', line 109

def self.extract_location(node)
  loc = node.location

  # For heredoc nodes, the location only covers the opening tag (<<~CSV)
  # We need to find the maximum end_offset including closing_loc
  end_offset = loc.end_offset
  end_line = loc.end_line
  end_column = loc.end_column

  # Check this node's closing_loc
  if node.respond_to?(:closing_loc) && node.closing_loc
    closing = node.closing_loc
    if closing.end_offset > end_offset
      end_offset = closing.end_offset
      end_line = closing.end_line
      end_column = closing.end_column
    end
  end

  # Recursively check all descendant nodes for heredoc closing_loc
  # Issue #74: handled direct children (e.g., LocalVariableWriteNode -> StringNode)
  # Issue #86: handles deeper nesting (e.g., CallNode -> ArgumentsNode -> StringNode)
  max_closing = find_max_closing_loc_recursive(node)
  if max_closing && max_closing[:end_offset] > end_offset
    end_offset = max_closing[:end_offset]
    end_line = max_closing[:end_line]
    end_column = max_closing[:end_column]
  end

  {
    start_line: loc.start_line,
    start_column: loc.start_column,
    end_line: end_line,
    end_column: end_column,
    start_offset: loc.start_offset,
    end_offset: end_offset
  }
end

.extract_metadata(node) ⇒ Object

Extract metadata specific to node type



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
471
472
473
474
475
476
# File 'lib/rfmt/prism_bridge.rb', line 411

def self.(node)
   = {}

  case node
  when Prism::ClassNode
    if (name = extract_class_or_module_name(node))
      ['name'] = name
    end
    if (superclass = extract_superclass_name(node))
      ['superclass'] = superclass
    end
  when Prism::ModuleNode
    if (name = extract_class_or_module_name(node))
      ['name'] = name
    end
  when Prism::DefNode
    if (name = extract_node_name(node))
      ['name'] = name
    end
    ['parameters_count'] = extract_parameter_count(node).to_s
    # Extract parameters text directly from source
    if node.parameters
      ['parameters_text'] = node.parameters.location.slice
      ['has_parens'] = (!node.lparen_loc.nil?).to_s
    end
    # Check if this is a class method (def self.method_name)
    if node.respond_to?(:receiver) && node.receiver
      receiver = node.receiver
      if receiver.is_a?(Prism::SelfNode)
        ['receiver'] = 'self'
      elsif receiver.respond_to?(:slice)
        ['receiver'] = receiver.slice
      end
    end
  when Prism::CallNode
    if (name = extract_node_name(node))
      ['name'] = name
    end
    if (message = extract_message_name(node))
      ['message'] = message
    end
  when Prism::StringNode
    if (content = extract_string_content(node))
      ['content'] = content
    end
  when Prism::IntegerNode
    if (value = extract_literal_value(node))
      ['value'] = value
    end
  when Prism::FloatNode
    if (value = extract_literal_value(node))
      ['value'] = value
    end
  when Prism::SymbolNode
    if (value = extract_literal_value(node))
      ['value'] = value
    end
  when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode
    ['name'] = node.name.to_s
  when Prism::IfNode, Prism::UnlessNode
    # Detect ternary operator: if_keyword_loc is nil for ternary
    ['is_ternary'] = node.if_keyword_loc.nil?.to_s if node.respond_to?(:if_keyword_loc)
  end

  
end

.find_max_closing_loc_recursive(node, depth: 0) ⇒ Object

Recursively find the maximum closing_loc among all descendant nodes Returns nil if no closing_loc found, otherwise { end_offset:, end_line:, end_column: }



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/rfmt/prism_bridge.rb', line 150

def self.find_max_closing_loc_recursive(node, depth: 0)
  return nil if depth > 10

  max_closing = nil

  node.child_nodes.compact.each do |child|
    if child.respond_to?(:closing_loc) && child.closing_loc
      closing = child.closing_loc
      if max_closing.nil? || closing.end_offset > max_closing[:end_offset]
        max_closing = {
          end_offset: closing.end_offset,
          end_line: closing.end_line,
          end_column: closing.end_column
        }
      end
    end

    child_max = find_max_closing_loc_recursive(child, depth: depth + 1)
    max_closing = child_max if child_max && (max_closing.nil? || child_max[:end_offset] > max_closing[:end_offset])
  end

  max_closing
end

.handle_parse_errors(result) ⇒ Object

Handle parsing errors from Prism

Raises:



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/rfmt/prism_bridge.rb', line 41

def self.handle_parse_errors(result)
  errors = result.errors.map do |error|
    {
      line: error.location.start_line,
      column: error.location.start_column,
      message: error.message
    }
  end

  error_messages = errors.map do |err|
    "#{err[:line]}:#{err[:column]}: #{err[:message]}"
  end.join("\n")

  raise ParseError, "Parse errors:\n#{error_messages}"
end

.node_type_name(node) ⇒ Object

Get the node type name from Prism node



101
102
103
104
105
106
# File 'lib/rfmt/prism_bridge.rb', line 101

def self.node_type_name(node)
  # Prism node class names are like "Prism::ProgramNode"
  # We want just "program_node" in snake_case
  node.class.name.split('::').last.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
      .gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
end

.parse(source) ⇒ String

Parse Ruby source code and return serialized AST

Parameters:

  • source (String)

    Ruby source code to parse

Returns:

  • (String)

    JSON-serialized AST with comments

Raises:



20
21
22
23
24
25
26
# File 'lib/rfmt/prism_bridge.rb', line 20

def self.parse(source)
  result = Prism.parse(source)

  handle_parse_errors(result) if result.failure?

  serialize_ast_with_comments(result)
end

.parse_file(file_path) ⇒ String

Parse Ruby source code from a file

Parameters:

  • file_path (String)

    Path to Ruby file

Returns:

  • (String)

    JSON-serialized AST

Raises:

  • (ParseError)

    if parsing fails

  • (Errno::ENOENT)

    if file doesn’t exist



33
34
35
36
37
38
# File 'lib/rfmt/prism_bridge.rb', line 33

def self.parse_file(file_path)
  source = File.read(file_path)
  parse(source)
rescue Errno::ENOENT
  raise ParseError, "File not found: #{file_path}"
end

.serialize_ast(node) ⇒ Object

Serialize the Prism AST to JSON



58
59
60
# File 'lib/rfmt/prism_bridge.rb', line 58

def self.serialize_ast(node)
  JSON.generate(convert_node(node), max_nesting: false)
end

.serialize_ast_with_comments(result) ⇒ Object

Serialize the Prism AST with comments to JSON



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/rfmt/prism_bridge.rb', line 63

def self.serialize_ast_with_comments(result)
  comments = result.comments.map do |comment|
    {
      comment_type: comment.class.name.split('::').last.downcase.gsub('comment', ''),
      location: {
        start_line: comment.location.start_line,
        start_column: comment.location.start_column,
        end_line: comment.location.end_line,
        end_column: comment.location.end_column,
        start_offset: comment.location.start_offset,
        end_offset: comment.location.end_offset
      },
      text: comment.location.slice,
      position: 'leading' # Default position, will be refined by Rust
    }
  end

  JSON.generate({
                  ast: convert_node(result.value),
                  comments: comments
                }, max_nesting: false)
end