method-ray 0.1.2 → 0.1.4

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.
data/rust/src/parser.rs CHANGED
@@ -1,46 +1,81 @@
1
1
  use anyhow::{Context, Result};
2
+ use bumpalo::Bump;
2
3
  use ruby_prism::{parse, ParseResult};
3
4
  use std::fs;
4
5
  use std::path::Path;
5
6
 
6
- /// Parse Ruby source code and return ruby-prism AST
7
+ /// Parse session - manages source bytes for multiple files using arena allocation
7
8
  ///
8
- /// Note: Uses Box::leak internally to ensure 'static lifetime
9
- pub fn parse_ruby_file(file_path: &Path) -> Result<ParseResult<'static>> {
10
- let source = fs::read_to_string(file_path)
11
- .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
12
-
13
- parse_ruby_source(&source, file_path.to_string_lossy().to_string())
9
+ /// Uses an arena allocator to efficiently manage source bytes during parsing.
10
+ /// When the session is dropped, all memory is released at once.
11
+ pub struct ParseSession {
12
+ arena: Bump,
14
13
  }
15
14
 
16
- /// Parse Ruby source code string
17
- pub fn parse_ruby_source(source: &str, file_name: String) -> Result<ParseResult<'static>> {
18
- // ruby-prism accepts &[u8]
19
- // Use Box::leak to ensure 'static lifetime (memory leak is acceptable for analysis tools)
20
- let source_bytes: &'static [u8] = Box::leak(source.as_bytes().to_vec().into_boxed_slice());
21
- let parse_result = parse(source_bytes);
22
-
23
- // Check parse errors
24
- let error_messages: Vec<String> = parse_result
25
- .errors()
26
- .map(|e| {
27
- format!(
28
- "Parse error at offset {}: {}",
29
- e.location().start_offset(),
30
- e.message()
31
- )
32
- })
33
- .collect();
34
-
35
- if !error_messages.is_empty() {
36
- anyhow::bail!(
37
- "Failed to parse Ruby source in {}:\n{}",
38
- file_name,
39
- error_messages.join("\n")
40
- );
15
+ impl ParseSession {
16
+ pub fn new() -> Self {
17
+ Self { arena: Bump::new() }
18
+ }
19
+
20
+ /// Create with pre-allocated capacity (recommended for batch file processing)
21
+ pub fn with_capacity(capacity: usize) -> Self {
22
+ Self {
23
+ arena: Bump::with_capacity(capacity),
24
+ }
25
+ }
26
+
27
+ /// Allocate source in arena and parse
28
+ pub fn parse_source<'a>(&'a self, source: &str, file_name: &str) -> Result<ParseResult<'a>> {
29
+ // Copy bytes to arena
30
+ let source_bytes = self.arena.alloc_slice_copy(source.as_bytes());
31
+ let parse_result = parse(source_bytes);
32
+
33
+ // Check for parse errors
34
+ let error_messages: Vec<String> = parse_result
35
+ .errors()
36
+ .map(|e| {
37
+ format!(
38
+ "Parse error at offset {}: {}",
39
+ e.location().start_offset(),
40
+ e.message()
41
+ )
42
+ })
43
+ .collect();
44
+
45
+ if !error_messages.is_empty() {
46
+ anyhow::bail!(
47
+ "Failed to parse Ruby source in {}:\n{}",
48
+ file_name,
49
+ error_messages.join("\n")
50
+ );
51
+ }
52
+
53
+ Ok(parse_result)
54
+ }
55
+
56
+ /// Read file and parse
57
+ pub fn parse_file<'a>(&'a self, file_path: &Path) -> Result<ParseResult<'a>> {
58
+ let source = fs::read_to_string(file_path)
59
+ .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
60
+
61
+ self.parse_source(&source, &file_path.to_string_lossy())
41
62
  }
42
63
 
43
- Ok(parse_result)
64
+ /// Get allocated memory size (for debugging)
65
+ pub fn allocated_bytes(&self) -> usize {
66
+ self.arena.allocated_bytes()
67
+ }
68
+
69
+ /// Reset arena (for memory control during batch file processing)
70
+ pub fn reset(&mut self) {
71
+ self.arena.reset();
72
+ }
73
+ }
74
+
75
+ impl Default for ParseSession {
76
+ fn default() -> Self {
77
+ Self::new()
78
+ }
44
79
  }
45
80
 
46
81
  #[cfg(test)]
