method-ray 0.1.6 → 0.1.8
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 +4 -4
- data/CHANGELOG.md +30 -0
- data/ext/Cargo.toml +1 -1
- data/lib/methodray/version.rb +1 -1
- data/rust/Cargo.toml +1 -1
- data/rust/src/analyzer/assignments.rs +152 -0
- data/rust/src/analyzer/attributes.rs +2 -1
- data/rust/src/analyzer/blocks.rs +4 -3
- data/rust/src/analyzer/calls.rs +7 -3
- data/rust/src/analyzer/definitions.rs +10 -5
- data/rust/src/analyzer/dispatch.rs +265 -24
- data/rust/src/analyzer/exceptions.rs +521 -0
- data/rust/src/analyzer/install.rs +22 -1
- data/rust/src/analyzer/loops.rs +176 -0
- data/rust/src/analyzer/mod.rs +32 -0
- data/rust/src/analyzer/operators.rs +119 -27
- data/rust/src/analyzer/parameters.rs +60 -9
- data/rust/src/cache/rbs_cache.rs +0 -1
- data/rust/src/cli/commands.rs +3 -3
- data/rust/src/diagnostics/diagnostic.rs +0 -3
- data/rust/src/diagnostics/formatter.rs +0 -1
- data/rust/src/env/box_manager.rs +2 -4
- data/rust/src/env/global_env.rs +43 -5
- data/rust/src/env/local_env.rs +35 -1
- data/rust/src/env/method_registry.rs +140 -17
- data/rust/src/env/scope.rs +143 -164
- data/rust/src/env/vertex_manager.rs +0 -1
- data/rust/src/graph/box.rs +217 -84
- data/rust/src/graph/change_set.rs +14 -0
- data/rust/src/lsp/server.rs +1 -1
- data/rust/src/rbs/loader.rs +1 -2
- data/rust/src/source_map.rs +0 -1
- data/rust/src/types.rs +0 -1
- metadata +4 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
//! Loops - while/until loop type inference
|
|
2
|
+
//!
|
|
3
|
+
//! Ruby loop expressions evaluate to nil (except when break passes a value).
|
|
4
|
+
//! Break value propagation is not yet supported.
|
|
5
|
+
|
|
6
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
7
|
+
use crate::graph::{ChangeSet, VertexId};
|
|
8
|
+
use crate::types::Type;
|
|
9
|
+
use ruby_prism::{UntilNode, WhileNode};
|
|
10
|
+
|
|
11
|
+
use super::install::{install_node, install_statements};
|
|
12
|
+
|
|
13
|
+
/// Process WhileNode: `while predicate; statements; end`
|
|
14
|
+
///
|
|
15
|
+
/// Returns nil. Traverses predicate and body to register method calls
|
|
16
|
+
/// and variable assignments in the type graph.
|
|
17
|
+
pub(crate) fn process_while_node(
|
|
18
|
+
genv: &mut GlobalEnv,
|
|
19
|
+
lenv: &mut LocalEnv,
|
|
20
|
+
changes: &mut ChangeSet,
|
|
21
|
+
source: &str,
|
|
22
|
+
while_node: &WhileNode,
|
|
23
|
+
) -> Option<VertexId> {
|
|
24
|
+
install_node(genv, lenv, changes, source, &while_node.predicate());
|
|
25
|
+
|
|
26
|
+
if let Some(stmts) = while_node.statements() {
|
|
27
|
+
install_statements(genv, lenv, changes, source, &stmts);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Some(genv.new_source(Type::Nil))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Process UntilNode: `until predicate; statements; end`
|
|
34
|
+
///
|
|
35
|
+
/// Returns nil. Traverses predicate and body to register method calls
|
|
36
|
+
/// and variable assignments in the type graph.
|
|
37
|
+
pub(crate) fn process_until_node(
|
|
38
|
+
genv: &mut GlobalEnv,
|
|
39
|
+
lenv: &mut LocalEnv,
|
|
40
|
+
changes: &mut ChangeSet,
|
|
41
|
+
source: &str,
|
|
42
|
+
until_node: &UntilNode,
|
|
43
|
+
) -> Option<VertexId> {
|
|
44
|
+
install_node(genv, lenv, changes, source, &until_node.predicate());
|
|
45
|
+
|
|
46
|
+
if let Some(stmts) = until_node.statements() {
|
|
47
|
+
install_statements(genv, lenv, changes, source, &stmts);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
Some(genv.new_source(Type::Nil))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[cfg(test)]
|
|
54
|
+
mod tests {
|
|
55
|
+
use crate::analyzer::install::AstInstaller;
|
|
56
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
57
|
+
use crate::graph::VertexId;
|
|
58
|
+
use crate::parser::ParseSession;
|
|
59
|
+
use crate::types::Type;
|
|
60
|
+
|
|
61
|
+
/// Helper: parse Ruby source, process with AstInstaller, and return GlobalEnv
|
|
62
|
+
fn analyze(source: &str) -> GlobalEnv {
|
|
63
|
+
let session = ParseSession::new();
|
|
64
|
+
let parse_result = session.parse_source(source, "test.rb").unwrap();
|
|
65
|
+
let root = parse_result.node();
|
|
66
|
+
let program = root.as_program_node().unwrap();
|
|
67
|
+
|
|
68
|
+
let mut genv = GlobalEnv::new();
|
|
69
|
+
let mut lenv = LocalEnv::new();
|
|
70
|
+
|
|
71
|
+
let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
|
|
72
|
+
for stmt in &program.statements().body() {
|
|
73
|
+
installer.install_node(&stmt);
|
|
74
|
+
}
|
|
75
|
+
installer.finish();
|
|
76
|
+
|
|
77
|
+
genv
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Helper: get the type string for a vertex ID (checks both Vertex and Source)
|
|
81
|
+
fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
|
|
82
|
+
if let Some(vertex) = genv.get_vertex(vtx) {
|
|
83
|
+
vertex.show()
|
|
84
|
+
} else if let Some(source) = genv.get_source(vtx) {
|
|
85
|
+
source.ty.show()
|
|
86
|
+
} else {
|
|
87
|
+
panic!("vertex {:?} not found as either Vertex or Source", vtx);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[test]
|
|
92
|
+
fn test_while_returns_nil() {
|
|
93
|
+
let source = r#"
|
|
94
|
+
class Foo
|
|
95
|
+
def bar
|
|
96
|
+
while true
|
|
97
|
+
"hello"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
"#;
|
|
102
|
+
let genv = analyze(source);
|
|
103
|
+
let info = genv
|
|
104
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
105
|
+
.expect("Foo#bar should be registered");
|
|
106
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
107
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[test]
|
|
111
|
+
fn test_until_returns_nil() {
|
|
112
|
+
let source = r#"
|
|
113
|
+
class Foo
|
|
114
|
+
def bar
|
|
115
|
+
until false
|
|
116
|
+
"hello"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
"#;
|
|
121
|
+
let genv = analyze(source);
|
|
122
|
+
let info = genv
|
|
123
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
124
|
+
.expect("Foo#bar should be registered");
|
|
125
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
126
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[test]
|
|
130
|
+
fn test_while_variable_assignment_in_body() {
|
|
131
|
+
// Should not panic — variable assignment inside loop is processed
|
|
132
|
+
let source = r#"
|
|
133
|
+
x = "initial"
|
|
134
|
+
while true
|
|
135
|
+
x = "hello"
|
|
136
|
+
end
|
|
137
|
+
"#;
|
|
138
|
+
analyze(source);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[test]
|
|
142
|
+
fn test_while_modifier_form() {
|
|
143
|
+
let source = r#"
|
|
144
|
+
class Foo
|
|
145
|
+
def bar
|
|
146
|
+
x = "hello" while false
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
"#;
|
|
150
|
+
let genv = analyze(source);
|
|
151
|
+
let info = genv
|
|
152
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
153
|
+
.expect("Foo#bar should be registered");
|
|
154
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
155
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[test]
|
|
159
|
+
fn test_begin_end_while() {
|
|
160
|
+
let source = r#"
|
|
161
|
+
class Foo
|
|
162
|
+
def bar
|
|
163
|
+
begin
|
|
164
|
+
"hello"
|
|
165
|
+
end while false
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
"#;
|
|
169
|
+
let genv = analyze(source);
|
|
170
|
+
let info = genv
|
|
171
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
172
|
+
.expect("Foo#bar should be registered");
|
|
173
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
174
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "nil");
|
|
175
|
+
}
|
|
176
|
+
}
|
data/rust/src/analyzer/mod.rs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
mod assignments;
|
|
1
2
|
mod attributes;
|
|
2
3
|
mod blocks;
|
|
3
4
|
mod calls;
|
|
4
5
|
mod conditionals;
|
|
5
6
|
mod definitions;
|
|
7
|
+
mod exceptions;
|
|
6
8
|
mod dispatch;
|
|
7
9
|
mod install;
|
|
8
10
|
mod literals;
|
|
11
|
+
mod loops;
|
|
9
12
|
mod operators;
|
|
10
13
|
mod parameters;
|
|
11
14
|
mod parentheses;
|
|
@@ -13,3 +16,32 @@ mod returns;
|
|
|
13
16
|
mod variables;
|
|
14
17
|
|
|
15
18
|
pub use install::AstInstaller;
|
|
19
|
+
|
|
20
|
+
/// Convert ruby-prism identifier bytes to a String (lossy).
|
|
21
|
+
///
|
|
22
|
+
/// ruby-prism returns identifiers (method names, variable names, constant names,
|
|
23
|
+
/// parameter names) as `&[u8]`. This helper provides a single conversion point
|
|
24
|
+
/// used throughout the analyzer.
|
|
25
|
+
///
|
|
26
|
+
/// Note: Uses `from_utf8_lossy` — invalid UTF-8 bytes are replaced with U+FFFD.
|
|
27
|
+
/// ruby-prism identifiers are expected to be valid UTF-8, so this should not
|
|
28
|
+
/// occur in practice. Do NOT use this function for arbitrary byte data such as
|
|
29
|
+
/// string literal contents.
|
|
30
|
+
pub(crate) fn bytes_to_name(bytes: &[u8]) -> String {
|
|
31
|
+
String::from_utf8_lossy(bytes).to_string()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[cfg(test)]
|
|
35
|
+
mod tests {
|
|
36
|
+
use super::bytes_to_name;
|
|
37
|
+
|
|
38
|
+
#[test]
|
|
39
|
+
fn test_bytes_to_name_valid_utf8() {
|
|
40
|
+
assert_eq!(bytes_to_name(b"hello"), "hello");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn test_bytes_to_name_invalid_utf8_replaced() {
|
|
45
|
+
assert_eq!(bytes_to_name(b"hello\xff"), "hello\u{FFFD}");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1,42 +1,52 @@
|
|
|
1
|
-
//! Operators - logical operator type inference (&&,
|
|
1
|
+
//! Operators - logical operator type inference (&&, ||, !)
|
|
2
2
|
|
|
3
3
|
use crate::env::{GlobalEnv, LocalEnv};
|
|
4
4
|
use crate::graph::{ChangeSet, VertexId};
|
|
5
|
-
use
|
|
5
|
+
use crate::types::Type;
|
|
6
|
+
use ruby_prism::{AndNode, Node, OrNode};
|
|
6
7
|
|
|
7
8
|
use super::install::install_node;
|
|
8
9
|
|
|
9
|
-
///
|
|
10
|
-
|
|
11
|
-
/// Short-circuit semantics: if `a` is falsy, returns `a`; otherwise returns `b`.
|
|
12
|
-
/// Static approximation: we cannot determine truthiness at compile time,
|
|
13
|
-
/// so we conservatively produce Union(type(a), type(b)).
|
|
14
|
-
pub(crate) fn process_and_node(
|
|
10
|
+
/// Merge two branch nodes into a union type vertex.
|
|
11
|
+
fn process_binary_logical_op<'a>(
|
|
15
12
|
genv: &mut GlobalEnv,
|
|
16
13
|
lenv: &mut LocalEnv,
|
|
17
14
|
changes: &mut ChangeSet,
|
|
18
15
|
source: &str,
|
|
19
|
-
|
|
16
|
+
left: Node<'a>,
|
|
17
|
+
right: Node<'a>,
|
|
20
18
|
) -> Option<VertexId> {
|
|
21
19
|
let result_vtx = genv.new_vertex();
|
|
22
20
|
|
|
23
|
-
let
|
|
24
|
-
if let Some(vtx) = left_vtx {
|
|
21
|
+
if let Some(vtx) = install_node(genv, lenv, changes, source, &left) {
|
|
25
22
|
genv.add_edge(vtx, result_vtx);
|
|
26
23
|
}
|
|
27
24
|
|
|
28
|
-
let
|
|
29
|
-
if let Some(vtx) = right_vtx {
|
|
25
|
+
if let Some(vtx) = install_node(genv, lenv, changes, source, &right) {
|
|
30
26
|
genv.add_edge(vtx, result_vtx);
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
Some(result_vtx)
|
|
34
30
|
}
|
|
35
31
|
|
|
32
|
+
/// Process AndNode (a && b): Union(type(a), type(b))
|
|
33
|
+
///
|
|
34
|
+
/// Runtime: if `a` is falsy, returns `a`; otherwise returns `b`.
|
|
35
|
+
/// Static: conservatively produce Union(type(a), type(b)).
|
|
36
|
+
pub(crate) fn process_and_node(
|
|
37
|
+
genv: &mut GlobalEnv,
|
|
38
|
+
lenv: &mut LocalEnv,
|
|
39
|
+
changes: &mut ChangeSet,
|
|
40
|
+
source: &str,
|
|
41
|
+
and_node: &AndNode,
|
|
42
|
+
) -> Option<VertexId> {
|
|
43
|
+
process_binary_logical_op(genv, lenv, changes, source, and_node.left(), and_node.right())
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
/// Process OrNode (a || b): Union(type(a), type(b))
|
|
37
47
|
///
|
|
38
|
-
///
|
|
39
|
-
/// Static
|
|
48
|
+
/// Runtime: if `a` is truthy, returns `a`; otherwise returns `b`.
|
|
49
|
+
/// Static: conservatively produce Union(type(a), type(b)).
|
|
40
50
|
pub(crate) fn process_or_node(
|
|
41
51
|
genv: &mut GlobalEnv,
|
|
42
52
|
lenv: &mut LocalEnv,
|
|
@@ -44,19 +54,28 @@ pub(crate) fn process_or_node(
|
|
|
44
54
|
source: &str,
|
|
45
55
|
or_node: &OrNode,
|
|
46
56
|
) -> Option<VertexId> {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
let left_vtx = install_node(genv, lenv, changes, source, &or_node.left());
|
|
50
|
-
if let Some(vtx) = left_vtx {
|
|
51
|
-
genv.add_edge(vtx, result_vtx);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
let right_vtx = install_node(genv, lenv, changes, source, &or_node.right());
|
|
55
|
-
if let Some(vtx) = right_vtx {
|
|
56
|
-
genv.add_edge(vtx, result_vtx);
|
|
57
|
-
}
|
|
57
|
+
process_binary_logical_op(genv, lenv, changes, source, or_node.left(), or_node.right())
|
|
58
|
+
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
/// Process not operator (!expr): TrueClass | FalseClass
|
|
61
|
+
///
|
|
62
|
+
/// In ruby-prism, `!expr` is represented as a CallNode with method name "!".
|
|
63
|
+
/// Static approximation: we cannot determine the receiver's truthiness at
|
|
64
|
+
/// compile time, so conservatively return TrueClass | FalseClass for any `!` call.
|
|
65
|
+
/// In practice, `!nil` and `!false` are always true, but we do not track that here.
|
|
66
|
+
///
|
|
67
|
+
/// Receiver side effects are already analyzed by the caller (process_needs_child).
|
|
68
|
+
///
|
|
69
|
+
/// TODO: Ruby allows overriding `BasicObject#!`. Currently we always return
|
|
70
|
+
/// TrueClass | FalseClass, ignoring user-defined `!` methods. If needed, look up
|
|
71
|
+
/// the receiver's RBS definition and use its return type instead.
|
|
72
|
+
pub(crate) fn process_not_operator(genv: &mut GlobalEnv) -> VertexId {
|
|
73
|
+
let result_vtx = genv.new_vertex();
|
|
74
|
+
let true_vtx = genv.new_source(Type::instance("TrueClass"));
|
|
75
|
+
let false_vtx = genv.new_source(Type::instance("FalseClass"));
|
|
76
|
+
genv.add_edge(true_vtx, result_vtx);
|
|
77
|
+
genv.add_edge(false_vtx, result_vtx);
|
|
78
|
+
result_vtx
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
#[cfg(test)]
|
|
@@ -189,4 +208,77 @@ end
|
|
|
189
208
|
assert!(type_str.contains("Symbol"), "should contain Symbol: {}", type_str);
|
|
190
209
|
}
|
|
191
210
|
|
|
211
|
+
// ============================================
|
|
212
|
+
// Not operator (!) tests
|
|
213
|
+
// ============================================
|
|
214
|
+
|
|
215
|
+
#[test]
|
|
216
|
+
fn test_not_operator_returns_boolean_union() {
|
|
217
|
+
let source = r#"
|
|
218
|
+
class Foo
|
|
219
|
+
def bar
|
|
220
|
+
!true
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
"#;
|
|
224
|
+
let genv = analyze(source);
|
|
225
|
+
let info = genv
|
|
226
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
227
|
+
.expect("bar should be registered");
|
|
228
|
+
let ty = get_type_show(&genv, info.return_vertex.unwrap());
|
|
229
|
+
assert!(ty.contains("TrueClass"), "expected TrueClass in {}", ty);
|
|
230
|
+
assert!(ty.contains("FalseClass"), "expected FalseClass in {}", ty);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[test]
|
|
234
|
+
fn test_not_operator_receiver_side_effects_analyzed() {
|
|
235
|
+
let source = r#"
|
|
236
|
+
class Foo
|
|
237
|
+
def bar
|
|
238
|
+
!(1.upcase)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
"#;
|
|
242
|
+
let genv = analyze(source);
|
|
243
|
+
assert!(
|
|
244
|
+
!genv.type_errors.is_empty(),
|
|
245
|
+
"expected type error for Integer#upcase"
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#[test]
|
|
250
|
+
fn test_double_not_operator_union() {
|
|
251
|
+
let source = r#"
|
|
252
|
+
class Foo
|
|
253
|
+
def bar
|
|
254
|
+
!!true
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
"#;
|
|
258
|
+
let genv = analyze(source);
|
|
259
|
+
let info = genv
|
|
260
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
261
|
+
.expect("bar should be registered");
|
|
262
|
+
let ty = get_type_show(&genv, info.return_vertex.unwrap());
|
|
263
|
+
assert!(ty.contains("TrueClass"), "expected TrueClass in {}", ty);
|
|
264
|
+
assert!(ty.contains("FalseClass"), "expected FalseClass in {}", ty);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#[test]
|
|
268
|
+
fn test_not_nil_returns_boolean() {
|
|
269
|
+
let source = r#"
|
|
270
|
+
class Foo
|
|
271
|
+
def bar
|
|
272
|
+
!nil
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
"#;
|
|
276
|
+
let genv = analyze(source);
|
|
277
|
+
let info = genv
|
|
278
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
279
|
+
.expect("bar should be registered");
|
|
280
|
+
let ty = get_type_show(&genv, info.return_vertex.unwrap());
|
|
281
|
+
assert!(ty.contains("TrueClass"), "expected TrueClass in {}", ty);
|
|
282
|
+
assert!(ty.contains("FalseClass"), "expected FalseClass in {}", ty);
|
|
283
|
+
}
|
|
192
284
|
}
|
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
//! - Creating vertices for parameters
|
|
6
6
|
//! - Registering parameters as local variables in method scope
|
|
7
7
|
|
|
8
|
+
use std::collections::HashMap;
|
|
9
|
+
|
|
8
10
|
use crate::env::{GlobalEnv, LocalEnv};
|
|
9
11
|
use crate::graph::{ChangeSet, VertexId};
|
|
10
12
|
use crate::types::Type;
|
|
11
13
|
|
|
14
|
+
use super::bytes_to_name;
|
|
15
|
+
|
|
12
16
|
/// Install a required parameter as a local variable
|
|
13
17
|
///
|
|
14
18
|
/// Required parameters start with Bot (untyped) type since we don't know
|
|
@@ -115,21 +119,23 @@ pub fn install_keyword_rest_parameter(
|
|
|
115
119
|
|
|
116
120
|
/// Install method parameters as local variables
|
|
117
121
|
///
|
|
118
|
-
/// Returns a
|
|
119
|
-
///
|
|
122
|
+
/// Returns a tuple of:
|
|
123
|
+
/// - Vec<VertexId>: positional parameter vertices (required and optional)
|
|
124
|
+
/// - HashMap<String, VertexId>: keyword parameter vertices (name → vertex)
|
|
120
125
|
pub(crate) fn install_parameters(
|
|
121
126
|
genv: &mut GlobalEnv,
|
|
122
127
|
lenv: &mut LocalEnv,
|
|
123
128
|
changes: &mut ChangeSet,
|
|
124
129
|
source: &str,
|
|
125
130
|
params_node: &ruby_prism::ParametersNode,
|
|
126
|
-
) -> Vec<VertexId> {
|
|
131
|
+
) -> (Vec<VertexId>, HashMap<String, VertexId>) {
|
|
127
132
|
let mut param_vtxs = Vec::new();
|
|
133
|
+
let mut keyword_param_vtxs: HashMap<String, VertexId> = HashMap::new();
|
|
128
134
|
|
|
129
135
|
// Required parameters: def foo(a, b)
|
|
130
136
|
for node in params_node.requireds().iter() {
|
|
131
137
|
if let Some(req_param) = node.as_required_parameter_node() {
|
|
132
|
-
let name =
|
|
138
|
+
let name = bytes_to_name(req_param.name().as_slice());
|
|
133
139
|
let vtx = install_required_parameter(genv, lenv, name);
|
|
134
140
|
param_vtxs.push(vtx);
|
|
135
141
|
}
|
|
@@ -138,7 +144,7 @@ pub(crate) fn install_parameters(
|
|
|
138
144
|
// Optional parameters: def foo(a = 1, b = "hello")
|
|
139
145
|
for node in params_node.optionals().iter() {
|
|
140
146
|
if let Some(opt_param) = node.as_optional_parameter_node() {
|
|
141
|
-
let name =
|
|
147
|
+
let name = bytes_to_name(opt_param.name().as_slice());
|
|
142
148
|
let default_value = opt_param.value();
|
|
143
149
|
|
|
144
150
|
let vtx = if let Some(default_vtx) =
|
|
@@ -157,24 +163,46 @@ pub(crate) fn install_parameters(
|
|
|
157
163
|
if let Some(rest_node) = params_node.rest() {
|
|
158
164
|
if let Some(rest_param) = rest_node.as_rest_parameter_node() {
|
|
159
165
|
if let Some(name_id) = rest_param.name() {
|
|
160
|
-
let name =
|
|
166
|
+
let name = bytes_to_name(name_id.as_slice());
|
|
161
167
|
install_rest_parameter(genv, lenv, name);
|
|
162
168
|
}
|
|
163
169
|
}
|
|
164
170
|
}
|
|
165
171
|
|
|
172
|
+
// Keyword parameters: def foo(name:, age: 0)
|
|
173
|
+
// Reuses install_required_parameter / install_optional_parameter
|
|
174
|
+
// since the logic is identical for positional and keyword parameters.
|
|
175
|
+
for node in params_node.keywords().iter() {
|
|
176
|
+
if let Some(req_kw) = node.as_required_keyword_parameter_node() {
|
|
177
|
+
let name = bytes_to_name(req_kw.name().as_slice());
|
|
178
|
+
let vtx = install_required_parameter(genv, lenv, name.clone());
|
|
179
|
+
keyword_param_vtxs.insert(name, vtx);
|
|
180
|
+
} else if let Some(opt_kw) = node.as_optional_keyword_parameter_node() {
|
|
181
|
+
let name = bytes_to_name(opt_kw.name().as_slice());
|
|
182
|
+
let default_value = opt_kw.value();
|
|
183
|
+
let vtx = if let Some(default_vtx) =
|
|
184
|
+
super::install::install_node(genv, lenv, changes, source, &default_value)
|
|
185
|
+
{
|
|
186
|
+
install_optional_parameter(genv, lenv, changes, name.clone(), default_vtx)
|
|
187
|
+
} else {
|
|
188
|
+
install_required_parameter(genv, lenv, name.clone())
|
|
189
|
+
};
|
|
190
|
+
keyword_param_vtxs.insert(name, vtx);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
166
194
|
// Keyword rest parameter: def foo(**kwargs)
|
|
167
|
-
// Not included in
|
|
195
|
+
// Not included in keyword_param_vtxs (collects all remaining keywords)
|
|
168
196
|
if let Some(kwrest_node) = params_node.keyword_rest() {
|
|
169
197
|
if let Some(kwrest_param) = kwrest_node.as_keyword_rest_parameter_node() {
|
|
170
198
|
if let Some(name_id) = kwrest_param.name() {
|
|
171
|
-
let name =
|
|
199
|
+
let name = bytes_to_name(name_id.as_slice());
|
|
172
200
|
install_keyword_rest_parameter(genv, lenv, name);
|
|
173
201
|
}
|
|
174
202
|
}
|
|
175
203
|
}
|
|
176
204
|
|
|
177
|
-
param_vtxs
|
|
205
|
+
(param_vtxs, keyword_param_vtxs)
|
|
178
206
|
}
|
|
179
207
|
|
|
180
208
|
#[cfg(test)]
|
|
@@ -215,4 +243,27 @@ mod tests {
|
|
|
215
243
|
assert_ne!(vtx_b, vtx_c);
|
|
216
244
|
assert_ne!(vtx_a, vtx_c);
|
|
217
245
|
}
|
|
246
|
+
|
|
247
|
+
#[test]
|
|
248
|
+
fn test_install_optional_parameter_inherits_default_type() {
|
|
249
|
+
let mut genv = GlobalEnv::new();
|
|
250
|
+
let mut lenv = LocalEnv::new();
|
|
251
|
+
let mut changes = ChangeSet::new();
|
|
252
|
+
|
|
253
|
+
// Default value: 0 (Integer)
|
|
254
|
+
let default_vtx = genv.new_source(Type::integer());
|
|
255
|
+
let vtx = install_optional_parameter(
|
|
256
|
+
&mut genv,
|
|
257
|
+
&mut lenv,
|
|
258
|
+
&mut changes,
|
|
259
|
+
"age".to_string(),
|
|
260
|
+
default_vtx,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
assert_eq!(lenv.get_var("age"), Some(vtx));
|
|
264
|
+
|
|
265
|
+
// Type should propagate from default value
|
|
266
|
+
let vertex = genv.get_vertex(vtx).unwrap();
|
|
267
|
+
assert_eq!(vertex.show(), "Integer");
|
|
268
|
+
}
|
|
218
269
|
}
|
data/rust/src/cache/rbs_cache.rs
CHANGED
data/rust/src/cli/commands.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//! CLI command implementations
|
|
2
2
|
|
|
3
3
|
use anyhow::Result;
|
|
4
|
-
use std::path::
|
|
4
|
+
use std::path::Path;
|
|
5
5
|
|
|
6
6
|
use crate::cache::RbsCache;
|
|
7
7
|
use crate::checker::FileChecker;
|
|
@@ -9,7 +9,7 @@ use crate::diagnostics;
|
|
|
9
9
|
|
|
10
10
|
/// Check a single Ruby file for type errors
|
|
11
11
|
/// Returns Ok(true) if no errors, Ok(false) if errors found
|
|
12
|
-
pub fn check_single_file(file_path: &
|
|
12
|
+
pub fn check_single_file(file_path: &Path, verbose: bool) -> Result<bool> {
|
|
13
13
|
let checker = FileChecker::new()?;
|
|
14
14
|
let diagnostics = checker.check_file(file_path)?;
|
|
15
15
|
|
|
@@ -38,7 +38,7 @@ pub fn check_project(_verbose: bool) -> Result<()> {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/// Watch a file for changes and re-check on modifications
|
|
41
|
-
pub fn watch_file(file_path: &
|
|
41
|
+
pub fn watch_file(file_path: &Path) -> Result<()> {
|
|
42
42
|
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
|
|
43
43
|
use std::sync::mpsc::channel;
|
|
44
44
|
use std::time::Duration;
|
|
@@ -2,7 +2,6 @@ use std::path::PathBuf;
|
|
|
2
2
|
|
|
3
3
|
/// Diagnostic severity level (LSP compatible)
|
|
4
4
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
5
|
-
#[allow(dead_code)]
|
|
6
5
|
pub enum DiagnosticLevel {
|
|
7
6
|
Error,
|
|
8
7
|
Warning,
|
|
@@ -28,7 +27,6 @@ pub struct Location {
|
|
|
28
27
|
|
|
29
28
|
/// Type checking diagnostic
|
|
30
29
|
#[derive(Debug, Clone)]
|
|
31
|
-
#[allow(dead_code)]
|
|
32
30
|
pub struct Diagnostic {
|
|
33
31
|
pub location: Location,
|
|
34
32
|
pub level: DiagnosticLevel,
|
|
@@ -36,7 +34,6 @@ pub struct Diagnostic {
|
|
|
36
34
|
pub code: Option<String>, // e.g., "E001"
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
#[allow(dead_code)]
|
|
40
37
|
impl Diagnostic {
|
|
41
38
|
/// Create an error diagnostic
|
|
42
39
|
pub fn error(location: Location, message: String) -> Self {
|
data/rust/src/env/box_manager.rs
CHANGED
|
@@ -6,7 +6,6 @@ use crate::graph::{BoxId, BoxTrait};
|
|
|
6
6
|
use std::collections::{HashMap, HashSet, VecDeque};
|
|
7
7
|
|
|
8
8
|
/// Manages boxes and their execution queue
|
|
9
|
-
#[allow(dead_code)]
|
|
10
9
|
pub struct BoxManager {
|
|
11
10
|
/// All registered boxes
|
|
12
11
|
pub boxes: HashMap<BoxId, Box<dyn BoxTrait>>,
|
|
@@ -24,7 +23,6 @@ impl Default for BoxManager {
|
|
|
24
23
|
}
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
#[allow(dead_code)]
|
|
28
26
|
impl BoxManager {
|
|
29
27
|
/// Create a new empty box manager
|
|
30
28
|
pub fn new() -> Self {
|
|
@@ -37,8 +35,8 @@ impl BoxManager {
|
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
/// Get a box by ID
|
|
40
|
-
pub fn get(&self, id: BoxId) -> Option<&
|
|
41
|
-
self.boxes.get(&id)
|
|
38
|
+
pub fn get(&self, id: BoxId) -> Option<&dyn BoxTrait> {
|
|
39
|
+
self.boxes.get(&id).map(|b| b.as_ref())
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
/// Remove a box and return it (for temporary mutation)
|