rfmt 0.1.0 → 0.2.2

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.
@@ -0,0 +1,363 @@
1
+ use crate::ast::{Comment, Node, NodeType};
2
+ use crate::config::{Config, IndentStyle};
3
+ use crate::error::Result;
4
+ use std::fmt::Write;
5
+
6
+ /// Code emitter that converts AST back to Ruby source code
7
+ pub struct Emitter {
8
+ config: Config,
9
+ source: String,
10
+ buffer: String,
11
+ all_comments: Vec<Comment>,
12
+ emitted_comment_indices: Vec<usize>,
13
+ }
14
+
15
+ impl Emitter {
16
+ pub fn new(config: Config) -> Self {
17
+ Self {
18
+ config,
19
+ source: String::new(),
20
+ buffer: String::new(),
21
+ all_comments: Vec::new(),
22
+ emitted_comment_indices: Vec::new(),
23
+ }
24
+ }
25
+
26
+ /// Create emitter with source code for fallback extraction
27
+ pub fn with_source(config: Config, source: String) -> Self {
28
+ Self {
29
+ config,
30
+ source,
31
+ buffer: String::new(),
32
+ all_comments: Vec::new(),
33
+ emitted_comment_indices: Vec::new(),
34
+ }
35
+ }
36
+
37
+ /// Emit Ruby source code from an AST
38
+ pub fn emit(&mut self, ast: &Node) -> Result<String> {
39
+ self.buffer.clear();
40
+ self.emitted_comment_indices.clear();
41
+
42
+ // Collect all comments from the AST
43
+ self.collect_comments(ast);
44
+
45
+ self.emit_node(ast, 0)?;
46
+ Ok(self.buffer.clone())
47
+ }
48
+
49
+ /// Recursively collect all comments from the AST
50
+ fn collect_comments(&mut self, node: &Node) {
51
+ self.all_comments.extend(node.comments.clone());
52
+ for child in &node.children {
53
+ self.collect_comments(child);
54
+ }
55
+ }
56
+
57
+ /// Emit comments that appear before a given line
58
+ fn emit_comments_before(&mut self, line: usize, indent_level: usize) -> Result<()> {
59
+ let indent_str = match self.config.formatting.indent_style {
60
+ IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * indent_level),
61
+ IndentStyle::Tabs => "\t".repeat(indent_level),
62
+ };
63
+
64
+ let mut indices_to_emit = Vec::new();
65
+ for (idx, comment) in self.all_comments.iter().enumerate() {
66
+ if self.emitted_comment_indices.contains(&idx) {
67
+ continue;
68
+ }
69
+
70
+ // Collect comments that end before this line
71
+ if comment.location.end_line < line {
72
+ indices_to_emit.push((idx, comment.text.clone()));
73
+ }
74
+ }
75
+
76
+ // Now emit the collected comments
77
+ for (idx, text) in indices_to_emit {
78
+ writeln!(self.buffer, "{}{}", indent_str, text)?;
79
+ self.emitted_comment_indices.push(idx);
80
+ }
81
+
82
+ Ok(())
83
+ }
84
+
85
+ /// Emit comments that appear on the same line (trailing comments)
86
+ fn emit_trailing_comments(&mut self, line: usize) -> Result<()> {
87
+ let mut indices_to_emit = Vec::new();
88
+ for (idx, comment) in self.all_comments.iter().enumerate() {
89
+ if self.emitted_comment_indices.contains(&idx) {
90
+ continue;
91
+ }
92
+
93
+ // Collect comments on the same line (trailing)
94
+ if comment.location.start_line == line {
95
+ indices_to_emit.push((idx, comment.text.clone()));
96
+ }
97
+ }
98
+
99
+ // Now emit the collected comments
100
+ for (idx, text) in indices_to_emit {
101
+ write!(self.buffer, " {}", text)?;
102
+ self.emitted_comment_indices.push(idx);
103
+ }
104
+
105
+ Ok(())
106
+ }
107
+
108
+ /// Emit a node with given indentation level
109
+ fn emit_node(&mut self, node: &Node, indent_level: usize) -> Result<()> {
110
+ match &node.node_type {
111
+ NodeType::ProgramNode => self.emit_program(node, indent_level)?,
112
+ NodeType::StatementsNode => self.emit_statements(node, indent_level)?,
113
+ NodeType::ClassNode => self.emit_class(node, indent_level)?,
114
+ NodeType::ModuleNode => self.emit_module(node, indent_level)?,
115
+ NodeType::DefNode => self.emit_method(node, indent_level)?,
116
+ _ => self.emit_generic(node, indent_level)?,
117
+ }
118
+ Ok(())
119
+ }
120
+
121
+ /// Emit program node (root)
122
+ fn emit_program(&mut self, node: &Node, indent_level: usize) -> Result<()> {
123
+ for (i, child) in node.children.iter().enumerate() {
124
+ self.emit_node(child, indent_level)?;
125
+
126
+ // Add newlines between top-level statements, normalizing to max 1 blank line
127
+ if i < node.children.len() - 1 {
128
+ let current_end_line = child.location.end_line;
129
+ let next_start_line = node.children[i + 1].location.start_line;
130
+ let line_diff = next_start_line.saturating_sub(current_end_line);
131
+
132
+ // Add 1 newline if consecutive, 2 newlines (1 blank line) if there was a gap
133
+ let newlines = if line_diff > 1 { 2 } else { 1 };
134
+ for _ in 0..newlines {
135
+ self.buffer.push('\n');
136
+ }
137
+ }
138
+ }
139
+ Ok(())
140
+ }
141
+
142
+ /// Emit statements node (body of class/module/def)
143
+ fn emit_statements(&mut self, node: &Node, indent_level: usize) -> Result<()> {
144
+ for (i, child) in node.children.iter().enumerate() {
145
+ self.emit_node(child, indent_level)?;
146
+
147
+ // Add newlines between statements, normalizing to max 1 blank line
148
+ if i < node.children.len() - 1 {
149
+ let current_end_line = child.location.end_line;
150
+ let next_start_line = node.children[i + 1].location.start_line;
151
+ let line_diff = next_start_line.saturating_sub(current_end_line);
152
+
153
+ // Add 1 newline if consecutive, 2 newlines (1 blank line) if there was a gap
154
+ let newlines = if line_diff > 1 { 2 } else { 1 };
155
+ for _ in 0..newlines {
156
+ self.buffer.push('\n');
157
+ }
158
+ }
159
+ }
160
+ Ok(())
161
+ }
162
+
163
+ /// Emit class definition
164
+ fn emit_class(&mut self, node: &Node, indent_level: usize) -> Result<()> {
165
+ // Emit any comments before this class
166
+ self.emit_comments_before(node.location.start_line, indent_level)?;
167
+
168
+ self.emit_indent(indent_level)?;
169
+ write!(self.buffer, "class ")?;
170
+
171
+ if let Some(name) = node.metadata.get("name") {
172
+ write!(self.buffer, "{}", name)?;
173
+ }
174
+
175
+ if let Some(superclass) = node.metadata.get("superclass") {
176
+ write!(self.buffer, " < {}", superclass)?;
177
+ }
178
+
179
+ self.buffer.push('\n');
180
+
181
+ // Emit body (children), but skip structural nodes like constant_read_node
182
+ for child in &node.children {
183
+ if self.is_structural_node(&child.node_type) {
184
+ continue;
185
+ }
186
+ self.emit_node(child, indent_level + 1)?;
187
+ // Note: don't add newline here, statements node will handle it
188
+ }
189
+
190
+ // Add newline before end if there was body content
191
+ if node
192
+ .children
193
+ .iter()
194
+ .any(|c| !self.is_structural_node(&c.node_type))
195
+ {
196
+ self.buffer.push('\n');
197
+ }
198
+
199
+ self.emit_indent(indent_level)?;
200
+ write!(self.buffer, "end")?;
201
+
202
+ Ok(())
203
+ }
204
+
205
+ /// Emit module definition
206
+ fn emit_module(&mut self, node: &Node, indent_level: usize) -> Result<()> {
207
+ // Emit any comments before this module
208
+ self.emit_comments_before(node.location.start_line, indent_level)?;
209
+
210
+ self.emit_indent(indent_level)?;
211
+ write!(self.buffer, "module ")?;
212
+
213
+ if let Some(name) = node.metadata.get("name") {
214
+ write!(self.buffer, "{}", name)?;
215
+ }
216
+
217
+ self.buffer.push('\n');
218
+
219
+ // Emit body (children), but skip structural nodes
220
+ for child in &node.children {
221
+ if self.is_structural_node(&child.node_type) {
222
+ continue;
223
+ }
224
+ self.emit_node(child, indent_level + 1)?;
225
+ }
226
+
227
+ // Add newline before end if there was body content
228
+ if node
229
+ .children
230
+ .iter()
231
+ .any(|c| !self.is_structural_node(&c.node_type))
232
+ {
233
+ self.buffer.push('\n');
234
+ }
235
+
236
+ self.emit_indent(indent_level)?;
237
+ write!(self.buffer, "end")?;
238
+
239
+ Ok(())
240
+ }
241
+
242
+ /// Emit method definition
243
+ fn emit_method(&mut self, node: &Node, indent_level: usize) -> Result<()> {
244
+ // Emit any comments before this method
245
+ self.emit_comments_before(node.location.start_line, indent_level)?;
246
+
247
+ self.emit_indent(indent_level)?;
248
+ write!(self.buffer, "def ")?;
249
+
250
+ if let Some(name) = node.metadata.get("name") {
251
+ write!(self.buffer, "{}", name)?;
252
+ }
253
+
254
+ // TODO: Handle parameters properly
255
+ // For now, extract from source if method has parameters
256
+ if node
257
+ .metadata
258
+ .get("parameters_count")
259
+ .and_then(|s| s.parse::<usize>().ok())
260
+ .unwrap_or(0)
261
+ > 0
262
+ {
263
+ // Extract parameter part from source
264
+ if !self.source.is_empty() && node.location.end_offset <= self.source.len() {
265
+ if let Some(source_text) = self
266
+ .source
267
+ .get(node.location.start_offset..node.location.end_offset)
268
+ {
269
+ // Find parameters in source (between def name and \n or ;)
270
+ if let Some(def_line) = source_text.lines().next() {
271
+ if let Some(params_start) = def_line.find('(') {
272
+ if let Some(params_end) = def_line.find(')') {
273
+ let params = &def_line[params_start..=params_end];
274
+ write!(self.buffer, "{}", params)?;
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ self.buffer.push('\n');
283
+
284
+ // Emit body (children), but skip structural nodes like parameter nodes
285
+ for child in &node.children {
286
+ if self.is_structural_node(&child.node_type) {
287
+ continue;
288
+ }
289
+ self.emit_node(child, indent_level + 1)?;
290
+ }
291
+
292
+ // Add newline before end if there was body content
293
+ if node
294
+ .children
295
+ .iter()
296
+ .any(|c| !self.is_structural_node(&c.node_type))
297
+ {
298
+ self.buffer.push('\n');
299
+ }
300
+
301
+ self.emit_indent(indent_level)?;
302
+ write!(self.buffer, "end")?;
303
+
304
+ Ok(())
305
+ }
306
+
307
+ /// Emit generic node by extracting from source
308
+ fn emit_generic(&mut self, node: &Node, indent_level: usize) -> Result<()> {
309
+ // Emit any comments before this node
310
+ self.emit_comments_before(node.location.start_line, indent_level)?;
311
+
312
+ if !self.source.is_empty() {
313
+ let start = node.location.start_offset;
314
+ let end = node.location.end_offset;
315
+
316
+ // Clone text first to avoid borrow conflict
317
+ let text_owned = self.source.get(start..end).map(|s| s.to_string());
318
+
319
+ if let Some(text) = text_owned {
320
+ // Add indentation before the extracted text
321
+ self.emit_indent(indent_level)?;
322
+ write!(self.buffer, "{}", text)?;
323
+
324
+ // Emit any trailing comments on the same line
325
+ self.emit_trailing_comments(node.location.end_line)?;
326
+ }
327
+ }
328
+ Ok(())
329
+ }
330
+
331
+ /// Emit indentation
332
+ fn emit_indent(&mut self, level: usize) -> Result<()> {
333
+ let indent_str = match self.config.formatting.indent_style {
334
+ IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * level),
335
+ IndentStyle::Tabs => "\t".repeat(level),
336
+ };
337
+
338
+ write!(self.buffer, "{}", indent_str)?;
339
+ Ok(())
340
+ }
341
+
342
+ /// Check if node is structural (part of definition syntax, not body)
343
+ fn is_structural_node(&self, node_type: &NodeType) -> bool {
344
+ matches!(
345
+ node_type,
346
+ NodeType::ConstantReadNode
347
+ | NodeType::ConstantWriteNode
348
+ | NodeType::ConstantPathNode
349
+ | NodeType::RequiredParameterNode
350
+ | NodeType::OptionalParameterNode
351
+ | NodeType::RestParameterNode
352
+ | NodeType::KeywordParameterNode
353
+ | NodeType::KeywordRestParameterNode
354
+ | NodeType::BlockParameterNode
355
+ )
356
+ }
357
+ }
358
+
359
+ impl Default for Emitter {
360
+ fn default() -> Self {
361
+ Self::new(Config::default())
362
+ }
363
+ }
@@ -0,0 +1,48 @@
1
+ use magnus::{Error as MagnusError, Ruby};
2
+ use thiserror::Error;
3
+
4
+ pub type Result<T> = std::result::Result<T, RfmtError>;
5
+
6
+ #[derive(Error, Debug)]
7
+ pub enum RfmtError {
8
+ #[error("Prism integration error: {0}")]
9
+ PrismError(String),
10
+
11
+ #[error("Format error: {0}")]
12
+ FormatError(String),
13
+
14
+ #[error("Unsupported feature: {feature}\n{explanation}")]
15
+ UnsupportedFeature {
16
+ feature: String,
17
+ explanation: String,
18
+ },
19
+
20
+ #[error("Configuration error: {message}")]
21
+ #[cfg(test)]
22
+ ConfigError { message: String },
23
+ }
24
+
25
+ // Implement From for std::fmt::Error
26
+ impl From<std::fmt::Error> for RfmtError {
27
+ fn from(err: std::fmt::Error) -> Self {
28
+ RfmtError::FormatError(err.to_string())
29
+ }
30
+ }
31
+
32
+ impl RfmtError {
33
+ /// Convert RfmtError to Magnus Error for Ruby interop
34
+ pub fn to_magnus_error(&self, ruby: &Ruby) -> MagnusError {
35
+ let exception_class = match self {
36
+ RfmtError::PrismError(_) => "PrismError",
37
+ RfmtError::FormatError(_) => "FormatError",
38
+ RfmtError::UnsupportedFeature { .. } => "UnsupportedFeature",
39
+ #[cfg(test)]
40
+ RfmtError::ConfigError { .. } => "ConfigError",
41
+ };
42
+
43
+ MagnusError::new(
44
+ ruby.exception_standard_error(),
45
+ format!("[Rfmt::{}] {}", exception_class, self),
46
+ )
47
+ }
48
+ }
data/ext/rfmt/src/lib.rs CHANGED
@@ -1,48 +1,71 @@
1
+ mod ast;
2
+ mod config;
3
+ mod emitter;
4
+ mod error;
5
+ mod logging;
6
+ mod parser;
7
+ mod policy;
8
+
9
+ use policy::SecurityPolicy;
10
+
11
+ use config::Config;
12
+ use emitter::Emitter;
1
13
  use magnus::{define_module, function, prelude::*, Error, Ruby};
14
+ use parser::{PrismAdapter, RubyParser};
15
+
16
+ fn format_ruby_code(ruby: &Ruby, source: String, json: String) -> Result<String, Error> {
17
+ let policy = SecurityPolicy::default();
18
+
19
+ policy
20
+ .validate_source_size(&source)
21
+ .map_err(|e| e.to_magnus_error(ruby))?;
22
+
23
+ log::debug!("Source code validated, size: {} bytes", source.len());
24
+
25
+ let parser = PrismAdapter::new();
26
+ let ast = parser.parse(&json).map_err(|e| e.to_magnus_error(ruby))?;
27
+
28
+ let config = Config::default();
29
+ let mut emitter = Emitter::with_source(config, source);
30
+
31
+ let formatted = emitter.emit(&ast).map_err(|e| e.to_magnus_error(ruby))?;
2
32
 
3
- fn format_ruby_code(source: String) -> String {
4
- // indent with 2 spaces
5
- let lines: Vec<&str> = source.lines().collect();
6
- let mut result = Vec::new();
7
- let mut indent_level: i32 = 0;
8
-
9
- for line in lines {
10
- let trimmed = line.trim();
11
-
12
- if trimmed == "end" || trimmed.starts_with("end ")
13
- || trimmed == "}" || trimmed == "]" {
14
- indent_level = indent_level.saturating_sub(1);
15
- }
16
-
17
- // eval indent
18
- if !trimmed.is_empty() {
19
- result.push(format!("{}{}", " ".repeat(indent_level as usize), trimmed));
20
- } else {
21
- result.push(String::new());
22
- }
23
-
24
- // add indent lebel
25
- if trimmed.starts_with("def ") || trimmed.starts_with("class ")
26
- || trimmed.starts_with("module ") || trimmed.starts_with("if ")
27
- || trimmed.starts_with("unless ") || trimmed.starts_with("while ")
28
- || trimmed.starts_with("for ") || trimmed.starts_with("do ")
29
- || trimmed == "do" || trimmed.ends_with(" do")
30
- || trimmed == "{" || trimmed == "[" {
31
- indent_level += 1;
32
- }
33
- }
34
-
35
- result.join("\n")
33
+ Ok(formatted)
34
+ }
35
+
36
+ /// Parse Ruby source code and return JSON AST representation
37
+ /// This is useful for debugging and integration testing
38
+ fn parse_to_json(ruby: &Ruby, source: String) -> Result<String, Error> {
39
+ let parser = PrismAdapter::new();
40
+ let ast = parser.parse(&source).map_err(|e| e.to_magnus_error(ruby))?;
41
+
42
+ Ok(format!("{:#?}", ast))
36
43
  }
37
44
 
38
45
  fn rust_version() -> String {
39
- "0.1.0 (Rust)".to_string()
46
+ "0.2.0 (Rust)".to_string()
40
47
  }
41
48
 
42
49
  #[magnus::init]
43
- fn init(_ruby: &Ruby) -> Result<(), Error> {
50
+ fn init(ruby: &Ruby) -> Result<(), Error> {
51
+ logging::RfmtLogger::init();
52
+ log::info!("Initializing rfmt Rust extension");
53
+
44
54
  let module = define_module("Rfmt")?;
45
- module.define_singleton_method("format_code", function!(format_ruby_code, 1))?;
55
+
56
+ module.define_singleton_method("format_code", function!(format_ruby_code, 2))?;
57
+ module.define_singleton_method("parse_to_json", function!(parse_to_json, 1))?;
46
58
  module.define_singleton_method("rust_version", function!(rust_version, 0))?;
59
+
60
+ let rfmt_error = ruby.define_error("RfmtError", ruby.exception_standard_error())?;
61
+ ruby.define_error("ParseError", rfmt_error)?;
62
+ ruby.define_error("ConfigError", rfmt_error)?;
63
+ ruby.define_error("PrismError", rfmt_error)?;
64
+ ruby.define_error("RuleError", rfmt_error)?;
65
+ ruby.define_error("InternalError", rfmt_error)?;
66
+ ruby.define_error("FormattingError", rfmt_error)?;
67
+ ruby.define_error("UnsupportedFeature", rfmt_error)?;
68
+
69
+ log::info!("rfmt Rust extension initialized successfully");
47
70
  Ok(())
48
71
  }
@@ -0,0 +1,128 @@
1
+ use log::{LevelFilter, Log, Metadata, Record};
2
+ use std::io::Write;
3
+ use std::sync::Mutex;
4
+
5
+ pub struct RfmtLogger {
6
+ level: LevelFilter,
7
+ output: Mutex<Box<dyn Write + Send>>,
8
+ }
9
+
10
+ impl RfmtLogger {
11
+ pub fn new(level: LevelFilter) -> Self {
12
+ Self {
13
+ level,
14
+ output: Mutex::new(Box::new(std::io::stderr())),
15
+ }
16
+ }
17
+
18
+ #[cfg(test)]
19
+ pub fn with_output(mut self, output: Box<dyn Write + Send>) -> Self {
20
+ self.output = Mutex::new(output);
21
+ self
22
+ }
23
+
24
+ pub fn init() {
25
+ let logger = Self::new(LevelFilter::Info);
26
+ log::set_boxed_logger(Box::new(logger)).expect("Failed to initialize logger");
27
+ log::set_max_level(LevelFilter::Trace);
28
+ }
29
+ }
30
+
31
+ impl Log for RfmtLogger {
32
+ fn enabled(&self, metadata: &Metadata) -> bool {
33
+ metadata.level() <= self.level
34
+ }
35
+
36
+ fn log(&self, record: &Record) {
37
+ if !self.enabled(record.metadata()) {
38
+ return;
39
+ }
40
+
41
+ let mut output = self.output.lock().unwrap();
42
+
43
+ writeln!(
44
+ output,
45
+ "[{}] {} - {}",
46
+ record.level(),
47
+ record.target(),
48
+ record.args()
49
+ )
50
+ .ok();
51
+ }
52
+
53
+ fn flush(&self) {
54
+ let mut output = self.output.lock().unwrap();
55
+ output.flush().ok();
56
+ }
57
+ }
58
+
59
+ #[cfg(test)]
60
+ mod tests {
61
+ use super::*;
62
+ use log::{debug, error, info, trace, warn};
63
+ use std::sync::{Arc, Mutex};
64
+
65
+ struct TestWriter {
66
+ data: Arc<Mutex<Vec<u8>>>,
67
+ }
68
+
69
+ impl TestWriter {
70
+ fn new() -> (Self, Arc<Mutex<Vec<u8>>>) {
71
+ let data = Arc::new(Mutex::new(Vec::new()));
72
+ (
73
+ Self {
74
+ data: Arc::clone(&data),
75
+ },
76
+ data,
77
+ )
78
+ }
79
+ }
80
+
81
+ impl Write for TestWriter {
82
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
83
+ self.data.lock().unwrap().write(buf)
84
+ }
85
+
86
+ fn flush(&mut self) -> std::io::Result<()> {
87
+ self.data.lock().unwrap().flush()
88
+ }
89
+ }
90
+
91
+ #[test]
92
+ fn test_logger_creation() {
93
+ let logger = RfmtLogger::new(LevelFilter::Info);
94
+ assert!(logger.enabled(&Metadata::builder().level(log::Level::Info).build()));
95
+ assert!(!logger.enabled(&Metadata::builder().level(log::Level::Debug).build()));
96
+ }
97
+
98
+ #[test]
99
+ fn test_logger_level_filtering() {
100
+ let logger = RfmtLogger::new(LevelFilter::Warn);
101
+
102
+ assert!(logger.enabled(&Metadata::builder().level(log::Level::Error).build()));
103
+ assert!(logger.enabled(&Metadata::builder().level(log::Level::Warn).build()));
104
+ assert!(!logger.enabled(&Metadata::builder().level(log::Level::Info).build()));
105
+ assert!(!logger.enabled(&Metadata::builder().level(log::Level::Debug).build()));
106
+ assert!(!logger.enabled(&Metadata::builder().level(log::Level::Trace).build()));
107
+ }
108
+
109
+ #[test]
110
+ fn test_logger_output() {
111
+ let (writer, data) = TestWriter::new();
112
+ let logger = RfmtLogger::new(LevelFilter::Info).with_output(Box::new(writer));
113
+
114
+ let record = Record::builder()
115
+ .level(log::Level::Info)
116
+ .target("test")
117
+ .args(format_args!("test message"))
118
+ .build();
119
+
120
+ logger.log(&record);
121
+ logger.flush();
122
+
123
+ let output = String::from_utf8(data.lock().unwrap().clone()).unwrap();
124
+ assert!(output.contains("[INFO]"));
125
+ assert!(output.contains("test"));
126
+ assert!(output.contains("test message"));
127
+ }
128
+ }
@@ -0,0 +1,3 @@
1
+ pub mod logger;
2
+
3
+ pub use logger::RfmtLogger;
@@ -0,0 +1,9 @@
1
+ use crate::ast::Node;
2
+ use crate::error::Result;
3
+
4
+ pub mod prism_adapter;
5
+ pub use prism_adapter::PrismAdapter;
6
+
7
+ pub trait RubyParser: Send + Sync {
8
+ fn parse(&self, source: &str) -> Result<Node>;
9
+ }