@@ -51,21 +86,24 @@ mod tests {
51
86
  fn test_parse_simple_ruby() {
52
87
  let source = r#"x = 1
53
88
  puts x"#;
54
- let result = parse_ruby_source(source, "test.rb".to_string());
89
+ let session = ParseSession::new();
90
+ let result = session.parse_source(source, "test.rb");
55
91
  assert!(result.is_ok());
56
92
  }
57
93
 
58
94
  #[test]
59
95
  fn test_parse_string_literal() {
60
96
  let source = r#""hello".upcase"#;
61
- let result = parse_ruby_source(source, "test.rb".to_string());
97
+ let session = ParseSession::new();
98
+ let result = session.parse_source(source, "test.rb");
62
99
  assert!(result.is_ok());
63
100
  }
64
101
 
65
102
  #[test]
66
103
  fn test_parse_array_literal() {
67
104
  let source = r#"[1, 2, 3].map { |x| x * 2 }"#;
68
- let result = parse_ruby_source(source, "test.rb".to_string());
105
+ let session = ParseSession::new();
106
+ let result = session.parse_source(source, "test.rb");
69
107
  assert!(result.is_ok());
70
108
  }
71
109
 
@@ -75,14 +113,16 @@ puts x"#;
75
113
  x = "hello"
76
114
  x.upcase
77
115
  end"#;
78
- let result = parse_ruby_source(source, "test.rb".to_string());
116
+ let session = ParseSession::new();
117
+ let result = session.parse_source(source, "test.rb");
79
118
  assert!(result.is_ok());
80
119
  }
81
120
 
82
121
  #[test]
83
122
  fn test_parse_invalid_ruby() {
84
123
  let source = "def\nend end";
85
- let result = parse_ruby_source(source, "test.rb".to_string());
124
+ let session = ParseSession::new();
125
+ let result = session.parse_source(source, "test.rb");
86
126
  assert!(result.is_err());
87
127
  }
88
128
 
@@ -90,7 +130,27 @@ end"#;
90
130
  fn test_parse_method_call() {
91
131
  let source = r#"user = User.new
92
132
  user.save"#;
93
- let result = parse_ruby_source(source, "test.rb".to_string());
133
+ let session = ParseSession::new();
134
+ let result = session.parse_source(source, "test.rb");
94
135
  assert!(result.is_ok());
95
136
  }
137
+
138
+ #[test]
139
+ fn test_parse_session_memory_tracking() {
140
+ let session = ParseSession::new();
141
+ let source = "x = 1";
142
+ let _ = session.parse_source(source, "test.rb").unwrap();
143
+ assert!(session.allocated_bytes() > 0);
144
+ }
145
+
146
+ #[test]
147
+ fn test_parse_session_reset() {
148
+ let mut session = ParseSession::new();
149
+ let source = "x = 1";
150
+ let _ = session.parse_source(source, "test.rb").unwrap();
151
+ let before_reset = session.allocated_bytes();
152
+ session.reset();
153
+ // After reset, allocated_bytes may still report used chunks but internal data is cleared
154
+ assert!(before_reset > 0);
155
+ }
96
156
  }
@@ -20,18 +20,12 @@ impl RbsTypeConverter {
20
20
 
21
21
  match type_name {
22
22
  "bool" => Type::Union(vec![
23
- Type::Instance {
24
- class_name: "TrueClass".to_string(),
25
- },
26
- Type::Instance {
27
- class_name: "FalseClass".to_string(),
28
- },
23
+ Type::instance("TrueClass"),
24
+ Type::instance("FalseClass"),
29
25
  ]),
30
26
  "void" | "nil" => Type::Nil,
31
27
  "untyped" | "top" => Type::Bot,
32
- _ => Type::Instance {
33
- class_name: type_name.to_string(),
34
- },
28
+ _ => Type::instance(type_name),
35
29
  }
36
30
  }
37
31
  }
