method-ray 0.1.1
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 +23 -0
- data/LICENSE +21 -0
- data/README.md +39 -0
- data/exe/methodray +7 -0
- data/ext/Cargo.toml +24 -0
- data/ext/extconf.rb +40 -0
- data/ext/src/cli.rs +33 -0
- data/ext/src/lib.rs +79 -0
- data/lib/methodray/cli.rb +28 -0
- data/lib/methodray/commands.rb +78 -0
- data/lib/methodray/version.rb +5 -0
- data/lib/methodray.rb +9 -0
- data/rust/Cargo.toml +39 -0
- data/rust/src/analyzer/calls.rs +56 -0
- data/rust/src/analyzer/definitions.rs +70 -0
- data/rust/src/analyzer/dispatch.rs +134 -0
- data/rust/src/analyzer/install.rs +226 -0
- data/rust/src/analyzer/literals.rs +85 -0
- data/rust/src/analyzer/mod.rs +11 -0
- data/rust/src/analyzer/tests/integration_test.rs +136 -0
- data/rust/src/analyzer/tests/mod.rs +1 -0
- data/rust/src/analyzer/variables.rs +76 -0
- data/rust/src/cache/mod.rs +3 -0
- data/rust/src/cache/rbs_cache.rs +158 -0
- data/rust/src/checker.rs +139 -0
- data/rust/src/cli/args.rs +40 -0
- data/rust/src/cli/commands.rs +139 -0
- data/rust/src/cli/mod.rs +6 -0
- data/rust/src/diagnostics/diagnostic.rs +125 -0
- data/rust/src/diagnostics/formatter.rs +119 -0
- data/rust/src/diagnostics/mod.rs +5 -0
- data/rust/src/env/box_manager.rs +121 -0
- data/rust/src/env/global_env.rs +279 -0
- data/rust/src/env/local_env.rs +58 -0
- data/rust/src/env/method_registry.rs +63 -0
- data/rust/src/env/mod.rs +15 -0
- data/rust/src/env/scope.rs +330 -0
- data/rust/src/env/type_error.rs +23 -0
- data/rust/src/env/vertex_manager.rs +195 -0
- data/rust/src/graph/box.rs +157 -0
- data/rust/src/graph/change_set.rs +115 -0
- data/rust/src/graph/mod.rs +7 -0
- data/rust/src/graph/vertex.rs +167 -0
- data/rust/src/lib.rs +24 -0
- data/rust/src/lsp/diagnostics.rs +133 -0
- data/rust/src/lsp/main.rs +8 -0
- data/rust/src/lsp/mod.rs +4 -0
- data/rust/src/lsp/server.rs +138 -0
- data/rust/src/main.rs +46 -0
- data/rust/src/parser.rs +96 -0
- data/rust/src/rbs/converter.rs +82 -0
- data/rust/src/rbs/error.rs +37 -0
- data/rust/src/rbs/loader.rs +183 -0
- data/rust/src/rbs/mod.rs +15 -0
- data/rust/src/source_map.rs +102 -0
- data/rust/src/types.rs +75 -0
- metadata +119 -0
data/rust/src/parser.rs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
use anyhow::{Context, Result};
|
|
2
|
+
use ruby_prism::{parse, ParseResult};
|
|
3
|
+
use std::fs;
|
|
4
|
+
use std::path::Path;
|
|
5
|
+
|
|
6
|
+
/// Parse Ruby source code and return ruby-prism AST
|
|
7
|
+
///
|
|
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())
|
|
14
|
+
}
|
|
15
|
+
|
|
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
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Ok(parse_result)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[cfg(test)]
|
|
47
|
+
mod tests {
|
|
48
|
+
use super::*;
|
|
49
|
+
|
|
50
|
+
#[test]
|
|
51
|
+
fn test_parse_simple_ruby() {
|
|
52
|
+
let source = r#"x = 1
|
|
53
|
+
puts x"#;
|
|
54
|
+
let result = parse_ruby_source(source, "test.rb".to_string());
|
|
55
|
+
assert!(result.is_ok());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[test]
|
|
59
|
+
fn test_parse_string_literal() {
|
|
60
|
+
let source = r#""hello".upcase"#;
|
|
61
|
+
let result = parse_ruby_source(source, "test.rb".to_string());
|
|
62
|
+
assert!(result.is_ok());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn test_parse_array_literal() {
|
|
67
|
+
let source = r#"[1, 2, 3].map { |x| x * 2 }"#;
|
|
68
|
+
let result = parse_ruby_source(source, "test.rb".to_string());
|
|
69
|
+
assert!(result.is_ok());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn test_parse_method_definition() {
|
|
74
|
+
let source = r#"def test_method
|
|
75
|
+
x = "hello"
|
|
76
|
+
x.upcase
|
|
77
|
+
end"#;
|
|
78
|
+
let result = parse_ruby_source(source, "test.rb".to_string());
|
|
79
|
+
assert!(result.is_ok());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn test_parse_invalid_ruby() {
|
|
84
|
+
let source = "def\nend end";
|
|
85
|
+
let result = parse_ruby_source(source, "test.rb".to_string());
|
|
86
|
+
assert!(result.is_err());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn test_parse_method_call() {
|
|
91
|
+
let source = r#"user = User.new
|
|
92
|
+
user.save"#;
|
|
93
|
+
let result = parse_ruby_source(source, "test.rb".to_string());
|
|
94
|
+
assert!(result.is_ok());
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
use crate::types::Type;
|
|
2
|
+
|
|
3
|
+
// RBS Type Converter
|
|
4
|
+
pub struct RbsTypeConverter;
|
|
5
|
+
|
|
6
|
+
impl RbsTypeConverter {
|
|
7
|
+
pub fn parse(rbs_type: &str) -> Type {
|
|
8
|
+
// Handle union types
|
|
9
|
+
if rbs_type.contains(" | ") {
|
|
10
|
+
let parts: Vec<&str> = rbs_type.split(" | ").collect();
|
|
11
|
+
let types: Vec<Type> = parts.iter().map(|s| Self::parse_single(s.trim())).collect();
|
|
12
|
+
return Type::Union(types);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
Self::parse_single(rbs_type)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fn parse_single(rbs_type: &str) -> Type {
|
|
19
|
+
let type_name = rbs_type.trim_start_matches("::");
|
|
20
|
+
|
|
21
|
+
match type_name {
|
|
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
|
+
},
|
|
29
|
+
]),
|
|
30
|
+
"void" | "nil" => Type::Nil,
|
|
31
|
+
"untyped" | "top" => Type::Bot,
|
|
32
|
+
_ => Type::Instance {
|
|
33
|
+
class_name: type_name.to_string(),
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[cfg(test)]
|
|
40
|
+
mod tests {
|
|
41
|
+
use super::*;
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn test_parse_simple_types() {
|
|
45
|
+
match RbsTypeConverter::parse("::String") {
|
|
46
|
+
Type::Instance { class_name } => assert_eq!(class_name, "String"),
|
|
47
|
+
_ => panic!("Expected Instance type"),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
match RbsTypeConverter::parse("Integer") {
|
|
51
|
+
Type::Instance { class_name } => assert_eq!(class_name, "Integer"),
|
|
52
|
+
_ => panic!("Expected Instance type"),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[test]
|
|
57
|
+
fn test_parse_special_types() {
|
|
58
|
+
assert!(matches!(RbsTypeConverter::parse("nil"), Type::Nil));
|
|
59
|
+
assert!(matches!(RbsTypeConverter::parse("void"), Type::Nil));
|
|
60
|
+
assert!(matches!(RbsTypeConverter::parse("untyped"), Type::Bot));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[test]
|
|
64
|
+
fn test_parse_bool() {
|
|
65
|
+
match RbsTypeConverter::parse("bool") {
|
|
66
|
+
Type::Union(types) => {
|
|
67
|
+
assert_eq!(types.len(), 2);
|
|
68
|
+
}
|
|
69
|
+
_ => panic!("Expected Union type for bool"),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[test]
|
|
74
|
+
fn test_parse_union_types() {
|
|
75
|
+
match RbsTypeConverter::parse("String | Integer") {
|
|
76
|
+
Type::Union(types) => {
|
|
77
|
+
assert_eq!(types.len(), 2);
|
|
78
|
+
}
|
|
79
|
+
_ => panic!("Expected Union type"),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
use std::fmt;
|
|
2
|
+
|
|
3
|
+
#[derive(Debug)]
|
|
4
|
+
pub enum RbsError {
|
|
5
|
+
RbsNotInstalled,
|
|
6
|
+
LoadError(String),
|
|
7
|
+
ParseError(String),
|
|
8
|
+
MagnusError(magnus::Error),
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
impl fmt::Display for RbsError {
|
|
12
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
13
|
+
match self {
|
|
14
|
+
RbsError::RbsNotInstalled => {
|
|
15
|
+
write!(f, "RBS gem not found. Please install: gem install rbs")
|
|
16
|
+
}
|
|
17
|
+
RbsError::LoadError(msg) => write!(f, "Failed to load RBS environment: {}", msg),
|
|
18
|
+
RbsError::ParseError(msg) => write!(f, "Failed to parse RBS type: {}", msg),
|
|
19
|
+
RbsError::MagnusError(e) => write!(f, "Magnus error: {}", e),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl std::error::Error for RbsError {}
|
|
25
|
+
|
|
26
|
+
impl From<magnus::Error> for RbsError {
|
|
27
|
+
fn from(err: magnus::Error) -> Self {
|
|
28
|
+
RbsError::MagnusError(err)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl From<RbsError> for magnus::Error {
|
|
33
|
+
fn from(err: RbsError) -> Self {
|
|
34
|
+
let ruby = unsafe { magnus::Ruby::get_unchecked() };
|
|
35
|
+
magnus::Error::new(ruby.exception_runtime_error(), err.to_string())
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
use crate::env::GlobalEnv;
|
|
2
|
+
use crate::rbs::converter::RbsTypeConverter;
|
|
3
|
+
use crate::rbs::error::RbsError;
|
|
4
|
+
use crate::types::Type;
|
|
5
|
+
use magnus::{Error, RArray, RHash, Ruby, TryConvert, Value};
|
|
6
|
+
|
|
7
|
+
/// Method information loaded from RBS
|
|
8
|
+
#[derive(Debug, Clone)]
|
|
9
|
+
pub struct RbsMethodInfo {
|
|
10
|
+
pub receiver_class: String,
|
|
11
|
+
pub method_name: String,
|
|
12
|
+
pub return_type: Type,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// Loader that calls RBS API via magnus to load method information
|
|
16
|
+
pub struct RbsLoader<'a> {
|
|
17
|
+
ruby: &'a Ruby,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
impl<'a> RbsLoader<'a> {
|
|
21
|
+
/// Create a new RbsLoader
|
|
22
|
+
/// Assumes RBS gem is already loaded on the Ruby side
|
|
23
|
+
pub fn new(ruby: &'a Ruby) -> Result<Self, RbsError> {
|
|
24
|
+
// Check if RBS module is defined
|
|
25
|
+
let rbs_defined: bool = ruby
|
|
26
|
+
.eval("defined?(RBS) == 'constant'")
|
|
27
|
+
.map_err(|e| RbsError::LoadError(format!("Failed to check RBS: {}", e)))?;
|
|
28
|
+
|
|
29
|
+
if !rbs_defined {
|
|
30
|
+
return Err(RbsError::RbsNotInstalled);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Ok(Self { ruby })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Load all method definitions from RBS
|
|
37
|
+
pub fn load_methods(&self) -> Result<Vec<RbsMethodInfo>, RbsError> {
|
|
38
|
+
// Load method_loader.rb
|
|
39
|
+
let rb_path = concat!(env!("CARGO_MANIFEST_DIR"), "/src/rbs/method_loader.rb");
|
|
40
|
+
let load_code = format!("require '{}'", rb_path);
|
|
41
|
+
let _: Value = self
|
|
42
|
+
.ruby
|
|
43
|
+
.eval(&load_code)
|
|
44
|
+
.map_err(|e| RbsError::LoadError(format!("Failed to load method_loader.rb: {}", e)))?;
|
|
45
|
+
|
|
46
|
+
// Instantiate Rbs::MethodLoader class and call method
|
|
47
|
+
let results: Value = self
|
|
48
|
+
.ruby
|
|
49
|
+
.eval("Rbs::MethodLoader.new.load_methods")
|
|
50
|
+
.map_err(|e| {
|
|
51
|
+
RbsError::LoadError(format!(
|
|
52
|
+
"Failed to call Rbs::MethodLoader#load_methods: {}",
|
|
53
|
+
e
|
|
54
|
+
))
|
|
55
|
+
})?;
|
|
56
|
+
|
|
57
|
+
self.parse_results(results)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Convert Ruby array results to Vec of RbsMethodInfo structs
|
|
61
|
+
fn parse_results(&self, results: Value) -> Result<Vec<RbsMethodInfo>, RbsError> {
|
|
62
|
+
let mut method_infos = Vec::new();
|
|
63
|
+
|
|
64
|
+
// Convert to Ruby array
|
|
65
|
+
let results_array = RArray::try_convert(results)
|
|
66
|
+
.map_err(|e| RbsError::ParseError(format!("Failed to convert to array: {}", e)))?;
|
|
67
|
+
|
|
68
|
+
// Process each element
|
|
69
|
+
for entry in results_array.into_iter() {
|
|
70
|
+
let entry: Value = entry;
|
|
71
|
+
|
|
72
|
+
// Convert to hash
|
|
73
|
+
let hash = RHash::try_convert(entry).map_err(|e| {
|
|
74
|
+
RbsError::ParseError(format!("Failed to convert entry to hash: {}", e))
|
|
75
|
+
})?;
|
|
76
|
+
|
|
77
|
+
// Get each field
|
|
78
|
+
let receiver_class_value = hash
|
|
79
|
+
.get(self.ruby.to_symbol("receiver_class"))
|
|
80
|
+
.ok_or_else(|| RbsError::ParseError("Missing receiver_class".to_string()))?;
|
|
81
|
+
let receiver_class: String =
|
|
82
|
+
String::try_convert(receiver_class_value).map_err(|e| {
|
|
83
|
+
RbsError::ParseError(format!("Failed to convert receiver_class: {}", e))
|
|
84
|
+
})?;
|
|
85
|
+
|
|
86
|
+
let method_name_value = hash
|
|
87
|
+
.get(self.ruby.to_symbol("method_name"))
|
|
88
|
+
.ok_or_else(|| RbsError::ParseError("Missing method_name".to_string()))?;
|
|
89
|
+
let method_name: String = String::try_convert(method_name_value).map_err(|e| {
|
|
90
|
+
RbsError::ParseError(format!("Failed to convert method_name: {}", e))
|
|
91
|
+
})?;
|
|
92
|
+
|
|
93
|
+
let return_type_value = hash
|
|
94
|
+
.get(self.ruby.to_symbol("return_type"))
|
|
95
|
+
.ok_or_else(|| RbsError::ParseError("Missing return_type".to_string()))?;
|
|
96
|
+
let return_type_str: String = String::try_convert(return_type_value).map_err(|e| {
|
|
97
|
+
RbsError::ParseError(format!("Failed to convert return_type: {}", e))
|
|
98
|
+
})?;
|
|
99
|
+
|
|
100
|
+
// Convert RBS type string to internal Type enum
|
|
101
|
+
let return_type = RbsTypeConverter::parse(&return_type_str);
|
|
102
|
+
|
|
103
|
+
method_infos.push(RbsMethodInfo {
|
|
104
|
+
receiver_class,
|
|
105
|
+
method_name,
|
|
106
|
+
return_type,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Ok(method_infos)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Helper function to register RBS methods to GlobalEnv
|
|
115
|
+
/// Uses cache to avoid slow Ruby FFI calls
|
|
116
|
+
pub fn register_rbs_methods(genv: &mut GlobalEnv, ruby: &Ruby) -> Result<usize, Error> {
|
|
117
|
+
use crate::cache::RbsCache;
|
|
118
|
+
|
|
119
|
+
let methodray_version = env!("CARGO_PKG_VERSION");
|
|
120
|
+
|
|
121
|
+
// Try to get RBS version
|
|
122
|
+
let rbs_version_value: Value = ruby
|
|
123
|
+
.eval("RBS::VERSION")
|
|
124
|
+
.unwrap_or_else(|_| ruby.eval("'unknown'").unwrap());
|
|
125
|
+
let rbs_version: String =
|
|
126
|
+
String::try_convert(rbs_version_value).unwrap_or_else(|_| "unknown".to_string());
|
|
127
|
+
|
|
128
|
+
// Try to load from cache
|
|
129
|
+
let methods = if let Ok(cache) = RbsCache::load() {
|
|
130
|
+
if cache.is_valid(methodray_version, &rbs_version) {
|
|
131
|
+
eprintln!("Loaded {} methods from cache", cache.methods.len());
|
|
132
|
+
cache.to_method_infos()
|
|
133
|
+
} else {
|
|
134
|
+
eprintln!("Cache invalid, reloading from RBS...");
|
|
135
|
+
let methods = load_and_cache_rbs_methods(ruby, methodray_version, &rbs_version)?;
|
|
136
|
+
methods
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
eprintln!("No cache found, loading from RBS...");
|
|
140
|
+
load_and_cache_rbs_methods(ruby, methodray_version, &rbs_version)?
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
let count = methods.len();
|
|
144
|
+
for method_info in methods {
|
|
145
|
+
let receiver_type = Type::Instance {
|
|
146
|
+
class_name: method_info.receiver_class,
|
|
147
|
+
};
|
|
148
|
+
genv.register_builtin_method(
|
|
149
|
+
receiver_type,
|
|
150
|
+
&method_info.method_name,
|
|
151
|
+
method_info.return_type,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Ok(count)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Load RBS methods and save to cache
|
|
159
|
+
fn load_and_cache_rbs_methods(
|
|
160
|
+
ruby: &Ruby,
|
|
161
|
+
version: &str,
|
|
162
|
+
rbs_version: &str,
|
|
163
|
+
) -> Result<Vec<RbsMethodInfo>, Error> {
|
|
164
|
+
use crate::cache::RbsCache;
|
|
165
|
+
|
|
166
|
+
let loader = RbsLoader::new(ruby)?;
|
|
167
|
+
let methods = loader.load_methods()?;
|
|
168
|
+
|
|
169
|
+
// Save to cache
|
|
170
|
+
let cache = RbsCache::from_method_infos(
|
|
171
|
+
methods.clone(),
|
|
172
|
+
version.to_string(),
|
|
173
|
+
rbs_version.to_string(),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if let Err(e) = cache.save() {
|
|
177
|
+
eprintln!("Warning: Failed to save RBS cache: {}", e);
|
|
178
|
+
} else {
|
|
179
|
+
eprintln!("Saved {} methods to cache", methods.len());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
Ok(methods)
|
|
183
|
+
}
|
data/rust/src/rbs/mod.rs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//! RBS type loading and conversion
|
|
2
|
+
|
|
3
|
+
#[cfg(feature = "ruby-ffi")]
|
|
4
|
+
pub mod converter;
|
|
5
|
+
#[cfg(feature = "ruby-ffi")]
|
|
6
|
+
pub mod error;
|
|
7
|
+
#[cfg(feature = "ruby-ffi")]
|
|
8
|
+
pub mod loader;
|
|
9
|
+
|
|
10
|
+
#[cfg(feature = "ruby-ffi")]
|
|
11
|
+
pub use converter::RbsTypeConverter;
|
|
12
|
+
#[cfg(feature = "ruby-ffi")]
|
|
13
|
+
pub use error::RbsError;
|
|
14
|
+
#[cfg(feature = "ruby-ffi")]
|
|
15
|
+
pub use loader::{register_rbs_methods, RbsLoader, RbsMethodInfo};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/// Convert byte offset to (line, column) - 1-indexed
|
|
2
|
+
fn offset_to_line_column(source: &str, offset: usize) -> (usize, usize) {
|
|
3
|
+
let mut line = 1;
|
|
4
|
+
let mut column = 1;
|
|
5
|
+
let mut current_offset = 0;
|
|
6
|
+
|
|
7
|
+
for ch in source.chars() {
|
|
8
|
+
if current_offset >= offset {
|
|
9
|
+
break;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if ch == '\n' {
|
|
13
|
+
line += 1;
|
|
14
|
+
column = 1;
|
|
15
|
+
} else {
|
|
16
|
+
column += 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
current_offset += ch.len_utf8();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
(line, column)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Source code location information
|
|
26
|
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
27
|
+
pub struct SourceLocation {
|
|
28
|
+
pub line: usize,
|
|
29
|
+
pub column: usize,
|
|
30
|
+
pub length: usize,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[allow(dead_code)]
|
|
34
|
+
impl SourceLocation {
|
|
35
|
+
pub fn new(line: usize, column: usize, length: usize) -> Self {
|
|
36
|
+
Self {
|
|
37
|
+
line,
|
|
38
|
+
column,
|
|
39
|
+
length,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Create from ruby-prism Location and source code
|
|
44
|
+
/// Calculates line/column from byte offset
|
|
45
|
+
pub fn from_prism_location_with_source(location: &ruby_prism::Location, source: &str) -> Self {
|
|
46
|
+
let start_offset = location.start_offset();
|
|
47
|
+
let length = location.end_offset() - start_offset;
|
|
48
|
+
|
|
49
|
+
// Calculate line and column from byte offset
|
|
50
|
+
let (line, column) = offset_to_line_column(source, start_offset);
|
|
51
|
+
|
|
52
|
+
Self {
|
|
53
|
+
line,
|
|
54
|
+
column,
|
|
55
|
+
length,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Create from ruby-prism Location (without source - uses approximation)
|
|
60
|
+
pub fn from_prism_location(location: &ruby_prism::Location) -> Self {
|
|
61
|
+
let start_offset = location.start_offset();
|
|
62
|
+
let length = location.end_offset() - start_offset;
|
|
63
|
+
|
|
64
|
+
// Without source, we can't calculate exact line/column
|
|
65
|
+
// Use offset as column for now
|
|
66
|
+
Self {
|
|
67
|
+
line: 1, // Placeholder
|
|
68
|
+
column: start_offset + 1,
|
|
69
|
+
length,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#[cfg(test)]
|
|
75
|
+
mod tests {
|
|
76
|
+
use super::*;
|
|
77
|
+
|
|
78
|
+
#[test]
|
|
79
|
+
fn test_source_location_creation() {
|
|
80
|
+
let loc = SourceLocation::new(10, 5, 6);
|
|
81
|
+
assert_eq!(loc.line, 10);
|
|
82
|
+
assert_eq!(loc.column, 5);
|
|
83
|
+
assert_eq!(loc.length, 6);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn test_offset_to_line_column() {
|
|
88
|
+
let source = "x = 1\ny = x.upcase";
|
|
89
|
+
// "x = 1\n" is 6 bytes (0-5)
|
|
90
|
+
// "y = x.upcase" starts at offset 6
|
|
91
|
+
// "y = x." is 6 bytes, so ".upcase" starts at offset 12
|
|
92
|
+
|
|
93
|
+
// Test offset 0 (start of line 1)
|
|
94
|
+
assert_eq!(offset_to_line_column(source, 0), (1, 1));
|
|
95
|
+
|
|
96
|
+
// Test offset 6 (start of line 2, after newline)
|
|
97
|
+
assert_eq!(offset_to_line_column(source, 6), (2, 1));
|
|
98
|
+
|
|
99
|
+
// Test offset 10 (the 'x' in 'x.upcase')
|
|
100
|
+
assert_eq!(offset_to_line_column(source, 10), (2, 5));
|
|
101
|
+
}
|
|
102
|
+
}
|
data/rust/src/types.rs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/// Type system for graph-based type inference
|
|
2
|
+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
3
|
+
#[allow(dead_code)]
|
|
4
|
+
pub enum Type {
|
|
5
|
+
/// Instance type: String, Integer, etc.
|
|
6
|
+
Instance { class_name: String },
|
|
7
|
+
/// Singleton type: for class methods
|
|
8
|
+
Singleton { class_name: String },
|
|
9
|
+
/// nil type
|
|
10
|
+
Nil,
|
|
11
|
+
/// Union type: sum of multiple types
|
|
12
|
+
Union(Vec<Type>),
|
|
13
|
+
/// Bottom type: no type information
|
|
14
|
+
Bot,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl Type {
|
|
18
|
+
/// Convert type to string representation
|
|
19
|
+
pub fn show(&self) -> String {
|
|
20
|
+
match self {
|
|
21
|
+
Type::Instance { class_name } => class_name.clone(),
|
|
22
|
+
Type::Singleton { class_name } => format!("singleton({})", class_name),
|
|
23
|
+
Type::Nil => "nil".to_string(),
|
|
24
|
+
Type::Union(types) => {
|
|
25
|
+
let names: Vec<_> = types.iter().map(|t| t.show()).collect();
|
|
26
|
+
names.join(" | ")
|
|
27
|
+
}
|
|
28
|
+
Type::Bot => "untyped".to_string(),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Convenience constructors
|
|
33
|
+
pub fn string() -> Self {
|
|
34
|
+
Type::Instance {
|
|
35
|
+
class_name: "String".to_string(),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pub fn integer() -> Self {
|
|
40
|
+
Type::Instance {
|
|
41
|
+
class_name: "Integer".to_string(),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub fn array() -> Self {
|
|
46
|
+
Type::Instance {
|
|
47
|
+
class_name: "Array".to_string(),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pub fn hash() -> Self {
|
|
52
|
+
Type::Instance {
|
|
53
|
+
class_name: "Hash".to_string(),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[cfg(test)]
|
|
59
|
+
mod tests {
|
|
60
|
+
use super::*;
|
|
61
|
+
|
|
62
|
+
#[test]
|
|
63
|
+
fn test_type_show() {
|
|
64
|
+
assert_eq!(Type::string().show(), "String");
|
|
65
|
+
assert_eq!(Type::integer().show(), "Integer");
|
|
66
|
+
assert_eq!(Type::Nil.show(), "nil");
|
|
67
|
+
assert_eq!(Type::Bot.show(), "untyped");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[test]
|
|
71
|
+
fn test_type_union() {
|
|
72
|
+
let union = Type::Union(vec![Type::string(), Type::integer()]);
|
|
73
|
+
assert_eq!(union.show(), "String | Integer");
|
|
74
|
+
}
|
|
75
|
+
}
|