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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
use super::VertexId;
|
|
2
|
+
|
|
3
|
+
/// Manages edge changes for type propagation
|
|
4
|
+
#[derive(Debug, Clone)]
|
|
5
|
+
pub struct ChangeSet {
|
|
6
|
+
new_edges: Vec<(VertexId, VertexId)>,
|
|
7
|
+
edges: Vec<(VertexId, VertexId)>,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
impl ChangeSet {
|
|
11
|
+
pub fn new() -> Self {
|
|
12
|
+
Self {
|
|
13
|
+
new_edges: Vec::new(),
|
|
14
|
+
edges: Vec::new(),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Add edge
|
|
19
|
+
pub fn add_edge(&mut self, src: VertexId, dst: VertexId) {
|
|
20
|
+
self.new_edges.push((src, dst));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Commit changes and return list of added/removed edges
|
|
24
|
+
pub fn reinstall(&mut self) -> Vec<EdgeUpdate> {
|
|
25
|
+
// Remove duplicates
|
|
26
|
+
self.new_edges.sort_by_key(|&(src, dst)| (src.0, dst.0));
|
|
27
|
+
self.new_edges.dedup();
|
|
28
|
+
|
|
29
|
+
let mut updates = Vec::new();
|
|
30
|
+
|
|
31
|
+
// New edges
|
|
32
|
+
for &(src, dst) in &self.new_edges {
|
|
33
|
+
if !self.edges.contains(&(src, dst)) {
|
|
34
|
+
updates.push(EdgeUpdate::Add { src, dst });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Removed edges
|
|
39
|
+
for &(src, dst) in &self.edges {
|
|
40
|
+
if !self.new_edges.contains(&(src, dst)) {
|
|
41
|
+
updates.push(EdgeUpdate::Remove { src, dst });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Commit edges
|
|
46
|
+
std::mem::swap(&mut self.edges, &mut self.new_edges);
|
|
47
|
+
self.new_edges.clear();
|
|
48
|
+
|
|
49
|
+
updates
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Edge update type
|
|
54
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
55
|
+
pub enum EdgeUpdate {
|
|
56
|
+
Add { src: VertexId, dst: VertexId },
|
|
57
|
+
Remove { src: VertexId, dst: VertexId },
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[cfg(test)]
|
|
61
|
+
mod tests {
|
|
62
|
+
use super::*;
|
|
63
|
+
|
|
64
|
+
#[test]
|
|
65
|
+
fn test_change_set_add() {
|
|
66
|
+
let mut cs = ChangeSet::new();
|
|
67
|
+
|
|
68
|
+
cs.add_edge(VertexId(1), VertexId(2));
|
|
69
|
+
cs.add_edge(VertexId(2), VertexId(3));
|
|
70
|
+
|
|
71
|
+
let updates = cs.reinstall();
|
|
72
|
+
|
|
73
|
+
assert_eq!(updates.len(), 2);
|
|
74
|
+
assert!(updates.contains(&EdgeUpdate::Add {
|
|
75
|
+
src: VertexId(1),
|
|
76
|
+
dst: VertexId(2)
|
|
77
|
+
}));
|
|
78
|
+
assert!(updates.contains(&EdgeUpdate::Add {
|
|
79
|
+
src: VertexId(2),
|
|
80
|
+
dst: VertexId(3)
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[test]
|
|
85
|
+
fn test_change_set_dedup() {
|
|
86
|
+
let mut cs = ChangeSet::new();
|
|
87
|
+
|
|
88
|
+
cs.add_edge(VertexId(1), VertexId(2));
|
|
89
|
+
cs.add_edge(VertexId(1), VertexId(2)); // Duplicate
|
|
90
|
+
|
|
91
|
+
let updates = cs.reinstall();
|
|
92
|
+
|
|
93
|
+
assert_eq!(updates.len(), 1); // Duplicates removed
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[test]
|
|
97
|
+
fn test_change_set_remove() {
|
|
98
|
+
let mut cs = ChangeSet::new();
|
|
99
|
+
|
|
100
|
+
// First commit
|
|
101
|
+
cs.add_edge(VertexId(1), VertexId(2));
|
|
102
|
+
cs.add_edge(VertexId(2), VertexId(3));
|
|
103
|
+
cs.reinstall();
|
|
104
|
+
|
|
105
|
+
// Second time: keep only (1,2)
|
|
106
|
+
cs.add_edge(VertexId(1), VertexId(2));
|
|
107
|
+
let updates = cs.reinstall();
|
|
108
|
+
|
|
109
|
+
assert_eq!(updates.len(), 1);
|
|
110
|
+
assert!(updates.contains(&EdgeUpdate::Remove {
|
|
111
|
+
src: VertexId(2),
|
|
112
|
+
dst: VertexId(3)
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
use crate::types::Type;
|
|
2
|
+
use std::collections::{HashMap, HashSet};
|
|
3
|
+
|
|
4
|
+
/// Vertex ID (uniquely identifies a vertex in the graph)
|
|
5
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
6
|
+
pub struct VertexId(pub usize);
|
|
7
|
+
|
|
8
|
+
/// Source: Vertex with fixed type (e.g., literals)
|
|
9
|
+
#[derive(Debug, Clone)]
|
|
10
|
+
pub struct Source {
|
|
11
|
+
pub ty: Type,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
impl Source {
|
|
15
|
+
pub fn new(ty: Type) -> Self {
|
|
16
|
+
Self { ty }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Vertex: Vertex that dynamically accumulates types (e.g., variables)
|
|
21
|
+
#[derive(Debug, Clone)]
|
|
22
|
+
pub struct Vertex {
|
|
23
|
+
/// Type -> Sources (set of Source IDs that provided this type)
|
|
24
|
+
pub types: HashMap<Type, HashSet<VertexId>>,
|
|
25
|
+
/// Set of connected Vertex IDs
|
|
26
|
+
pub next_vtxs: HashSet<VertexId>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl Vertex {
|
|
30
|
+
pub fn new() -> Self {
|
|
31
|
+
Self {
|
|
32
|
+
types: HashMap::new(),
|
|
33
|
+
next_vtxs: HashSet::new(),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Add connection destination
|
|
38
|
+
pub fn add_next(&mut self, next_id: VertexId) {
|
|
39
|
+
self.next_vtxs.insert(next_id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Add type (core of type propagation)
|
|
43
|
+
/// Returns: list of newly added types and destinations to propagate to
|
|
44
|
+
pub fn on_type_added(
|
|
45
|
+
&mut self,
|
|
46
|
+
src_id: VertexId,
|
|
47
|
+
added_types: Vec<Type>,
|
|
48
|
+
) -> Vec<(VertexId, Vec<Type>)> {
|
|
49
|
+
let mut new_added_types = Vec::new();
|
|
50
|
+
|
|
51
|
+
for ty in added_types {
|
|
52
|
+
if let Some(sources) = self.types.get_mut(&ty) {
|
|
53
|
+
// Type already exists: add Source
|
|
54
|
+
sources.insert(src_id);
|
|
55
|
+
} else {
|
|
56
|
+
// New type: add type and record Source
|
|
57
|
+
let mut sources = HashSet::new();
|
|
58
|
+
sources.insert(src_id);
|
|
59
|
+
self.types.insert(ty.clone(), sources);
|
|
60
|
+
new_added_types.push(ty);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If no new types, don't propagate anything
|
|
65
|
+
if new_added_types.is_empty() {
|
|
66
|
+
return vec![];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Propagate to connections
|
|
70
|
+
self.next_vtxs
|
|
71
|
+
.iter()
|
|
72
|
+
.map(|&next_id| (next_id, new_added_types.clone()))
|
|
73
|
+
.collect()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Convert type to string representation
|
|
77
|
+
pub fn show(&self) -> String {
|
|
78
|
+
if self.types.is_empty() {
|
|
79
|
+
return "untyped".to_string();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let mut type_strs: Vec<_> = self.types.keys().map(|t| t.show()).collect();
|
|
83
|
+
type_strs.sort();
|
|
84
|
+
type_strs.dedup();
|
|
85
|
+
|
|
86
|
+
if type_strs.len() == 1 {
|
|
87
|
+
type_strs[0].clone()
|
|
88
|
+
} else {
|
|
89
|
+
format!("({})", type_strs.join(" | "))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
impl Default for Vertex {
|
|
95
|
+
fn default() -> Self {
|
|
96
|
+
Self::new()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[cfg(test)]
|
|
101
|
+
mod tests {
|
|
102
|
+
use super::*;
|
|
103
|
+
|
|
104
|
+
#[test]
|
|
105
|
+
fn test_source_type() {
|
|
106
|
+
let src = Source::new(Type::string());
|
|
107
|
+
assert_eq!(src.ty.show(), "String");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[test]
|
|
111
|
+
fn test_vertex_empty() {
|
|
112
|
+
let vtx = Vertex::new();
|
|
113
|
+
assert_eq!(vtx.show(), "untyped");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[test]
|
|
117
|
+
fn test_vertex_single_type() {
|
|
118
|
+
let mut vtx = Vertex::new();
|
|
119
|
+
|
|
120
|
+
// Add String type
|
|
121
|
+
let propagations = vtx.on_type_added(VertexId(1), vec![Type::string()]);
|
|
122
|
+
assert_eq!(vtx.show(), "String");
|
|
123
|
+
assert_eq!(propagations.len(), 0); // No propagation since no connections
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#[test]
|
|
127
|
+
fn test_vertex_union_type() {
|
|
128
|
+
let mut vtx = Vertex::new();
|
|
129
|
+
|
|
130
|
+
// Add String type
|
|
131
|
+
vtx.on_type_added(VertexId(1), vec![Type::string()]);
|
|
132
|
+
assert_eq!(vtx.show(), "String");
|
|
133
|
+
|
|
134
|
+
// Add Integer type → becomes Union type
|
|
135
|
+
vtx.on_type_added(VertexId(1), vec![Type::integer()]);
|
|
136
|
+
assert_eq!(vtx.show(), "(Integer | String)");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[test]
|
|
140
|
+
fn test_vertex_propagation() {
|
|
141
|
+
let mut vtx = Vertex::new();
|
|
142
|
+
|
|
143
|
+
// Add connections
|
|
144
|
+
vtx.add_next(VertexId(3));
|
|
145
|
+
vtx.add_next(VertexId(4));
|
|
146
|
+
|
|
147
|
+
// Add type → propagated to connections
|
|
148
|
+
let propagations = vtx.on_type_added(VertexId(1), vec![Type::string()]);
|
|
149
|
+
|
|
150
|
+
assert_eq!(propagations.len(), 2);
|
|
151
|
+
assert!(propagations.contains(&(VertexId(3), vec![Type::string()])));
|
|
152
|
+
assert!(propagations.contains(&(VertexId(4), vec![Type::string()])));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[test]
|
|
156
|
+
fn test_vertex_no_duplicate_propagation() {
|
|
157
|
+
let mut vtx = Vertex::new();
|
|
158
|
+
vtx.add_next(VertexId(3));
|
|
159
|
+
|
|
160
|
+
// Add same type twice → only first time propagates
|
|
161
|
+
let prop1 = vtx.on_type_added(VertexId(1), vec![Type::string()]);
|
|
162
|
+
assert_eq!(prop1.len(), 1);
|
|
163
|
+
|
|
164
|
+
let prop2 = vtx.on_type_added(VertexId(1), vec![Type::string()]);
|
|
165
|
+
assert_eq!(prop2.len(), 0); // No propagation since already exists
|
|
166
|
+
}
|
|
167
|
+
}
|
data/rust/src/lib.rs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//! Method-Ray Core - Static type checker for Ruby
|
|
2
|
+
//!
|
|
3
|
+
//! This crate provides the core type inference engine.
|
|
4
|
+
|
|
5
|
+
pub mod analyzer;
|
|
6
|
+
pub mod cache;
|
|
7
|
+
pub mod diagnostics;
|
|
8
|
+
pub mod env;
|
|
9
|
+
pub mod graph;
|
|
10
|
+
pub mod parser;
|
|
11
|
+
pub mod source_map;
|
|
12
|
+
pub mod types;
|
|
13
|
+
|
|
14
|
+
#[cfg(feature = "ruby-ffi")]
|
|
15
|
+
pub mod rbs;
|
|
16
|
+
|
|
17
|
+
#[cfg(any(feature = "cli", feature = "lsp"))]
|
|
18
|
+
pub mod checker;
|
|
19
|
+
|
|
20
|
+
#[cfg(feature = "cli")]
|
|
21
|
+
pub mod cli;
|
|
22
|
+
|
|
23
|
+
#[cfg(feature = "lsp")]
|
|
24
|
+
pub mod lsp;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
use crate::diagnostics::{Diagnostic as MethodRayDiagnostic, DiagnosticLevel};
|
|
2
|
+
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
|
|
3
|
+
|
|
4
|
+
/// Extract method name length from error message
|
|
5
|
+
/// Supports messages like:
|
|
6
|
+
/// - "undefined method `downcase` for Integer"
|
|
7
|
+
/// - "method `upcase` is defined for ..."
|
|
8
|
+
fn extract_method_name_length(message: &str) -> Option<u32> {
|
|
9
|
+
// Pattern: `method_name`
|
|
10
|
+
if let Some(start) = message.find('`') {
|
|
11
|
+
if let Some(end) = message[start + 1..].find('`') {
|
|
12
|
+
let method_name = &message[start + 1..start + 1 + end];
|
|
13
|
+
return Some(method_name.len() as u32);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
None
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Convert MethodRay Diagnostic to LSP Diagnostic
|
|
20
|
+
pub fn to_lsp_diagnostic(diag: &MethodRayDiagnostic) -> Diagnostic {
|
|
21
|
+
let severity = match diag.level {
|
|
22
|
+
DiagnosticLevel::Error => DiagnosticSeverity::ERROR,
|
|
23
|
+
DiagnosticLevel::Warning => DiagnosticSeverity::WARNING,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let start_line = if diag.location.line > 0 {
|
|
27
|
+
(diag.location.line - 1) as u32
|
|
28
|
+
} else {
|
|
29
|
+
0
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let start_char = if diag.location.column > 0 {
|
|
33
|
+
(diag.location.column - 1) as u32
|
|
34
|
+
} else {
|
|
35
|
+
0
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Use actual source length if available, otherwise extract from message
|
|
39
|
+
let highlight_length = diag
|
|
40
|
+
.location
|
|
41
|
+
.length
|
|
42
|
+
.map(|len| len as u32)
|
|
43
|
+
.or_else(|| extract_method_name_length(&diag.message))
|
|
44
|
+
.unwrap_or(5);
|
|
45
|
+
let end_char = start_char + highlight_length;
|
|
46
|
+
|
|
47
|
+
Diagnostic {
|
|
48
|
+
range: Range {
|
|
49
|
+
start: Position {
|
|
50
|
+
line: start_line,
|
|
51
|
+
character: start_char,
|
|
52
|
+
},
|
|
53
|
+
end: Position {
|
|
54
|
+
line: start_line,
|
|
55
|
+
character: end_char,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
severity: Some(severity),
|
|
59
|
+
code: None,
|
|
60
|
+
code_description: None,
|
|
61
|
+
source: Some("methodray".to_string()),
|
|
62
|
+
message: diag.message.clone(),
|
|
63
|
+
related_information: None,
|
|
64
|
+
tags: None,
|
|
65
|
+
data: None,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[cfg(test)]
|
|
70
|
+
mod tests {
|
|
71
|
+
use super::*;
|
|
72
|
+
use crate::diagnostics::Location;
|
|
73
|
+
|
|
74
|
+
#[test]
|
|
75
|
+
fn test_to_lsp_diagnostic() {
|
|
76
|
+
use std::path::PathBuf;
|
|
77
|
+
|
|
78
|
+
let methodray_diag = MethodRayDiagnostic {
|
|
79
|
+
level: DiagnosticLevel::Error,
|
|
80
|
+
location: Location {
|
|
81
|
+
file: PathBuf::from("test.rb"),
|
|
82
|
+
line: 5,
|
|
83
|
+
column: 10,
|
|
84
|
+
length: Some(6), // "upcase".len()
|
|
85
|
+
},
|
|
86
|
+
message: "undefined method `upcase` for Integer".to_string(),
|
|
87
|
+
code: None,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
let lsp_diag = to_lsp_diagnostic(&methodray_diag);
|
|
91
|
+
|
|
92
|
+
assert_eq!(lsp_diag.range.start.line, 4); // 0-indexed
|
|
93
|
+
assert_eq!(lsp_diag.range.start.character, 9); // 0-indexed
|
|
94
|
+
assert_eq!(lsp_diag.range.end.character, 15); // start(9) + length(6)
|
|
95
|
+
assert_eq!(lsp_diag.severity, Some(DiagnosticSeverity::ERROR));
|
|
96
|
+
assert_eq!(lsp_diag.message, "undefined method `upcase` for Integer");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
fn test_extract_method_name_length() {
|
|
101
|
+
assert_eq!(
|
|
102
|
+
extract_method_name_length("undefined method `downcase` for Integer"),
|
|
103
|
+
Some(8)
|
|
104
|
+
);
|
|
105
|
+
assert_eq!(
|
|
106
|
+
extract_method_name_length("method `upcase` is defined for String"),
|
|
107
|
+
Some(6)
|
|
108
|
+
);
|
|
109
|
+
assert_eq!(extract_method_name_length("no method name here"), None);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[test]
|
|
113
|
+
fn test_highlight_length_for_downcase() {
|
|
114
|
+
use std::path::PathBuf;
|
|
115
|
+
|
|
116
|
+
let methodray_diag = MethodRayDiagnostic {
|
|
117
|
+
level: DiagnosticLevel::Error,
|
|
118
|
+
location: Location {
|
|
119
|
+
file: PathBuf::from("test.rb"),
|
|
120
|
+
line: 2,
|
|
121
|
+
column: 5,
|
|
122
|
+
length: Some(8), // "downcase".len()
|
|
123
|
+
},
|
|
124
|
+
message: "undefined method `downcase` for Integer".to_string(),
|
|
125
|
+
code: None,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
let lsp_diag = to_lsp_diagnostic(&methodray_diag);
|
|
129
|
+
|
|
130
|
+
assert_eq!(lsp_diag.range.start.character, 4); // column 5 -> 0-indexed = 4
|
|
131
|
+
assert_eq!(lsp_diag.range.end.character, 12); // start(4) + length(8)
|
|
132
|
+
}
|
|
133
|
+
}
|
data/rust/src/lsp/mod.rs
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
use anyhow::Context;
|
|
2
|
+
use std::collections::HashMap;
|
|
3
|
+
use tokio::sync::RwLock;
|
|
4
|
+
use tower_lsp::jsonrpc::Result;
|
|
5
|
+
use tower_lsp::lsp_types::*;
|
|
6
|
+
use tower_lsp::{Client, LanguageServer, LspService, Server};
|
|
7
|
+
|
|
8
|
+
use super::diagnostics::to_lsp_diagnostic;
|
|
9
|
+
use crate::checker::FileChecker;
|
|
10
|
+
|
|
11
|
+
pub struct MethodRayServer {
|
|
12
|
+
client: Client,
|
|
13
|
+
documents: RwLock<HashMap<Url, String>>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl MethodRayServer {
|
|
17
|
+
pub fn new(client: Client) -> Self {
|
|
18
|
+
Self {
|
|
19
|
+
client,
|
|
20
|
+
documents: RwLock::new(HashMap::new()),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async fn check_document(&self, uri: Url) {
|
|
25
|
+
let documents = self.documents.read().await;
|
|
26
|
+
|
|
27
|
+
if let Some(source) = documents.get(&uri) {
|
|
28
|
+
match self.run_type_check(&uri, source).await {
|
|
29
|
+
Ok(diagnostics) => {
|
|
30
|
+
self.client
|
|
31
|
+
.publish_diagnostics(uri.clone(), diagnostics, None)
|
|
32
|
+
.await;
|
|
33
|
+
}
|
|
34
|
+
Err(e) => {
|
|
35
|
+
self.client
|
|
36
|
+
.log_message(MessageType::ERROR, format!("Type check failed: {}", e))
|
|
37
|
+
.await;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async fn run_type_check(&self, uri: &Url, source: &str) -> anyhow::Result<Vec<Diagnostic>> {
|
|
44
|
+
// Convert URI to file path
|
|
45
|
+
let file_path = uri
|
|
46
|
+
.to_file_path()
|
|
47
|
+
.map_err(|_| anyhow::anyhow!("Invalid file URI: {}", uri))?;
|
|
48
|
+
|
|
49
|
+
// Create a temporary file for checking
|
|
50
|
+
let temp_dir = tempfile::tempdir()?;
|
|
51
|
+
let temp_file = temp_dir.path().join("temp.rb");
|
|
52
|
+
std::fs::write(&temp_file, source)?;
|
|
53
|
+
|
|
54
|
+
// Run type check using FileChecker
|
|
55
|
+
let checker = FileChecker::new().with_context(|| "Failed to create FileChecker")?;
|
|
56
|
+
|
|
57
|
+
let methodray_diagnostics = checker
|
|
58
|
+
.check_file(&temp_file)
|
|
59
|
+
.with_context(|| format!("Failed to check file: {}", file_path.display()))?;
|
|
60
|
+
|
|
61
|
+
// Convert to LSP diagnostics
|
|
62
|
+
let lsp_diagnostics = methodray_diagnostics
|
|
63
|
+
.iter()
|
|
64
|
+
.map(to_lsp_diagnostic)
|
|
65
|
+
.collect();
|
|
66
|
+
|
|
67
|
+
Ok(lsp_diagnostics)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[tower_lsp::async_trait]
|
|
72
|
+
impl LanguageServer for MethodRayServer {
|
|
73
|
+
async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
|
|
74
|
+
Ok(InitializeResult {
|
|
75
|
+
capabilities: ServerCapabilities {
|
|
76
|
+
text_document_sync: Some(TextDocumentSyncCapability::Kind(
|
|
77
|
+
TextDocumentSyncKind::FULL,
|
|
78
|
+
)),
|
|
79
|
+
..Default::default()
|
|
80
|
+
},
|
|
81
|
+
..Default::default()
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async fn initialized(&self, _: InitializedParams) {
|
|
86
|
+
self.client
|
|
87
|
+
.log_message(MessageType::INFO, "MethodRay LSP server initialized")
|
|
88
|
+
.await;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async fn did_open(&self, params: DidOpenTextDocumentParams) {
|
|
92
|
+
let uri = params.text_document.uri;
|
|
93
|
+
let text = params.text_document.text;
|
|
94
|
+
|
|
95
|
+
self.documents.write().await.insert(uri.clone(), text);
|
|
96
|
+
self.check_document(uri).await;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async fn did_change(&self, params: DidChangeTextDocumentParams) {
|
|
100
|
+
let uri = params.text_document.uri;
|
|
101
|
+
|
|
102
|
+
if let Some(change) = params.content_changes.first() {
|
|
103
|
+
let text = change.text.clone();
|
|
104
|
+
self.documents.write().await.insert(uri.clone(), text);
|
|
105
|
+
// Note: In production, we'd want debouncing here
|
|
106
|
+
// self.check_document(uri).await;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async fn did_save(&self, params: DidSaveTextDocumentParams) {
|
|
111
|
+
self.check_document(params.text_document.uri).await;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async fn did_close(&self, params: DidCloseTextDocumentParams) {
|
|
115
|
+
self.documents
|
|
116
|
+
.write()
|
|
117
|
+
.await
|
|
118
|
+
.remove(¶ms.text_document.uri);
|
|
119
|
+
|
|
120
|
+
// Clear diagnostics
|
|
121
|
+
self.client
|
|
122
|
+
.publish_diagnostics(params.text_document.uri, vec![], None)
|
|
123
|
+
.await;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async fn shutdown(&self) -> Result<()> {
|
|
127
|
+
Ok(())
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
pub async fn run_server() {
|
|
132
|
+
let stdin = tokio::io::stdin();
|
|
133
|
+
let stdout = tokio::io::stdout();
|
|
134
|
+
|
|
135
|
+
let (service, socket) = LspService::new(|client| MethodRayServer::new(client));
|
|
136
|
+
|
|
137
|
+
Server::new(stdin, stdout, socket).serve(service).await;
|
|
138
|
+
}
|
data/rust/src/main.rs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//! MethodRay CLI - Fast Ruby type checker
|
|
2
|
+
|
|
3
|
+
use anyhow::Result;
|
|
4
|
+
use clap::Parser;
|
|
5
|
+
|
|
6
|
+
mod analyzer;
|
|
7
|
+
mod cache;
|
|
8
|
+
mod checker;
|
|
9
|
+
mod cli;
|
|
10
|
+
mod diagnostics;
|
|
11
|
+
mod env;
|
|
12
|
+
mod graph;
|
|
13
|
+
mod parser;
|
|
14
|
+
mod rbs;
|
|
15
|
+
mod source_map;
|
|
16
|
+
mod types;
|
|
17
|
+
|
|
18
|
+
use cli::{commands, Cli, Commands};
|
|
19
|
+
|
|
20
|
+
fn main() -> Result<()> {
|
|
21
|
+
let cli = Cli::parse();
|
|
22
|
+
|
|
23
|
+
match cli.command {
|
|
24
|
+
Commands::Check { file, verbose } => {
|
|
25
|
+
if let Some(file_path) = file {
|
|
26
|
+
let success = commands::check_single_file(&file_path, verbose)?;
|
|
27
|
+
if !success {
|
|
28
|
+
std::process::exit(1);
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
commands::check_project(verbose)?;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
Commands::Watch { file } => {
|
|
35
|
+
commands::watch_file(&file)?;
|
|
36
|
+
}
|
|
37
|
+
Commands::Version => {
|
|
38
|
+
commands::print_version();
|
|
39
|
+
}
|
|
40
|
+
Commands::ClearCache => {
|
|
41
|
+
commands::clear_cache()?;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Ok(())
|
|
46
|
+
}
|