@@ -43,12 +37,23 @@ mod tests {
43
37
  #[test]
44
38
  fn test_parse_simple_types() {
45
39
  match RbsTypeConverter::parse("::String") {
46
- Type::Instance { class_name } => assert_eq!(class_name, "String"),
40
+ Type::Instance { name } => assert_eq!(name.full_name(), "String"),
47
41
  _ => panic!("Expected Instance type"),
48
42
  }
49
43
 
50
44
  match RbsTypeConverter::parse("Integer") {
51
- Type::Instance { class_name } => assert_eq!(class_name, "Integer"),
45
+ Type::Instance { name } => assert_eq!(name.full_name(), "Integer"),
46
+ _ => panic!("Expected Instance type"),
47
+ }
48
+ }
49
+
50
+ #[test]
51
+ fn test_parse_qualified_types() {
52
+ match RbsTypeConverter::parse("::Api::User") {
53
+ Type::Instance { name } => {
54
+ assert_eq!(name.full_name(), "Api::User");
55
+ assert_eq!(name.name(), "User");
56
+ }
52
57
  _ => panic!("Expected Instance type"),
53
58
  }
54
59
  }
@@ -2,6 +2,7 @@ use crate::env::GlobalEnv;
2
2
  use crate::rbs::converter::RbsTypeConverter;
3
3
  use crate::rbs::error::RbsError;
4
4
  use crate::types::Type;
5
+ use magnus::value::ReprValue;
5
6
  use magnus::{Error, RArray, RHash, Ruby, TryConvert, Value};
6
7
 
7
8
  /// Method information loaded from RBS
@@ -10,6 +11,7 @@ pub struct RbsMethodInfo {
10
11
  pub receiver_class: String,
11
12
  pub method_name: String,
12
13
  pub return_type: Type,
14
+ pub block_param_types: Option<Vec<String>>,
13
15
  }
14
16
 
15
17
  /// Loader that calls RBS API via magnus to load method information
@@ -100,10 +102,33 @@ impl<'a> RbsLoader<'a> {
100
102
  // Convert RBS type string to internal Type enum
101
103
  let return_type = RbsTypeConverter::parse(&return_type_str);
102
104
 
105
+ // Parse block_param_types (optional)
106
+ let block_param_types: Option<Vec<String>> =
107
+ if let Some(bpt_value) = hash.get(self.ruby.to_symbol("block_param_types")) {
108
+ if bpt_value.is_nil() {
109
+ None
110
+ } else if let Ok(bpt_array) = RArray::try_convert(bpt_value) {
111
+ let types: Vec<String> = bpt_array
112
+ .into_iter()
113
+ .filter_map(|v| String::try_convert(v).ok())
114
+ .collect();
115
+ if types.is_empty() {
116
+ None
117
+ } else {
118
+ Some(types)
119
+ }
120
+ } else {
121
+ None
122
+ }
123
+ } else {
124
+ None
125
+ };
126
+
103
127
  method_infos.push(RbsMethodInfo {
104
128
  receiver_class,
105
129
  method_name,
106
130
  return_type,
131
+ block_param_types,
107
132
  });
108
133
  }
109
134
 
@@ -128,7 +153,6 @@ pub fn register_rbs_methods(genv: &mut GlobalEnv, ruby: &Ruby) -> Result<usize,
128
153
  // Try to load from cache
129
154
  let methods = if let Ok(cache) = RbsCache::load() {
130
155
  if cache.is_valid(methodray_version, &rbs_version) {
131
- eprintln!("Loaded {} methods from cache", cache.methods.len());
132
156
  cache.to_method_infos()
133
157
  } else {
134
158
  eprintln!("Cache invalid, reloading from RBS...");
@@ -142,13 +166,19 @@ pub fn register_rbs_methods(genv: &mut GlobalEnv, ruby: &Ruby) -> Result<usize,
142
166
 
143
167
  let count = methods.len();
144
168
  for method_info in methods {
145
- let receiver_type = Type::Instance {
146
- class_name: method_info.receiver_class,
147
- };
148
- genv.register_builtin_method(
169
+ let receiver_type = Type::instance(&method_info.receiver_class);
170
+ // Convert block param type strings to Type enums
171
+ let block_param_types = method_info.block_param_types.map(|types| {
172
+ types
173
+ .iter()
174
+ .map(|s| RbsTypeConverter::parse(s))
175
+ .collect()
176
+ });
177
+ genv.register_builtin_method_with_block(
149
178
  receiver_type,
150
179
  &method_info.method_name,
151
180
  method_info.return_type,
181
+ block_param_types,
152
182
  );
153
183
  }
154
184
 
data/rust/src/rbs/mod.rs CHANGED
@@ -1,14 +1,15 @@
1
1
  //! RBS type loading and conversion
2
2
 
3
- #[cfg(feature = "ruby-ffi")]
3
+ // Converter is always available (no Ruby FFI dependency)
4
4
  pub mod converter;
5
+ pub use converter::RbsTypeConverter;
6
+
7
+ // These require Ruby FFI for RBS loading
5
8
  #[cfg(feature = "ruby-ffi")]
6
9
  pub mod error;
7
10
  #[cfg(feature = "ruby-ffi")]
8
11
  pub mod loader;
9
12
 
10
- #[cfg(feature = "ruby-ffi")]
11
- pub use converter::RbsTypeConverter;
12
13
  #[cfg(feature = "ruby-ffi")]
13
14
  pub use error::RbsError;
14
15
  #[cfg(feature = "ruby-ffi")]