rfmt 1.3.0-arm64-darwin
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +223 -0
- data/LICENSE.txt +21 -0
- data/README.md +397 -0
- data/exe/rfmt +21 -0
- data/lib/rfmt/3.1/rfmt.bundle +0 -0
- data/lib/rfmt/3.2/rfmt.bundle +0 -0
- data/lib/rfmt/3.3/rfmt.bundle +0 -0
- data/lib/rfmt/cache.rb +112 -0
- data/lib/rfmt/cli.rb +308 -0
- data/lib/rfmt/configuration.rb +95 -0
- data/lib/rfmt/prism_bridge.rb +390 -0
- data/lib/rfmt/prism_node_extractor.rb +115 -0
- data/lib/rfmt/version.rb +5 -0
- data/lib/rfmt.rb +172 -0
- data/lib/ruby_lsp/rfmt/addon.rb +20 -0
- data/lib/ruby_lsp/rfmt/formatter_runner.rb +26 -0
- metadata +68 -0
|
@@ -0,0 +1,390 @@
|
|
|
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))
|
|
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
|
+
{
|
|
66
|
+
comment_type: comment.class.name.split('::').last.downcase.gsub('comment', ''),
|
|
67
|
+
location: {
|
|
68
|
+
start_line: comment.location.start_line,
|
|
69
|
+
start_column: comment.location.start_column,
|
|
70
|
+
end_line: comment.location.end_line,
|
|
71
|
+
end_column: comment.location.end_column,
|
|
72
|
+
start_offset: comment.location.start_offset,
|
|
73
|
+
end_offset: comment.location.end_offset
|
|
74
|
+
},
|
|
75
|
+
text: comment.location.slice,
|
|
76
|
+
position: 'leading' # Default position, will be refined by Rust
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
JSON.generate({
|
|
81
|
+
ast: convert_node(result.value),
|
|
82
|
+
comments: comments
|
|
83
|
+
})
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Convert a Prism node to our internal representation
|
|
87
|
+
def self.convert_node(node)
|
|
88
|
+
return nil if node.nil?
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
node_type: node_type_name(node),
|
|
92
|
+
location: extract_location(node),
|
|
93
|
+
children: extract_children(node),
|
|
94
|
+
metadata: extract_metadata(node),
|
|
95
|
+
comments: extract_comments(node),
|
|
96
|
+
formatting: extract_formatting(node)
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Get the node type name from Prism node
|
|
101
|
+
def self.node_type_name(node)
|
|
102
|
+
# Prism node class names are like "Prism::ProgramNode"
|
|
103
|
+
# We want just "program_node" in snake_case
|
|
104
|
+
node.class.name.split('::').last.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
105
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Extract location information from node
|
|
109
|
+
def self.extract_location(node)
|
|
110
|
+
loc = node.location
|
|
111
|
+
{
|
|
112
|
+
start_line: loc.start_line,
|
|
113
|
+
start_column: loc.start_column,
|
|
114
|
+
end_line: loc.end_line,
|
|
115
|
+
end_column: loc.end_column,
|
|
116
|
+
start_offset: loc.start_offset,
|
|
117
|
+
end_offset: loc.end_offset
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Extract child nodes
|
|
122
|
+
def self.extract_children(node)
|
|
123
|
+
children = []
|
|
124
|
+
|
|
125
|
+
begin
|
|
126
|
+
# Different node types have different child accessors
|
|
127
|
+
children = case node
|
|
128
|
+
when Prism::ProgramNode
|
|
129
|
+
node.statements ? node.statements.body : []
|
|
130
|
+
when Prism::StatementsNode
|
|
131
|
+
node.body || []
|
|
132
|
+
when Prism::ClassNode
|
|
133
|
+
[
|
|
134
|
+
node.constant_path,
|
|
135
|
+
node.superclass,
|
|
136
|
+
node.body
|
|
137
|
+
].compact
|
|
138
|
+
when Prism::ModuleNode
|
|
139
|
+
[
|
|
140
|
+
node.constant_path,
|
|
141
|
+
node.body
|
|
142
|
+
].compact
|
|
143
|
+
when Prism::DefNode
|
|
144
|
+
params = if node.parameters
|
|
145
|
+
node.parameters.child_nodes.compact
|
|
146
|
+
else
|
|
147
|
+
[]
|
|
148
|
+
end
|
|
149
|
+
params + [node.body].compact
|
|
150
|
+
when Prism::CallNode
|
|
151
|
+
result = []
|
|
152
|
+
result << node.receiver if node.receiver
|
|
153
|
+
result.concat(node.arguments.child_nodes.compact) if node.arguments
|
|
154
|
+
result << node.block if node.block
|
|
155
|
+
result
|
|
156
|
+
when Prism::IfNode, Prism::UnlessNode
|
|
157
|
+
[
|
|
158
|
+
node.predicate,
|
|
159
|
+
node.statements,
|
|
160
|
+
node.consequent
|
|
161
|
+
].compact
|
|
162
|
+
when Prism::ElseNode
|
|
163
|
+
[node.statements].compact
|
|
164
|
+
when Prism::ArrayNode
|
|
165
|
+
node.elements || []
|
|
166
|
+
when Prism::HashNode
|
|
167
|
+
node.elements || []
|
|
168
|
+
when Prism::BlockNode
|
|
169
|
+
params = if node.parameters
|
|
170
|
+
node.parameters.child_nodes.compact
|
|
171
|
+
else
|
|
172
|
+
[]
|
|
173
|
+
end
|
|
174
|
+
params + [node.body].compact
|
|
175
|
+
when Prism::BeginNode
|
|
176
|
+
[
|
|
177
|
+
node.statements,
|
|
178
|
+
node.rescue_clause,
|
|
179
|
+
node.ensure_clause
|
|
180
|
+
].compact
|
|
181
|
+
when Prism::EnsureNode
|
|
182
|
+
[node.statements].compact
|
|
183
|
+
when Prism::LambdaNode
|
|
184
|
+
params = if node.parameters
|
|
185
|
+
node.parameters.child_nodes.compact
|
|
186
|
+
else
|
|
187
|
+
[]
|
|
188
|
+
end
|
|
189
|
+
params + [node.body].compact
|
|
190
|
+
when Prism::RescueNode
|
|
191
|
+
result = []
|
|
192
|
+
result.concat(node.exceptions) if node.exceptions
|
|
193
|
+
result << node.reference if node.reference
|
|
194
|
+
result << node.statements if node.statements
|
|
195
|
+
result << node.subsequent if node.subsequent
|
|
196
|
+
result
|
|
197
|
+
when Prism::SymbolNode, Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode
|
|
198
|
+
[]
|
|
199
|
+
when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode
|
|
200
|
+
[node.value].compact
|
|
201
|
+
when Prism::ReturnNode
|
|
202
|
+
node.arguments ? node.arguments.child_nodes.compact : []
|
|
203
|
+
when Prism::OrNode
|
|
204
|
+
[node.left, node.right].compact
|
|
205
|
+
when Prism::AssocNode
|
|
206
|
+
[node.key, node.value].compact
|
|
207
|
+
when Prism::KeywordHashNode
|
|
208
|
+
node.elements || []
|
|
209
|
+
when Prism::InterpolatedStringNode
|
|
210
|
+
node.parts || []
|
|
211
|
+
when Prism::EmbeddedStatementsNode
|
|
212
|
+
[node.statements].compact
|
|
213
|
+
when Prism::CaseNode
|
|
214
|
+
[node.predicate, *node.conditions, node.else_clause].compact
|
|
215
|
+
when Prism::WhenNode
|
|
216
|
+
[*node.conditions, node.statements].compact
|
|
217
|
+
when Prism::WhileNode, Prism::UntilNode
|
|
218
|
+
[node.predicate, node.statements].compact
|
|
219
|
+
when Prism::ForNode
|
|
220
|
+
[node.index, node.collection, node.statements].compact
|
|
221
|
+
when Prism::BreakNode, Prism::NextNode
|
|
222
|
+
node.arguments ? node.arguments.child_nodes.compact : []
|
|
223
|
+
when Prism::RedoNode, Prism::RetryNode
|
|
224
|
+
[]
|
|
225
|
+
when Prism::YieldNode
|
|
226
|
+
node.arguments ? node.arguments.child_nodes.compact : []
|
|
227
|
+
when Prism::SuperNode
|
|
228
|
+
result = []
|
|
229
|
+
result.concat(node.arguments.child_nodes.compact) if node.arguments
|
|
230
|
+
result << node.block if node.block
|
|
231
|
+
result
|
|
232
|
+
when Prism::ForwardingSuperNode
|
|
233
|
+
node.block ? [node.block] : []
|
|
234
|
+
when Prism::RescueModifierNode
|
|
235
|
+
[node.expression, node.rescue_expression].compact
|
|
236
|
+
when Prism::RangeNode
|
|
237
|
+
[node.left, node.right].compact
|
|
238
|
+
when Prism::RegularExpressionNode
|
|
239
|
+
[]
|
|
240
|
+
when Prism::SplatNode
|
|
241
|
+
[node.expression].compact
|
|
242
|
+
when Prism::AndNode
|
|
243
|
+
[node.left, node.right].compact
|
|
244
|
+
when Prism::NotNode
|
|
245
|
+
[node.expression].compact
|
|
246
|
+
when Prism::InterpolatedRegularExpressionNode, Prism::InterpolatedSymbolNode,
|
|
247
|
+
Prism::InterpolatedXStringNode
|
|
248
|
+
node.parts || []
|
|
249
|
+
when Prism::XStringNode
|
|
250
|
+
[]
|
|
251
|
+
when Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode, Prism::SelfNode
|
|
252
|
+
[]
|
|
253
|
+
when Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode
|
|
254
|
+
[node.value].compact
|
|
255
|
+
when Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
|
|
256
|
+
Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
|
|
257
|
+
Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
|
|
258
|
+
Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
259
|
+
Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode
|
|
260
|
+
[node.value].compact
|
|
261
|
+
when Prism::ClassVariableOperatorWriteNode, Prism::GlobalVariableOperatorWriteNode,
|
|
262
|
+
Prism::LocalVariableOperatorWriteNode, Prism::InstanceVariableOperatorWriteNode,
|
|
263
|
+
Prism::ConstantOperatorWriteNode
|
|
264
|
+
[node.value].compact
|
|
265
|
+
when Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode,
|
|
266
|
+
Prism::ConstantPathOperatorWriteNode
|
|
267
|
+
[node.target, node.value].compact
|
|
268
|
+
when Prism::ConstantPathWriteNode
|
|
269
|
+
[node.target, node.value].compact
|
|
270
|
+
when Prism::CaseMatchNode
|
|
271
|
+
[node.predicate, *node.conditions, node.else_clause].compact
|
|
272
|
+
when Prism::InNode
|
|
273
|
+
[node.pattern, node.statements].compact
|
|
274
|
+
when Prism::MatchPredicateNode, Prism::MatchRequiredNode
|
|
275
|
+
[node.value, node.pattern].compact
|
|
276
|
+
when Prism::ParenthesesNode
|
|
277
|
+
[node.body].compact
|
|
278
|
+
when Prism::DefinedNode
|
|
279
|
+
[node.value].compact
|
|
280
|
+
when Prism::SingletonClassNode
|
|
281
|
+
[node.expression, node.body].compact
|
|
282
|
+
when Prism::AliasMethodNode
|
|
283
|
+
[node.new_name, node.old_name].compact
|
|
284
|
+
when Prism::AliasGlobalVariableNode
|
|
285
|
+
[node.new_name, node.old_name].compact
|
|
286
|
+
when Prism::UndefNode
|
|
287
|
+
node.names || []
|
|
288
|
+
when Prism::AssocSplatNode
|
|
289
|
+
[node.value].compact
|
|
290
|
+
when Prism::BlockArgumentNode
|
|
291
|
+
[node.expression].compact
|
|
292
|
+
when Prism::MultiWriteNode
|
|
293
|
+
[*node.lefts, node.rest, *node.rights, node.value].compact
|
|
294
|
+
when Prism::MultiTargetNode
|
|
295
|
+
[*node.lefts, node.rest, *node.rights].compact
|
|
296
|
+
else
|
|
297
|
+
# For unknown types, try to get child nodes if they exist
|
|
298
|
+
[]
|
|
299
|
+
end
|
|
300
|
+
rescue StandardError => e
|
|
301
|
+
# Log warning in debug mode but continue processing
|
|
302
|
+
warn "Warning: Failed to extract children from #{node.class}: #{e.message}" if $DEBUG
|
|
303
|
+
children = []
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
children.compact.map { |child| convert_node(child) }
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Extract metadata specific to node type
|
|
310
|
+
def self.extract_metadata(node)
|
|
311
|
+
metadata = {}
|
|
312
|
+
|
|
313
|
+
case node
|
|
314
|
+
when Prism::ClassNode
|
|
315
|
+
if (name = extract_class_or_module_name(node))
|
|
316
|
+
metadata['name'] = name
|
|
317
|
+
end
|
|
318
|
+
if (superclass = extract_superclass_name(node))
|
|
319
|
+
metadata['superclass'] = superclass
|
|
320
|
+
end
|
|
321
|
+
when Prism::ModuleNode
|
|
322
|
+
if (name = extract_class_or_module_name(node))
|
|
323
|
+
metadata['name'] = name
|
|
324
|
+
end
|
|
325
|
+
when Prism::DefNode
|
|
326
|
+
if (name = extract_node_name(node))
|
|
327
|
+
metadata['name'] = name
|
|
328
|
+
end
|
|
329
|
+
metadata['parameters_count'] = extract_parameter_count(node).to_s
|
|
330
|
+
# Check if this is a class method (def self.method_name)
|
|
331
|
+
if node.respond_to?(:receiver) && node.receiver
|
|
332
|
+
receiver = node.receiver
|
|
333
|
+
if receiver.is_a?(Prism::SelfNode)
|
|
334
|
+
metadata['receiver'] = 'self'
|
|
335
|
+
elsif receiver.respond_to?(:slice)
|
|
336
|
+
metadata['receiver'] = receiver.slice
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
when Prism::CallNode
|
|
340
|
+
if (name = extract_node_name(node))
|
|
341
|
+
metadata['name'] = name
|
|
342
|
+
end
|
|
343
|
+
if (message = extract_message_name(node))
|
|
344
|
+
metadata['message'] = message
|
|
345
|
+
end
|
|
346
|
+
when Prism::StringNode
|
|
347
|
+
if (content = extract_string_content(node))
|
|
348
|
+
metadata['content'] = content
|
|
349
|
+
end
|
|
350
|
+
when Prism::IntegerNode
|
|
351
|
+
if (value = extract_literal_value(node))
|
|
352
|
+
metadata['value'] = value
|
|
353
|
+
end
|
|
354
|
+
when Prism::FloatNode
|
|
355
|
+
if (value = extract_literal_value(node))
|
|
356
|
+
metadata['value'] = value
|
|
357
|
+
end
|
|
358
|
+
when Prism::SymbolNode
|
|
359
|
+
if (value = extract_literal_value(node))
|
|
360
|
+
metadata['value'] = value
|
|
361
|
+
end
|
|
362
|
+
when Prism::IfNode, Prism::UnlessNode
|
|
363
|
+
# Detect ternary operator: if_keyword_loc is nil for ternary
|
|
364
|
+
metadata['is_ternary'] = node.if_keyword_loc.nil?.to_s if node.respond_to?(:if_keyword_loc)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
metadata
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Extract comments associated with the node
|
|
371
|
+
def self.extract_comments(_node)
|
|
372
|
+
# Prism attaches comments to the parse result, not individual nodes
|
|
373
|
+
# For Phase 1, we'll return empty array and implement in Phase 2
|
|
374
|
+
[]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Extract formatting information
|
|
378
|
+
def self.extract_formatting(node)
|
|
379
|
+
loc = node.location
|
|
380
|
+
{
|
|
381
|
+
indent_level: 0, # Will be calculated during formatting
|
|
382
|
+
needs_blank_line_before: false,
|
|
383
|
+
needs_blank_line_after: false,
|
|
384
|
+
preserve_newlines: false,
|
|
385
|
+
multiline: loc.start_line != loc.end_line,
|
|
386
|
+
original_formatting: nil # Can store original text if needed
|
|
387
|
+
}
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
data/lib/rfmt/version.rb
ADDED
data/lib/rfmt.rb
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'rfmt/version'
|
|
4
|
+
require_relative 'rfmt/rfmt'
|
|
5
|
+
require_relative 'rfmt/prism_bridge'
|
|
6
|
+
|
|
7
|
+
module Rfmt
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
# Errors from Rust side
|
|
10
|
+
class RfmtError < Error; end
|
|
11
|
+
# AST validation errors
|
|
12
|
+
class ValidationError < RfmtError; end
|
|
13
|
+
|
|
14
|
+
# Format Ruby source code
|
|
15
|
+
# @param source [String] Ruby source code to format
|
|
16
|
+
# @return [String] Formatted Ruby code
|
|
17
|
+
def self.format(source)
|
|
18
|
+
# Step 1: Parse with Prism (Ruby side)
|
|
19
|
+
prism_json = PrismBridge.parse(source)
|
|
20
|
+
|
|
21
|
+
# Step 2: Format in Rust
|
|
22
|
+
# Pass both source and AST to enable source extraction fallback
|
|
23
|
+
format_code(source, prism_json)
|
|
24
|
+
rescue PrismBridge::ParseError => e
|
|
25
|
+
# Re-raise with more context
|
|
26
|
+
raise Error, "Failed to parse Ruby code: #{e.message}"
|
|
27
|
+
rescue RfmtError
|
|
28
|
+
# Rust side errors are re-raised as-is to preserve error details
|
|
29
|
+
raise
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
raise Error, "Unexpected error during formatting: #{e.class}: #{e.message}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Format a Ruby file
|
|
35
|
+
# @param path [String] Path to Ruby file
|
|
36
|
+
# @return [String] Formatted Ruby code
|
|
37
|
+
def self.format_file(path)
|
|
38
|
+
source = File.read(path)
|
|
39
|
+
format(source)
|
|
40
|
+
rescue Errno::ENOENT
|
|
41
|
+
raise Error, "File not found: #{path}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get version information
|
|
45
|
+
# @return [String] Version string including Ruby and Rust versions
|
|
46
|
+
def self.version_info
|
|
47
|
+
"Ruby: #{VERSION}, Rust: #{rust_version}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Parse Ruby code to AST (for debugging)
|
|
51
|
+
# @param source [String] Ruby source code
|
|
52
|
+
# @return [String] AST representation
|
|
53
|
+
def self.parse(source)
|
|
54
|
+
prism_json = PrismBridge.parse(source)
|
|
55
|
+
parse_to_json(prism_json)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Configuration management
|
|
59
|
+
module Config
|
|
60
|
+
# Default configuration template
|
|
61
|
+
DEFAULT_CONFIG = <<~YAML
|
|
62
|
+
# rfmt Configuration File
|
|
63
|
+
# This file controls how rfmt formats your Ruby code.
|
|
64
|
+
# See https://github.com/fs0414/rfmt for full documentation.
|
|
65
|
+
|
|
66
|
+
version: "1.0"
|
|
67
|
+
|
|
68
|
+
# Formatting options
|
|
69
|
+
formatting:
|
|
70
|
+
# Maximum line length before wrapping (40-500)
|
|
71
|
+
line_length: 100
|
|
72
|
+
|
|
73
|
+
# Number of spaces or tabs per indentation level (1-8)
|
|
74
|
+
indent_width: 2
|
|
75
|
+
|
|
76
|
+
# Use "spaces" or "tabs" for indentation
|
|
77
|
+
indent_style: "spaces"
|
|
78
|
+
|
|
79
|
+
# Quote style for strings: "double", "single", or "consistent"
|
|
80
|
+
quote_style: "double"
|
|
81
|
+
|
|
82
|
+
# Files to include in formatting (glob patterns)
|
|
83
|
+
include:
|
|
84
|
+
- "**/*.rb"
|
|
85
|
+
- "**/*.rake"
|
|
86
|
+
- "**/Rakefile"
|
|
87
|
+
- "**/Gemfile"
|
|
88
|
+
|
|
89
|
+
# Files to exclude from formatting (glob patterns)
|
|
90
|
+
exclude:
|
|
91
|
+
- "vendor/**/*"
|
|
92
|
+
- "tmp/**/*"
|
|
93
|
+
- "node_modules/**/*"
|
|
94
|
+
- "db/schema.rb"
|
|
95
|
+
YAML
|
|
96
|
+
|
|
97
|
+
# Generate a default configuration file
|
|
98
|
+
# @param path [String] Path where to create the config file (default: .rfmt.yml)
|
|
99
|
+
# @param force [Boolean] Overwrite existing file if true
|
|
100
|
+
# @return [Boolean] true if file was created, false if already exists
|
|
101
|
+
def self.init(path = '.rfmt.yml', force: false)
|
|
102
|
+
if File.exist?(path) && !force
|
|
103
|
+
warn "Configuration file already exists: #{path}"
|
|
104
|
+
warn 'Use force: true to overwrite'
|
|
105
|
+
return false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
File.write(path, DEFAULT_CONFIG)
|
|
109
|
+
puts "Created rfmt configuration file: #{path}"
|
|
110
|
+
true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Find configuration file in current or parent directories
|
|
114
|
+
# @return [String, nil] Path to config file or nil if not found
|
|
115
|
+
def self.find
|
|
116
|
+
current_dir = Dir.pwd
|
|
117
|
+
|
|
118
|
+
loop do
|
|
119
|
+
['.rfmt.yml', '.rfmt.yaml', 'rfmt.yml', 'rfmt.yaml'].each do |filename|
|
|
120
|
+
config_path = File.join(current_dir, filename)
|
|
121
|
+
return config_path if File.exist?(config_path)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
parent = File.dirname(current_dir)
|
|
125
|
+
break if parent == current_dir # Reached root
|
|
126
|
+
|
|
127
|
+
current_dir = parent
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Check user home directory
|
|
131
|
+
home_dir = begin
|
|
132
|
+
Dir.home
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
if home_dir
|
|
137
|
+
['.rfmt.yml', '.rfmt.yaml', 'rfmt.yml', 'rfmt.yaml'].each do |filename|
|
|
138
|
+
config_path = File.join(home_dir, filename)
|
|
139
|
+
return config_path if File.exist?(config_path)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Check if configuration file exists
|
|
147
|
+
# @return [Boolean] true if config file exists
|
|
148
|
+
def self.exists?
|
|
149
|
+
!find.nil?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Load and validate configuration file
|
|
153
|
+
# @param path [String, nil] Path to config file (default: auto-detect)
|
|
154
|
+
# @return [Hash] Loaded configuration
|
|
155
|
+
def self.load(path = nil)
|
|
156
|
+
require 'yaml'
|
|
157
|
+
|
|
158
|
+
config_path = path || find
|
|
159
|
+
|
|
160
|
+
unless config_path
|
|
161
|
+
warn 'No configuration file found, using defaults'
|
|
162
|
+
return {}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
YAML.load_file(config_path)
|
|
166
|
+
rescue Errno::ENOENT
|
|
167
|
+
raise Error, "Configuration file not found: #{config_path}"
|
|
168
|
+
rescue Psych::SyntaxError => e
|
|
169
|
+
raise Error, "Invalid YAML in configuration file: #{e.message}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_lsp/addon'
|
|
4
|
+
require_relative 'formatter_runner'
|
|
5
|
+
|
|
6
|
+
module RubyLsp
|
|
7
|
+
module Rfmt
|
|
8
|
+
class Addon < ::RubyLsp::Addon
|
|
9
|
+
def name
|
|
10
|
+
'rfmt'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def activate(global_state, _message_queue)
|
|
14
|
+
global_state.register_formatter('rfmt', FormatterRunner.new)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def deactivate; end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|