rfmt 1.7.0 → 2.0.0.beta1

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/Cargo.lock +90 -1225
  4. data/Cargo.toml +6 -0
  5. data/README.md +32 -28
  6. data/ext/rfmt/Cargo.toml +9 -28
  7. data/ext/rfmt/src/ast/mod.rs +2 -0
  8. data/ext/rfmt/src/config/mod.rs +267 -30
  9. data/ext/rfmt/src/error/mod.rs +14 -3
  10. data/ext/rfmt/src/format/formatter.rs +22 -11
  11. data/ext/rfmt/src/format/registry.rs +8 -0
  12. data/ext/rfmt/src/format/rule.rs +208 -136
  13. data/ext/rfmt/src/format/rules/call.rs +14 -22
  14. data/ext/rfmt/src/format/rules/fallback.rs +4 -11
  15. data/ext/rfmt/src/format/rules/if_unless.rs +25 -3
  16. data/ext/rfmt/src/format/rules/loops.rs +3 -1
  17. data/ext/rfmt/src/format/rules/variable_write.rs +16 -9
  18. data/ext/rfmt/src/lib.rs +45 -12
  19. data/ext/rfmt/src/parser/mod.rs +2 -0
  20. data/ext/rfmt/src/parser/native_adapter.rs +2735 -0
  21. data/ext/rfmt/src/parser/prism_adapter.rs +5 -0
  22. data/ext/rfmt/src/validation.rs +69 -0
  23. data/ext/rfmt/tests/fixtures/parity/comments_mixed.rb +9 -0
  24. data/ext/rfmt/tests/fixtures/parity/constructs.rb +50 -0
  25. data/ext/rfmt/tests/fixtures/parity/embdoc.rb +12 -0
  26. data/ext/rfmt/tests/fixtures/parity/heredoc_assign.rb +15 -0
  27. data/ext/rfmt/tests/fixtures/parity/heredoc_call_args.rb +17 -0
  28. data/ext/rfmt/tests/fixtures/parity/metadata_classes.rb +30 -0
  29. data/ext/rfmt/tests/fixtures/parity/metadata_conditionals.rb +14 -0
  30. data/ext/rfmt/tests/fixtures/parity/metadata_defs.rb +33 -0
  31. data/ext/rfmt/tests/fixtures/parity/multibyte.rb +14 -0
  32. data/ext/rfmt/tests/fixtures/parity/numeric.rb +6 -0
  33. data/ext/rfmt/tests/fixtures/parity/plain.rb +31 -0
  34. data/ext/rfmt/tests/native_parity.rs +245 -0
  35. data/ext/rfmt/tests/ruby_prism_smoke.rs +66 -0
  36. data/lib/rfmt/cli.rb +37 -7
  37. data/lib/rfmt/configuration.rb +2 -20
  38. data/lib/rfmt/version.rb +1 -1
  39. data/lib/rfmt.rb +47 -19
  40. metadata +17 -18
  41. data/lib/rfmt/prism_bridge.rb +0 -529
  42. data/lib/rfmt/prism_node_extractor.rb +0 -115
@@ -1,529 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'prism'
4
- require 'json'
5
- require_relative 'prism_node_extractor'
6
-
7
- module Rfmt
8
- # PrismBridge provides the Ruby-side integration with the Prism parser
9
- # It parses Ruby source code and converts the AST to a JSON format
10
- # that can be consumed by the Rust formatter
11
- class PrismBridge
12
- extend PrismNodeExtractor
13
-
14
- class ParseError < StandardError; end
15
-
16
- # Parse Ruby source code and return serialized AST
17
- # @param source [String] Ruby source code to parse
18
- # @return [String] JSON-serialized AST with comments
19
- # @raise [ParseError] if parsing fails
20
- def self.parse(source)
21
- result = Prism.parse(source)
22
-
23
- handle_parse_errors(result) if result.failure?
24
-
25
- serialize_ast_with_comments(result)
26
- end
27
-
28
- # Parse Ruby source code from a file
29
- # @param file_path [String] Path to Ruby file
30
- # @return [String] JSON-serialized AST
31
- # @raise [ParseError] if parsing fails
32
- # @raise [Errno::ENOENT] if file doesn't exist
33
- def self.parse_file(file_path)
34
- source = File.read(file_path)
35
- parse(source)
36
- rescue Errno::ENOENT
37
- raise ParseError, "File not found: #{file_path}"
38
- end
39
-
40
- # Handle parsing errors from Prism
41
- def self.handle_parse_errors(result)
42
- errors = result.errors.map do |error|
43
- {
44
- line: error.location.start_line,
45
- column: error.location.start_column,
46
- message: error.message
47
- }
48
- end
49
-
50
- error_messages = errors.map do |err|
51
- "#{err[:line]}:#{err[:column]}: #{err[:message]}"
52
- end.join("\n")
53
-
54
- raise ParseError, "Parse errors:\n#{error_messages}"
55
- end
56
-
57
- # Serialize the Prism AST to JSON
58
- def self.serialize_ast(node)
59
- JSON.generate(convert_node(node), max_nesting: false)
60
- end
61
-
62
- # Serialize the Prism AST with comments to JSON
63
- def self.serialize_ast_with_comments(result)
64
- comments = result.comments.map do |comment|
65
- loc = comment.location
66
- end_line = loc.end_line
67
- # Prism's `=begin ... =end` (EmbDocComment) location ends at
68
- # `<=end_line>:0`, i.e. the column-0 start of the line AFTER the
69
- # `=end` terminator. Reporting that larger end_line makes the
70
- # comment appear to overlap with the next statement, so the
71
- # comment gets misattributed to a deeper node (e.g. the first
72
- # expression inside the next method body) and ends up emitted in
73
- # the wrong place. Snap it back to the terminator line.
74
- end_line = loc.end_line - 1 if loc.end_column.zero? && loc.end_line > loc.start_line
75
-
76
- {
77
- comment_type: comment.class.name.split('::').last.downcase.gsub('comment', ''),
78
- location: {
79
- start_line: loc.start_line,
80
- start_column: loc.start_column,
81
- end_line: end_line,
82
- end_column: loc.end_column,
83
- start_offset: loc.start_offset,
84
- end_offset: loc.end_offset
85
- },
86
- text: loc.slice,
87
- position: 'leading' # Default position, will be refined by Rust
88
- }
89
- end
90
-
91
- JSON.generate({
92
- ast: convert_node(result.value),
93
- comments: comments
94
- }, max_nesting: false)
95
- end
96
-
97
- # Convert a Prism node to our internal representation
98
- def self.convert_node(node)
99
- return nil if node.nil?
100
-
101
- {
102
- node_type: node_type_name(node),
103
- location: extract_location(node),
104
- children: extract_children(node),
105
- metadata: extract_metadata(node),
106
- comments: extract_comments(node),
107
- formatting: extract_formatting(node)
108
- }
109
- end
110
-
111
- # Get the node type name from Prism node
112
- def self.node_type_name(node)
113
- # Prism node class names are like "Prism::ProgramNode"
114
- # We want just "program_node" in snake_case
115
- node.class.name.split('::').last.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
116
- .gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
117
- end
118
-
119
- # Extract location information from node
120
- def self.extract_location(node)
121
- loc = node.location
122
-
123
- # For heredoc nodes, the location only covers the opening tag (<<~CSV)
124
- # We need to find the maximum end_offset including closing_loc
125
- end_offset = loc.end_offset
126
- end_line = loc.end_line
127
- end_column = loc.end_column
128
-
129
- # Check this node's closing_loc
130
- if node.respond_to?(:closing_loc) && node.closing_loc
131
- closing = node.closing_loc
132
- if closing.end_offset > end_offset
133
- end_offset = closing.end_offset
134
- end_line = heredoc_terminator_line(closing)
135
- end_column = closing.end_column
136
- end
137
- end
138
-
139
- # Recursively check all descendant nodes for heredoc closing_loc
140
- # Issue #74: handled direct children (e.g., LocalVariableWriteNode -> StringNode)
141
- # Issue #86: handles deeper nesting (e.g., CallNode -> ArgumentsNode -> StringNode)
142
- max_closing = find_max_closing_loc_recursive(node)
143
- if max_closing && max_closing[:end_offset] > end_offset
144
- end_offset = max_closing[:end_offset]
145
- end_line = max_closing[:end_line]
146
- end_column = max_closing[:end_column]
147
- end
148
-
149
- {
150
- start_line: loc.start_line,
151
- start_column: loc.start_column,
152
- end_line: end_line,
153
- end_column: end_column,
154
- start_offset: loc.start_offset,
155
- end_offset: end_offset
156
- }
157
- end
158
-
159
- # Prism reports a heredoc's `closing_loc` as `<terminator_line>:0..(terminator_line+1):0`,
160
- # i.e. its `end_line` is the LINE AFTER the terminator. Using that as the
161
- # node's `end_line` makes blank-line preservation fail: a real blank line
162
- # right after the terminator looks identical (diff == 1) to two adjacent
163
- # statements with no separator. Use the terminator's own line instead.
164
- def self.heredoc_terminator_line(closing_loc)
165
- if closing_loc.end_column.zero? && closing_loc.end_line > closing_loc.start_line
166
- closing_loc.start_line
167
- else
168
- closing_loc.end_line
169
- end
170
- end
171
-
172
- # Recursively find the maximum closing_loc among all descendant nodes
173
- # Returns nil if no closing_loc found, otherwise { end_offset:, end_line:, end_column: }
174
- def self.find_max_closing_loc_recursive(node, depth: 0)
175
- return nil if depth > 10
176
-
177
- max_closing = nil
178
-
179
- node.child_nodes.compact.each do |child|
180
- if child.respond_to?(:closing_loc) && child.closing_loc
181
- closing = child.closing_loc
182
- if max_closing.nil? || closing.end_offset > max_closing[:end_offset]
183
- max_closing = {
184
- end_offset: closing.end_offset,
185
- end_line: heredoc_terminator_line(closing),
186
- end_column: closing.end_column
187
- }
188
- end
189
- end
190
-
191
- child_max = find_max_closing_loc_recursive(child, depth: depth + 1)
192
- max_closing = child_max if child_max && (max_closing.nil? || child_max[:end_offset] > max_closing[:end_offset])
193
- end
194
-
195
- max_closing
196
- end
197
-
198
- # Extract child nodes
199
- def self.extract_children(node)
200
- children = []
201
-
202
- begin
203
- # Different node types have different child accessors
204
- children = case node
205
- when Prism::ProgramNode
206
- node.statements ? node.statements.body : []
207
- when Prism::StatementsNode
208
- node.body || []
209
- when Prism::ClassNode
210
- [
211
- node.constant_path,
212
- node.superclass,
213
- node.body
214
- ].compact
215
- when Prism::ModuleNode
216
- [
217
- node.constant_path,
218
- node.body
219
- ].compact
220
- when Prism::DefNode
221
- params = if node.parameters
222
- node.parameters.child_nodes.compact
223
- else
224
- []
225
- end
226
- params + [node.body].compact
227
- when Prism::CallNode
228
- result = []
229
- result << node.receiver if node.receiver
230
- result.concat(node.arguments.child_nodes.compact) if node.arguments
231
- result << node.block if node.block
232
- result
233
- when Prism::IfNode, Prism::UnlessNode
234
- [
235
- node.predicate,
236
- node.statements,
237
- node.consequent
238
- ].compact
239
- when Prism::ElseNode
240
- [node.statements].compact
241
- when Prism::ArrayNode
242
- node.elements || []
243
- when Prism::HashNode
244
- node.elements || []
245
- when Prism::BlockNode
246
- params = if node.parameters
247
- node.parameters.child_nodes.compact
248
- else
249
- []
250
- end
251
- params + [node.body].compact
252
- when Prism::BeginNode
253
- [
254
- node.statements,
255
- node.rescue_clause,
256
- node.else_clause,
257
- node.ensure_clause
258
- ].compact
259
- when Prism::EnsureNode
260
- [node.statements].compact
261
- when Prism::LambdaNode
262
- params = if node.parameters
263
- node.parameters.child_nodes.compact
264
- else
265
- []
266
- end
267
- params + [node.body].compact
268
- when Prism::RescueNode
269
- result = []
270
- result.concat(node.exceptions) if node.exceptions
271
- result << node.reference if node.reference
272
- result << node.statements if node.statements
273
- result << node.subsequent if node.subsequent
274
- result
275
- when Prism::SymbolNode, Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode
276
- []
277
- when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode
278
- [node.value].compact
279
- when Prism::ReturnNode
280
- node.arguments ? node.arguments.child_nodes.compact : []
281
- when Prism::OrNode
282
- [node.left, node.right].compact
283
- when Prism::AssocNode
284
- [node.key, node.value].compact
285
- when Prism::KeywordHashNode
286
- node.elements || []
287
- when Prism::InterpolatedStringNode
288
- node.parts || []
289
- when Prism::EmbeddedStatementsNode
290
- [node.statements].compact
291
- when Prism::CaseNode
292
- [node.predicate, *node.conditions, node.else_clause].compact
293
- when Prism::WhenNode
294
- [*node.conditions, node.statements].compact
295
- when Prism::WhileNode, Prism::UntilNode
296
- [node.predicate, node.statements].compact
297
- when Prism::ForNode
298
- [node.index, node.collection, node.statements].compact
299
- when Prism::BreakNode, Prism::NextNode
300
- node.arguments ? node.arguments.child_nodes.compact : []
301
- when Prism::RedoNode, Prism::RetryNode
302
- []
303
- when Prism::YieldNode
304
- node.arguments ? node.arguments.child_nodes.compact : []
305
- when Prism::SuperNode
306
- result = []
307
- result.concat(node.arguments.child_nodes.compact) if node.arguments
308
- result << node.block if node.block
309
- result
310
- when Prism::ForwardingSuperNode
311
- node.block ? [node.block] : []
312
- when Prism::RescueModifierNode
313
- [node.expression, node.rescue_expression].compact
314
- when Prism::RangeNode
315
- [node.left, node.right].compact
316
- when Prism::RegularExpressionNode
317
- []
318
- when Prism::SplatNode
319
- [node.expression].compact
320
- when Prism::AndNode
321
- [node.left, node.right].compact
322
- when Prism::InterpolatedRegularExpressionNode, Prism::InterpolatedSymbolNode,
323
- Prism::InterpolatedXStringNode
324
- node.parts || []
325
- when Prism::XStringNode
326
- []
327
- when Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode, Prism::SelfNode
328
- []
329
- when Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode
330
- [node.value].compact
331
- when Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
332
- Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
333
- Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
334
- Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
335
- Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode
336
- [node.value].compact
337
- when Prism::ClassVariableOperatorWriteNode, Prism::GlobalVariableOperatorWriteNode,
338
- Prism::LocalVariableOperatorWriteNode, Prism::InstanceVariableOperatorWriteNode,
339
- Prism::ConstantOperatorWriteNode
340
- [node.value].compact
341
- when Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode,
342
- Prism::ConstantPathOperatorWriteNode
343
- [node.target, node.value].compact
344
- when Prism::ConstantPathWriteNode
345
- [node.target, node.value].compact
346
- when Prism::CaseMatchNode
347
- [node.predicate, *node.conditions, node.else_clause].compact
348
- when Prism::InNode
349
- [node.pattern, node.statements].compact
350
- when Prism::MatchPredicateNode, Prism::MatchRequiredNode
351
- [node.value, node.pattern].compact
352
- when Prism::ParenthesesNode
353
- [node.body].compact
354
- when Prism::DefinedNode
355
- [node.value].compact
356
- when Prism::SingletonClassNode
357
- [node.expression, node.body].compact
358
- when Prism::AliasMethodNode
359
- [node.new_name, node.old_name].compact
360
- when Prism::AliasGlobalVariableNode
361
- [node.new_name, node.old_name].compact
362
- when Prism::UndefNode
363
- node.names || []
364
- when Prism::AssocSplatNode
365
- [node.value].compact
366
- when Prism::BlockArgumentNode
367
- [node.expression].compact
368
- when Prism::MultiWriteNode
369
- [*node.lefts, node.rest, *node.rights, node.value].compact
370
- when Prism::MultiTargetNode
371
- [*node.lefts, node.rest, *node.rights].compact
372
- when Prism::SourceFileNode, Prism::SourceLineNode, Prism::SourceEncodingNode
373
- []
374
- when Prism::PreExecutionNode, Prism::PostExecutionNode
375
- [node.statements].compact
376
- # Numeric literals
377
- when Prism::RationalNode, Prism::ImaginaryNode
378
- [node.numeric].compact
379
- # String interpolation
380
- when Prism::EmbeddedVariableNode
381
- [node.variable].compact
382
- # Pattern matching patterns
383
- when Prism::ArrayPatternNode
384
- [*node.requireds, node.rest, *node.posts].compact
385
- when Prism::HashPatternNode
386
- [*node.elements, node.rest].compact
387
- when Prism::FindPatternNode
388
- [node.left, *node.requireds, node.right].compact
389
- when Prism::CapturePatternNode
390
- [node.value, node.target].compact
391
- when Prism::AlternationPatternNode
392
- [node.left, node.right].compact
393
- when Prism::PinnedExpressionNode
394
- [node.expression].compact
395
- when Prism::PinnedVariableNode
396
- [node.variable].compact
397
- # Forwarding and special parameters
398
- when Prism::ForwardingArgumentsNode, Prism::ForwardingParameterNode,
399
- Prism::NoKeywordsParameterNode
400
- []
401
- # References
402
- when Prism::BackReferenceReadNode, Prism::NumberedReferenceReadNode
403
- []
404
- # Call/Index compound assignment
405
- when Prism::CallAndWriteNode, Prism::CallOrWriteNode, Prism::CallOperatorWriteNode
406
- [node.receiver, node.value].compact
407
- when Prism::IndexAndWriteNode, Prism::IndexOrWriteNode, Prism::IndexOperatorWriteNode
408
- [node.receiver, node.arguments, node.value].compact
409
- # Match
410
- when Prism::MatchWriteNode
411
- [node.call, *node.targets].compact
412
- when Prism::MatchLastLineNode, Prism::InterpolatedMatchLastLineNode
413
- []
414
- # Other
415
- when Prism::FlipFlopNode
416
- [node.left, node.right].compact
417
- when Prism::ImplicitNode
418
- [node.value].compact
419
- when Prism::ImplicitRestNode
420
- []
421
- else
422
- # For unknown types, try to get child nodes if they exist
423
- []
424
- end
425
- rescue StandardError => e
426
- # Log warning in debug mode but continue processing
427
- warn "Warning: Failed to extract children from #{node.class}: #{e.message}" if $DEBUG
428
- children = []
429
- end
430
-
431
- children.compact.map { |child| convert_node(child) }
432
- end
433
-
434
- # Extract metadata specific to node type
435
- def self.extract_metadata(node)
436
- metadata = {}
437
-
438
- case node
439
- when Prism::ClassNode
440
- if (name = extract_class_or_module_name(node))
441
- metadata['name'] = name
442
- end
443
- if (superclass = extract_superclass_name(node))
444
- metadata['superclass'] = superclass
445
- end
446
- when Prism::ModuleNode
447
- if (name = extract_class_or_module_name(node))
448
- metadata['name'] = name
449
- end
450
- when Prism::DefNode
451
- # Prefer `name_loc.slice` over `name.to_s` so unary operator suffixes
452
- # (`def !@`, `def +@`, `def -@`) survive the round-trip. Prism
453
- # normalizes `name` to the symbol with the `@` stripped for `!@`,
454
- # which would otherwise rewrite `def !@` to `def !` silently.
455
- name = if node.respond_to?(:name_loc) && node.name_loc
456
- node.name_loc.slice
457
- else
458
- extract_node_name(node)
459
- end
460
- metadata['name'] = name if name
461
- metadata['parameters_count'] = extract_parameter_count(node).to_s
462
- # Extract parameters text directly from source
463
- if node.parameters
464
- metadata['parameters_text'] = node.parameters.location.slice
465
- metadata['has_parens'] = (!node.lparen_loc.nil?).to_s
466
- end
467
- # Check if this is a class method (def self.method_name)
468
- if node.respond_to?(:receiver) && node.receiver
469
- receiver = node.receiver
470
- if receiver.is_a?(Prism::SelfNode)
471
- metadata['receiver'] = 'self'
472
- elsif receiver.respond_to?(:slice)
473
- metadata['receiver'] = receiver.slice
474
- end
475
- end
476
- when Prism::CallNode
477
- if (name = extract_node_name(node))
478
- metadata['name'] = name
479
- end
480
- if (message = extract_message_name(node))
481
- metadata['message'] = message
482
- end
483
- when Prism::StringNode
484
- if (content = extract_string_content(node))
485
- metadata['content'] = content
486
- end
487
- when Prism::IntegerNode
488
- if (value = extract_literal_value(node))
489
- metadata['value'] = value
490
- end
491
- when Prism::FloatNode
492
- if (value = extract_literal_value(node))
493
- metadata['value'] = value
494
- end
495
- when Prism::SymbolNode
496
- if (value = extract_literal_value(node))
497
- metadata['value'] = value
498
- end
499
- when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode
500
- metadata['name'] = node.name.to_s
501
- when Prism::IfNode, Prism::UnlessNode
502
- # Detect ternary operator: if_keyword_loc is nil for ternary
503
- metadata['is_ternary'] = node.if_keyword_loc.nil?.to_s if node.respond_to?(:if_keyword_loc)
504
- end
505
-
506
- metadata
507
- end
508
-
509
- # Extract comments associated with the node
510
- def self.extract_comments(_node)
511
- # Prism attaches comments to the parse result, not individual nodes
512
- # For Phase 1, we'll return empty array and implement in Phase 2
513
- []
514
- end
515
-
516
- # Extract formatting information
517
- def self.extract_formatting(node)
518
- loc = node.location
519
- {
520
- indent_level: 0, # Will be calculated during formatting
521
- needs_blank_line_before: false,
522
- needs_blank_line_after: false,
523
- preserve_newlines: false,
524
- multiline: loc.start_line != loc.end_line,
525
- original_formatting: nil # Can store original text if needed
526
- }
527
- end
528
- end
529
- end
@@ -1,115 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rfmt
4
- # PrismNodeExtractor provides safe methods to extract information from Prism nodes
5
- # This module encapsulates the logic for accessing Prism node properties,
6
- # making the code resilient to Prism API changes
7
- module PrismNodeExtractor
8
- # Extract the name from a node
9
- # @param node [Prism::Node] The node to extract name from
10
- # @return [String, nil] The node name or nil if not available
11
- def extract_node_name(node)
12
- return nil unless node.respond_to?(:name)
13
-
14
- node.name.to_s
15
- end
16
-
17
- # Extract full name from class or module node (handles namespaced names like Foo::Bar::Baz)
18
- # @param node [Prism::ClassNode, Prism::ModuleNode] The class or module node
19
- # @return [String, nil] The full name or nil if not available
20
- def extract_class_or_module_name(node)
21
- return nil unless node.respond_to?(:constant_path)
22
-
23
- cp = node.constant_path
24
- return node.name.to_s if cp.nil?
25
-
26
- case cp
27
- when Prism::ConstantReadNode
28
- cp.name.to_s
29
- when Prism::ConstantPathNode
30
- if cp.respond_to?(:full_name)
31
- cp.full_name.to_s
32
- elsif cp.respond_to?(:slice)
33
- cp.slice
34
- else
35
- cp.location.slice
36
- end
37
- else
38
- node.name.to_s
39
- end
40
- end
41
-
42
- # Extract superclass name from a class node
43
- # @param class_node [Prism::ClassNode] The class node
44
- # @return [String, nil] The superclass name or nil if not available
45
- def extract_superclass_name(class_node)
46
- return nil unless class_node.respond_to?(:superclass)
47
-
48
- sc = class_node.superclass
49
- return nil if sc.nil?
50
-
51
- case sc
52
- when Prism::ConstantReadNode
53
- sc.name.to_s
54
- when Prism::ConstantPathNode
55
- # Try full_name first, fall back to slice for original source
56
- if sc.respond_to?(:full_name)
57
- sc.full_name.to_s
58
- elsif sc.respond_to?(:slice)
59
- sc.slice
60
- else
61
- sc.location.slice
62
- end
63
- when Prism::CallNode
64
- # Handle cases like ActiveRecord::Migration[8.1]
65
- # Use slice to get the original source text
66
- sc.slice
67
- else
68
- # Fallback: try to get original source text
69
- if sc.respond_to?(:slice)
70
- sc.slice
71
- else
72
- sc.location.slice
73
- end
74
- end
75
- end
76
-
77
- # Extract parameter count from a method definition node
78
- # @param def_node [Prism::DefNode] The method definition node
79
- # @return [Integer] The number of parameters (0 if none)
80
- def extract_parameter_count(def_node)
81
- return 0 unless def_node.respond_to?(:parameters)
82
- return 0 if def_node.parameters.nil?
83
- return 0 unless def_node.parameters.respond_to?(:child_nodes)
84
-
85
- def_node.parameters.child_nodes.compact.length
86
- end
87
-
88
- # Extract message name from a call node
89
- # @param call_node [Prism::CallNode] The call node
90
- # @return [String, nil] The message name or nil if not available
91
- def extract_message_name(call_node)
92
- return nil unless call_node.respond_to?(:message)
93
-
94
- call_node.message.to_s
95
- end
96
-
97
- # Extract content from a string node
98
- # @param string_node [Prism::StringNode] The string node
99
- # @return [String, nil] The string content or nil if not available
100
- def extract_string_content(string_node)
101
- return nil unless string_node.respond_to?(:content)
102
-
103
- string_node.content
104
- end
105
-
106
- # Extract value from a literal node (Integer, Float, Symbol)
107
- # @param node [Prism::Node] The literal node
108
- # @return [String, nil] The value as string or nil if not available
109
- def extract_literal_value(node)
110
- return nil unless node.respond_to?(:value)
111
-
112
- node.value.to_s
113
- end
114
- end
115
- end