method-ray 0.1.8 → 0.1.9
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 +18 -0
- data/{rust → core}/Cargo.toml +1 -1
- data/core/src/analyzer/assignments.rs +499 -0
- data/{rust → core}/src/analyzer/blocks.rs +140 -0
- data/{rust → core}/src/analyzer/definitions.rs +6 -5
- data/{rust → core}/src/analyzer/dispatch.rs +295 -31
- data/{rust → core}/src/analyzer/exceptions.rs +104 -3
- data/{rust → core}/src/analyzer/install.rs +16 -1
- data/{rust → core}/src/analyzer/literals.rs +3 -17
- data/{rust → core}/src/analyzer/loops.rs +126 -1
- data/{rust → core}/src/analyzer/mod.rs +1 -0
- data/{rust → core}/src/analyzer/parameters.rs +160 -0
- data/core/src/analyzer/super_calls.rs +285 -0
- data/{rust → core}/src/env/global_env.rs +18 -4
- data/{rust → core}/src/env/method_registry.rs +109 -23
- data/{rust → core}/src/env/scope.rs +78 -0
- data/{rust → core}/src/types.rs +11 -0
- data/ext/Cargo.toml +2 -2
- data/lib/methodray/binary_locator.rb +2 -2
- data/lib/methodray/commands.rb +1 -1
- data/lib/methodray/version.rb +1 -1
- metadata +54 -53
- data/rust/src/analyzer/assignments.rs +0 -152
- /data/{rust → core}/src/analyzer/attributes.rs +0 -0
- /data/{rust → core}/src/analyzer/calls.rs +0 -0
- /data/{rust → core}/src/analyzer/conditionals.rs +0 -0
- /data/{rust → core}/src/analyzer/operators.rs +0 -0
- /data/{rust → core}/src/analyzer/parentheses.rs +0 -0
- /data/{rust → core}/src/analyzer/returns.rs +0 -0
- /data/{rust → core}/src/analyzer/variables.rs +0 -0
- /data/{rust → core}/src/cache/mod.rs +0 -0
- /data/{rust → core}/src/cache/rbs_cache.rs +0 -0
- /data/{rust → core}/src/checker.rs +0 -0
- /data/{rust → core}/src/cli/args.rs +0 -0
- /data/{rust → core}/src/cli/commands.rs +0 -0
- /data/{rust → core}/src/cli/mod.rs +0 -0
- /data/{rust → core}/src/diagnostics/diagnostic.rs +0 -0
- /data/{rust → core}/src/diagnostics/formatter.rs +0 -0
- /data/{rust → core}/src/diagnostics/mod.rs +0 -0
- /data/{rust → core}/src/env/box_manager.rs +0 -0
- /data/{rust → core}/src/env/local_env.rs +0 -0
- /data/{rust → core}/src/env/mod.rs +0 -0
- /data/{rust → core}/src/env/type_error.rs +0 -0
- /data/{rust → core}/src/env/vertex_manager.rs +0 -0
- /data/{rust → core}/src/graph/box.rs +0 -0
- /data/{rust → core}/src/graph/change_set.rs +0 -0
- /data/{rust → core}/src/graph/mod.rs +0 -0
- /data/{rust → core}/src/graph/vertex.rs +0 -0
- /data/{rust → core}/src/lib.rs +0 -0
- /data/{rust → core}/src/lsp/diagnostics.rs +0 -0
- /data/{rust → core}/src/lsp/main.rs +0 -0
- /data/{rust → core}/src/lsp/mod.rs +0 -0
- /data/{rust → core}/src/lsp/server.rs +0 -0
- /data/{rust → core}/src/main.rs +0 -0
- /data/{rust → core}/src/parser.rs +0 -0
- /data/{rust → core}/src/rbs/converter.rs +0 -0
- /data/{rust → core}/src/rbs/error.rs +0 -0
- /data/{rust → core}/src/rbs/loader.rs +0 -0
- /data/{rust → core}/src/rbs/mod.rs +0 -0
- /data/{rust → core}/src/source_map.rs +0 -0
|
@@ -128,6 +128,55 @@ fn install_block_parameter(genv: &mut GlobalEnv, lenv: &mut LocalEnv, name: Stri
|
|
|
128
128
|
#[cfg(test)]
|
|
129
129
|
mod tests {
|
|
130
130
|
use super::*;
|
|
131
|
+
use crate::analyzer::install::AstInstaller;
|
|
132
|
+
use crate::env::LocalEnv;
|
|
133
|
+
use crate::parser::ParseSession;
|
|
134
|
+
use crate::types::Type;
|
|
135
|
+
|
|
136
|
+
fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
|
|
137
|
+
if let Some(vertex) = genv.get_vertex(vtx) {
|
|
138
|
+
vertex.show()
|
|
139
|
+
} else if let Some(source) = genv.get_source(vtx) {
|
|
140
|
+
source.ty.show()
|
|
141
|
+
} else {
|
|
142
|
+
panic!("vertex {:?} not found as either Vertex or Source", vtx);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn analyze_with_stdlib(source: &str) -> GlobalEnv {
|
|
147
|
+
let session = ParseSession::new();
|
|
148
|
+
let parse_result = session.parse_source(source, "test.rb").unwrap();
|
|
149
|
+
let root = parse_result.node();
|
|
150
|
+
let program = root.as_program_node().unwrap();
|
|
151
|
+
|
|
152
|
+
let mut genv = GlobalEnv::new();
|
|
153
|
+
|
|
154
|
+
// Register stdlib methods needed for block tests
|
|
155
|
+
genv.register_builtin_method_with_block(
|
|
156
|
+
Type::array(),
|
|
157
|
+
"each",
|
|
158
|
+
Type::array(),
|
|
159
|
+
Some(vec![Type::instance("Elem")]),
|
|
160
|
+
);
|
|
161
|
+
genv.register_builtin_method_with_block(
|
|
162
|
+
Type::string(),
|
|
163
|
+
"each_char",
|
|
164
|
+
Type::string(),
|
|
165
|
+
Some(vec![Type::string()]),
|
|
166
|
+
);
|
|
167
|
+
genv.register_builtin_method(Type::integer(), "even?", Type::instance("TrueClass"));
|
|
168
|
+
genv.register_builtin_method(Type::string(), "upcase", Type::string());
|
|
169
|
+
|
|
170
|
+
let mut lenv = LocalEnv::new();
|
|
171
|
+
|
|
172
|
+
let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
|
|
173
|
+
for stmt in &program.statements().body() {
|
|
174
|
+
installer.install_node(&stmt);
|
|
175
|
+
}
|
|
176
|
+
installer.finish();
|
|
177
|
+
|
|
178
|
+
genv
|
|
179
|
+
}
|
|
131
180
|
|
|
132
181
|
#[test]
|
|
133
182
|
fn test_enter_exit_block_scope() {
|
|
@@ -173,4 +222,95 @@ mod tests {
|
|
|
173
222
|
|
|
174
223
|
exit_block_scope(&mut genv);
|
|
175
224
|
}
|
|
225
|
+
|
|
226
|
+
#[test]
|
|
227
|
+
fn test_block_parameter_type_from_array() {
|
|
228
|
+
let source = r#"
|
|
229
|
+
class Foo
|
|
230
|
+
def bar
|
|
231
|
+
[1, 2, 3].each { |x| x.even? }
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
"#;
|
|
235
|
+
let genv = analyze_with_stdlib(source);
|
|
236
|
+
assert!(
|
|
237
|
+
genv.type_errors.is_empty(),
|
|
238
|
+
"x.even? should not produce type errors: {:?}",
|
|
239
|
+
genv.type_errors
|
|
240
|
+
);
|
|
241
|
+
// Verify bar returns Array (each returns its receiver)
|
|
242
|
+
let info = genv.resolve_method(&Type::instance("Foo"), "bar").unwrap();
|
|
243
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
244
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "Array");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[test]
|
|
248
|
+
fn test_block_external_variable_access() {
|
|
249
|
+
let source = r#"
|
|
250
|
+
class Foo
|
|
251
|
+
def bar
|
|
252
|
+
y = "hello"
|
|
253
|
+
[1].each { y.upcase }
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
"#;
|
|
257
|
+
let genv = analyze_with_stdlib(source);
|
|
258
|
+
assert!(
|
|
259
|
+
genv.type_errors.is_empty(),
|
|
260
|
+
"y.upcase should not produce type errors: {:?}",
|
|
261
|
+
genv.type_errors
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#[test]
|
|
266
|
+
fn test_block_parameter_from_each_char() {
|
|
267
|
+
let source = r#"
|
|
268
|
+
class Foo
|
|
269
|
+
def bar
|
|
270
|
+
"hello".each_char { |c| c.upcase }
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
"#;
|
|
274
|
+
let genv = analyze_with_stdlib(source);
|
|
275
|
+
assert!(
|
|
276
|
+
genv.type_errors.is_empty(),
|
|
277
|
+
"c.upcase should not produce type errors: {:?}",
|
|
278
|
+
genv.type_errors
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#[test]
|
|
283
|
+
fn test_block_body_does_not_affect_method_return() {
|
|
284
|
+
let source = r#"
|
|
285
|
+
class Foo
|
|
286
|
+
def bar
|
|
287
|
+
[1, 2].each { |x| "string" }
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
"#;
|
|
291
|
+
let genv = analyze_with_stdlib(source);
|
|
292
|
+
// each returns its receiver (Array), not the block body result (String)
|
|
293
|
+
let info = genv.resolve_method(&Type::instance("Foo"), "bar").unwrap();
|
|
294
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
295
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "Array");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#[test]
|
|
299
|
+
fn test_nested_blocks() {
|
|
300
|
+
let source = r#"
|
|
301
|
+
class Foo
|
|
302
|
+
def bar
|
|
303
|
+
[1, 2].each { |x|
|
|
304
|
+
"hello".each_char { |c| c.upcase }
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
"#;
|
|
309
|
+
let genv = analyze_with_stdlib(source);
|
|
310
|
+
assert!(
|
|
311
|
+
genv.type_errors.is_empty(),
|
|
312
|
+
"nested block should not produce type errors: {:?}",
|
|
313
|
+
genv.type_errors
|
|
314
|
+
);
|
|
315
|
+
}
|
|
176
316
|
}
|
|
@@ -26,7 +26,8 @@ pub(crate) fn process_class_node(
|
|
|
26
26
|
class_node: &ruby_prism::ClassNode,
|
|
27
27
|
) -> Option<VertexId> {
|
|
28
28
|
let class_name = extract_class_name(class_node);
|
|
29
|
-
|
|
29
|
+
let superclass = class_node.superclass().and_then(|sup| extract_constant_path(&sup));
|
|
30
|
+
install_class(genv, class_name, superclass.as_deref());
|
|
30
31
|
|
|
31
32
|
if let Some(body) = class_node.body() {
|
|
32
33
|
if let Some(statements) = body.as_statements_node() {
|
|
@@ -126,8 +127,8 @@ pub(crate) fn process_def_node(
|
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
/// Install class definition
|
|
129
|
-
fn install_class(genv: &mut GlobalEnv, class_name: String) {
|
|
130
|
-
genv.enter_class(class_name);
|
|
130
|
+
fn install_class(genv: &mut GlobalEnv, class_name: String, superclass: Option<&str>) {
|
|
131
|
+
genv.enter_class(class_name, superclass);
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
/// Install module definition
|
|
@@ -203,7 +204,7 @@ mod tests {
|
|
|
203
204
|
fn test_enter_exit_class_scope() {
|
|
204
205
|
let mut genv = GlobalEnv::new();
|
|
205
206
|
|
|
206
|
-
install_class(&mut genv, "User".to_string());
|
|
207
|
+
install_class(&mut genv, "User".to_string(), None);
|
|
207
208
|
assert_eq!(
|
|
208
209
|
genv.scope_manager.current_class_name(),
|
|
209
210
|
Some("User".to_string())
|
|
@@ -231,7 +232,7 @@ mod tests {
|
|
|
231
232
|
fn test_nested_method_scope() {
|
|
232
233
|
let mut genv = GlobalEnv::new();
|
|
233
234
|
|
|
234
|
-
install_class(&mut genv, "User".to_string());
|
|
235
|
+
install_class(&mut genv, "User".to_string(), None);
|
|
235
236
|
install_method(&mut genv, "greet".to_string());
|
|
236
237
|
|
|
237
238
|
// Still in User class context
|
|
@@ -18,6 +18,45 @@ use super::variables::{
|
|
|
18
18
|
install_self,
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
/// Collect positional and keyword arguments from AST argument nodes.
|
|
22
|
+
///
|
|
23
|
+
/// Shared by method calls (`dispatch.rs`) and super calls (`super_calls.rs`).
|
|
24
|
+
pub(crate) fn collect_arguments<'a>(
|
|
25
|
+
genv: &mut GlobalEnv,
|
|
26
|
+
lenv: &mut LocalEnv,
|
|
27
|
+
changes: &mut ChangeSet,
|
|
28
|
+
source: &str,
|
|
29
|
+
args: impl Iterator<Item = ruby_prism::Node<'a>>,
|
|
30
|
+
) -> (Vec<VertexId>, Option<HashMap<String, VertexId>>) {
|
|
31
|
+
let mut positional: Vec<VertexId> = Vec::new();
|
|
32
|
+
let mut keyword: HashMap<String, VertexId> = HashMap::new();
|
|
33
|
+
|
|
34
|
+
for arg in args {
|
|
35
|
+
if let Some(kw_hash) = arg.as_keyword_hash_node() {
|
|
36
|
+
for element in kw_hash.elements().iter() {
|
|
37
|
+
let assoc = match element.as_assoc_node() {
|
|
38
|
+
Some(a) => a,
|
|
39
|
+
None => continue,
|
|
40
|
+
};
|
|
41
|
+
let name = match assoc.key().as_symbol_node() {
|
|
42
|
+
Some(sym) => bytes_to_name(sym.unescaped()),
|
|
43
|
+
None => continue,
|
|
44
|
+
};
|
|
45
|
+
if let Some(vtx) =
|
|
46
|
+
super::install::install_node(genv, lenv, changes, source, &assoc.value())
|
|
47
|
+
{
|
|
48
|
+
keyword.insert(name, vtx);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} else if let Some(vtx) = super::install::install_node(genv, lenv, changes, source, &arg) {
|
|
52
|
+
positional.push(vtx);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let kw = (!keyword.is_empty()).then_some(keyword);
|
|
57
|
+
(positional, kw)
|
|
58
|
+
}
|
|
59
|
+
|
|
21
60
|
/// Kind of attr_* declaration
|
|
22
61
|
#[derive(Debug, Clone, Copy)]
|
|
23
62
|
pub enum AttrKind {
|
|
@@ -62,6 +101,10 @@ pub enum NeedsChildKind<'a> {
|
|
|
62
101
|
kind: AttrKind,
|
|
63
102
|
attr_names: Vec<String>,
|
|
64
103
|
},
|
|
104
|
+
/// include declaration: `include Greetable, Enumerable`
|
|
105
|
+
IncludeDeclaration {
|
|
106
|
+
module_names: Vec<String>,
|
|
107
|
+
},
|
|
65
108
|
}
|
|
66
109
|
|
|
67
110
|
/// First pass: check if node can be handled immediately without child processing
|
|
@@ -189,6 +232,25 @@ pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsCh
|
|
|
189
232
|
return None;
|
|
190
233
|
}
|
|
191
234
|
|
|
235
|
+
if method_name == "include" {
|
|
236
|
+
let module_names: Vec<String> = call_node
|
|
237
|
+
.arguments()
|
|
238
|
+
.map(|args| {
|
|
239
|
+
args.arguments()
|
|
240
|
+
.iter()
|
|
241
|
+
.filter_map(|arg| {
|
|
242
|
+
super::definitions::extract_constant_path(&arg)
|
|
243
|
+
})
|
|
244
|
+
.collect()
|
|
245
|
+
})
|
|
246
|
+
.unwrap_or_default();
|
|
247
|
+
|
|
248
|
+
if !module_names.is_empty() {
|
|
249
|
+
return Some(NeedsChildKind::IncludeDeclaration { module_names });
|
|
250
|
+
}
|
|
251
|
+
return None;
|
|
252
|
+
}
|
|
253
|
+
|
|
192
254
|
let prism_location = call_node
|
|
193
255
|
.message_loc()
|
|
194
256
|
.unwrap_or_else(|| node.location());
|
|
@@ -262,6 +324,16 @@ pub(crate) fn process_needs_child(
|
|
|
262
324
|
super::attributes::process_attr_declaration(genv, kind, attr_names);
|
|
263
325
|
None
|
|
264
326
|
}
|
|
327
|
+
NeedsChildKind::IncludeDeclaration { module_names } => {
|
|
328
|
+
if let Some(class_name) = genv.scope_manager.current_qualified_name() {
|
|
329
|
+
// Ruby processes `include A, B` right-to-left (B first, then A on top),
|
|
330
|
+
// so A ends up with higher MRO priority. Reverse to match this behavior.
|
|
331
|
+
for module_name in module_names.iter().rev() {
|
|
332
|
+
genv.record_include(&class_name, module_name);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
None
|
|
336
|
+
}
|
|
265
337
|
}
|
|
266
338
|
}
|
|
267
339
|
|
|
@@ -310,31 +382,8 @@ fn process_method_call_common<'a>(
|
|
|
310
382
|
return Some(super::operators::process_not_operator(genv));
|
|
311
383
|
}
|
|
312
384
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
let mut keyword_arg_vtxs: HashMap<String, VertexId> = HashMap::new();
|
|
316
|
-
|
|
317
|
-
for arg in &arguments {
|
|
318
|
-
if let Some(kw_hash) = arg.as_keyword_hash_node() {
|
|
319
|
-
for element in kw_hash.elements().iter() {
|
|
320
|
-
let assoc = match element.as_assoc_node() {
|
|
321
|
-
Some(a) => a,
|
|
322
|
-
None => continue,
|
|
323
|
-
};
|
|
324
|
-
let name = match assoc.key().as_symbol_node() {
|
|
325
|
-
Some(sym) => bytes_to_name(sym.unescaped()),
|
|
326
|
-
None => continue,
|
|
327
|
-
};
|
|
328
|
-
if let Some(vtx) =
|
|
329
|
-
super::install::install_node(genv, lenv, changes, source, &assoc.value())
|
|
330
|
-
{
|
|
331
|
-
keyword_arg_vtxs.insert(name, vtx);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
} else if let Some(vtx) = super::install::install_node(genv, lenv, changes, source, arg) {
|
|
335
|
-
positional_arg_vtxs.push(vtx);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
385
|
+
let (positional_arg_vtxs, kwarg_vtxs) =
|
|
386
|
+
collect_arguments(genv, lenv, changes, source, arguments.into_iter());
|
|
338
387
|
|
|
339
388
|
if let Some(block_node) = block {
|
|
340
389
|
if let Some(block) = block_node.as_block_node() {
|
|
@@ -355,12 +404,6 @@ fn process_method_call_common<'a>(
|
|
|
355
404
|
}
|
|
356
405
|
}
|
|
357
406
|
|
|
358
|
-
let kwarg_vtxs = if keyword_arg_vtxs.is_empty() {
|
|
359
|
-
None
|
|
360
|
-
} else {
|
|
361
|
-
Some(keyword_arg_vtxs)
|
|
362
|
-
};
|
|
363
|
-
|
|
364
407
|
Some(finish_method_call(
|
|
365
408
|
genv,
|
|
366
409
|
recv_vtx,
|
|
@@ -1134,4 +1177,225 @@ User.new.profile(name: "Alice", age: 30)
|
|
|
1134
1177
|
let ret_vtx = info.return_vertex.unwrap();
|
|
1135
1178
|
assert_eq!(get_type_show(&genv, ret_vtx), "String");
|
|
1136
1179
|
}
|
|
1180
|
+
|
|
1181
|
+
// === Include (mixin) tests ===
|
|
1182
|
+
|
|
1183
|
+
// Test 31: Basic include — module method resolved on class instance
|
|
1184
|
+
#[test]
|
|
1185
|
+
fn test_include_basic() {
|
|
1186
|
+
let source = r#"
|
|
1187
|
+
module Greetable
|
|
1188
|
+
def greet
|
|
1189
|
+
"Hello!"
|
|
1190
|
+
end
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
class User
|
|
1194
|
+
include Greetable
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
User.new.greet
|
|
1198
|
+
"#;
|
|
1199
|
+
let genv = analyze(source);
|
|
1200
|
+
assert!(
|
|
1201
|
+
genv.type_errors.is_empty(),
|
|
1202
|
+
"include should resolve Greetable#greet: {:?}",
|
|
1203
|
+
genv.type_errors
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Test 32: Include method return type inference
|
|
1208
|
+
#[test]
|
|
1209
|
+
fn test_include_method_return_type() {
|
|
1210
|
+
let source = r#"
|
|
1211
|
+
module Greetable
|
|
1212
|
+
def greet
|
|
1213
|
+
"Hello!"
|
|
1214
|
+
end
|
|
1215
|
+
end
|
|
1216
|
+
|
|
1217
|
+
class User
|
|
1218
|
+
include Greetable
|
|
1219
|
+
|
|
1220
|
+
def say_hello
|
|
1221
|
+
greet
|
|
1222
|
+
end
|
|
1223
|
+
end
|
|
1224
|
+
"#;
|
|
1225
|
+
let genv = analyze(source);
|
|
1226
|
+
|
|
1227
|
+
let info = genv
|
|
1228
|
+
.resolve_method(&Type::instance("User"), "say_hello")
|
|
1229
|
+
.expect("User#say_hello should be registered");
|
|
1230
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
1231
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "String");
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Test 33: Multiple includes — last included module has priority
|
|
1235
|
+
#[test]
|
|
1236
|
+
fn test_include_multiple_modules() {
|
|
1237
|
+
let source = r#"
|
|
1238
|
+
module A
|
|
1239
|
+
def foo
|
|
1240
|
+
"from A"
|
|
1241
|
+
end
|
|
1242
|
+
end
|
|
1243
|
+
|
|
1244
|
+
module B
|
|
1245
|
+
def foo
|
|
1246
|
+
42
|
|
1247
|
+
end
|
|
1248
|
+
end
|
|
1249
|
+
|
|
1250
|
+
class User
|
|
1251
|
+
include A
|
|
1252
|
+
include B
|
|
1253
|
+
end
|
|
1254
|
+
|
|
1255
|
+
User.new.foo
|
|
1256
|
+
"#;
|
|
1257
|
+
let genv = analyze(source);
|
|
1258
|
+
assert!(genv.type_errors.is_empty());
|
|
1259
|
+
|
|
1260
|
+
// B is included last → B#foo (Integer) should be resolved
|
|
1261
|
+
let info = genv
|
|
1262
|
+
.resolve_method(&Type::instance("User"), "foo")
|
|
1263
|
+
.expect("User#foo should be resolved via include");
|
|
1264
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
1265
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "Integer");
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Test 34: Class's own method takes priority over included module
|
|
1269
|
+
#[test]
|
|
1270
|
+
fn test_include_class_method_priority() {
|
|
1271
|
+
let source = r#"
|
|
1272
|
+
module Greetable
|
|
1273
|
+
def greet
|
|
1274
|
+
"Hello from module!"
|
|
1275
|
+
end
|
|
1276
|
+
end
|
|
1277
|
+
|
|
1278
|
+
class User
|
|
1279
|
+
include Greetable
|
|
1280
|
+
|
|
1281
|
+
def greet
|
|
1282
|
+
42
|
|
1283
|
+
end
|
|
1284
|
+
end
|
|
1285
|
+
|
|
1286
|
+
User.new.greet
|
|
1287
|
+
"#;
|
|
1288
|
+
let genv = analyze(source);
|
|
1289
|
+
|
|
1290
|
+
let info = genv
|
|
1291
|
+
.resolve_method(&Type::instance("User"), "greet")
|
|
1292
|
+
.expect("User#greet should be resolved");
|
|
1293
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
1294
|
+
// Class's own method (Integer) takes priority
|
|
1295
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "Integer");
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Test 35: Include with qualified module name
|
|
1299
|
+
#[test]
|
|
1300
|
+
fn test_include_qualified_module() {
|
|
1301
|
+
let source = r#"
|
|
1302
|
+
module Api
|
|
1303
|
+
module Helpers
|
|
1304
|
+
def help
|
|
1305
|
+
"help"
|
|
1306
|
+
end
|
|
1307
|
+
end
|
|
1308
|
+
end
|
|
1309
|
+
|
|
1310
|
+
class User
|
|
1311
|
+
include Api::Helpers
|
|
1312
|
+
end
|
|
1313
|
+
|
|
1314
|
+
User.new.help
|
|
1315
|
+
"#;
|
|
1316
|
+
let genv = analyze(source);
|
|
1317
|
+
assert!(
|
|
1318
|
+
genv.type_errors.is_empty(),
|
|
1319
|
+
"include Api::Helpers should resolve: {:?}",
|
|
1320
|
+
genv.type_errors
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Test 36: Include with simultaneous multiple modules
|
|
1325
|
+
#[test]
|
|
1326
|
+
fn test_include_simultaneous_multiple() {
|
|
1327
|
+
let source = r#"
|
|
1328
|
+
module A
|
|
1329
|
+
def a_method
|
|
1330
|
+
"a"
|
|
1331
|
+
end
|
|
1332
|
+
end
|
|
1333
|
+
|
|
1334
|
+
module B
|
|
1335
|
+
def b_method
|
|
1336
|
+
42
|
|
1337
|
+
end
|
|
1338
|
+
end
|
|
1339
|
+
|
|
1340
|
+
class User
|
|
1341
|
+
include A, B
|
|
1342
|
+
end
|
|
1343
|
+
|
|
1344
|
+
User.new.a_method
|
|
1345
|
+
User.new.b_method
|
|
1346
|
+
"#;
|
|
1347
|
+
let genv = analyze(source);
|
|
1348
|
+
assert!(
|
|
1349
|
+
genv.type_errors.is_empty(),
|
|
1350
|
+
"include A, B should resolve both modules: {:?}",
|
|
1351
|
+
genv.type_errors
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Test 37: Include with unknown module (no crash)
|
|
1356
|
+
#[test]
|
|
1357
|
+
fn test_include_unknown_module() {
|
|
1358
|
+
let source = r#"
|
|
1359
|
+
class User
|
|
1360
|
+
include UnknownModule
|
|
1361
|
+
end
|
|
1362
|
+
"#;
|
|
1363
|
+
let genv = analyze(source);
|
|
1364
|
+
// Should not panic; unknown module is recorded but methods won't resolve
|
|
1365
|
+
let _ = genv;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Test 38: Simultaneous include A, B — A has higher MRO priority (Ruby semantics)
|
|
1369
|
+
#[test]
|
|
1370
|
+
fn test_include_simultaneous_order() {
|
|
1371
|
+
let source = r#"
|
|
1372
|
+
module A
|
|
1373
|
+
def foo
|
|
1374
|
+
"from A"
|
|
1375
|
+
end
|
|
1376
|
+
end
|
|
1377
|
+
|
|
1378
|
+
module B
|
|
1379
|
+
def foo
|
|
1380
|
+
42
|
|
1381
|
+
end
|
|
1382
|
+
end
|
|
1383
|
+
|
|
1384
|
+
class User
|
|
1385
|
+
include A, B
|
|
1386
|
+
end
|
|
1387
|
+
|
|
1388
|
+
User.new.foo
|
|
1389
|
+
"#;
|
|
1390
|
+
let genv = analyze(source);
|
|
1391
|
+
assert!(genv.type_errors.is_empty());
|
|
1392
|
+
|
|
1393
|
+
// Ruby's `include A, B` processes right-to-left: B first, then A on top.
|
|
1394
|
+
// A has higher MRO priority → A#foo (String) should be resolved.
|
|
1395
|
+
let info = genv
|
|
1396
|
+
.resolve_method(&Type::instance("User"), "foo")
|
|
1397
|
+
.expect("User#foo should be resolved via include");
|
|
1398
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
1399
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "String");
|
|
1400
|
+
}
|
|
1137
1401
|
}
|
|
@@ -81,6 +81,24 @@ fn process_rescue_chain(
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/// Extract the exception type from rescue node's exception class list.
|
|
85
|
+
/// Falls back to StandardError when no exceptions are specified or none can be resolved.
|
|
86
|
+
// TODO: Non-constant exception expressions (method calls, splats, variables) are silently skipped.
|
|
87
|
+
fn extract_exception_type(rescue_node: &RescueNode) -> Type {
|
|
88
|
+
let types: Vec<Type> = rescue_node
|
|
89
|
+
.exceptions()
|
|
90
|
+
.iter()
|
|
91
|
+
.filter_map(|exc| super::definitions::extract_constant_path(&exc))
|
|
92
|
+
.map(|name| Type::instance(&name))
|
|
93
|
+
.collect();
|
|
94
|
+
|
|
95
|
+
if types.is_empty() {
|
|
96
|
+
Type::instance("StandardError")
|
|
97
|
+
} else {
|
|
98
|
+
Type::union_of(types)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
/// Process a single RescueNode body.
|
|
85
103
|
/// Registers the rescue variable (=> e), processes the body,
|
|
86
104
|
/// then removes the variable from scope.
|
|
@@ -97,14 +115,14 @@ fn process_rescue_body(
|
|
|
97
115
|
|
|
98
116
|
// Save/restore rescue variable binding (=> e)
|
|
99
117
|
// TODO: Only LocalVariableTargetNode is handled; instance/global/class vars are not yet supported.
|
|
100
|
-
// TODO: Always typed as StandardError regardless of declared exception class.
|
|
101
118
|
let var_binding = if let Some(ref_node) = rescue_node.reference() {
|
|
102
119
|
ref_node.as_local_variable_target_node().map(|target| {
|
|
103
120
|
let name = bytes_to_name(target.name().as_slice());
|
|
104
121
|
let saved = lenv.get_var(&name);
|
|
105
122
|
let exception_vtx = genv.new_vertex();
|
|
106
|
-
let
|
|
107
|
-
genv.
|
|
123
|
+
let exception_type = extract_exception_type(rescue_node);
|
|
124
|
+
let exception_src = genv.new_source(exception_type);
|
|
125
|
+
genv.add_edge(exception_src, exception_vtx);
|
|
108
126
|
lenv.new_var(name.clone(), exception_vtx);
|
|
109
127
|
(name, saved)
|
|
110
128
|
})
|
|
@@ -497,6 +515,89 @@ end
|
|
|
497
515
|
);
|
|
498
516
|
}
|
|
499
517
|
|
|
518
|
+
#[test]
|
|
519
|
+
fn test_rescue_specific_exception_class() {
|
|
520
|
+
let source = r#"
|
|
521
|
+
class Foo
|
|
522
|
+
def bar
|
|
523
|
+
begin
|
|
524
|
+
"hello"
|
|
525
|
+
rescue ArgumentError => e
|
|
526
|
+
e
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
"#;
|
|
531
|
+
let genv = analyze(source);
|
|
532
|
+
let info = genv
|
|
533
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
534
|
+
.expect("Foo#bar should be registered");
|
|
535
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
536
|
+
let type_str = get_type_show(&genv, ret_vtx);
|
|
537
|
+
assert!(
|
|
538
|
+
type_str.contains("ArgumentError"),
|
|
539
|
+
"should contain ArgumentError: {}",
|
|
540
|
+
type_str
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#[test]
|
|
545
|
+
fn test_rescue_multiple_exception_classes() {
|
|
546
|
+
let source = r#"
|
|
547
|
+
class Foo
|
|
548
|
+
def bar
|
|
549
|
+
begin
|
|
550
|
+
"hello"
|
|
551
|
+
rescue TypeError, NameError => e
|
|
552
|
+
e
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
"#;
|
|
557
|
+
let genv = analyze(source);
|
|
558
|
+
let info = genv
|
|
559
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
560
|
+
.expect("Foo#bar should be registered");
|
|
561
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
562
|
+
let type_str = get_type_show(&genv, ret_vtx);
|
|
563
|
+
assert!(
|
|
564
|
+
type_str.contains("TypeError"),
|
|
565
|
+
"should contain TypeError: {}",
|
|
566
|
+
type_str
|
|
567
|
+
);
|
|
568
|
+
assert!(
|
|
569
|
+
type_str.contains("NameError"),
|
|
570
|
+
"should contain NameError: {}",
|
|
571
|
+
type_str
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
#[test]
|
|
576
|
+
fn test_rescue_qualified_exception_class() {
|
|
577
|
+
let source = r#"
|
|
578
|
+
class Foo
|
|
579
|
+
def bar
|
|
580
|
+
begin
|
|
581
|
+
"hello"
|
|
582
|
+
rescue Net::HTTPError => e
|
|
583
|
+
e
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
"#;
|
|
588
|
+
let genv = analyze(source);
|
|
589
|
+
let info = genv
|
|
590
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
591
|
+
.expect("Foo#bar should be registered");
|
|
592
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
593
|
+
let type_str = get_type_show(&genv, ret_vtx);
|
|
594
|
+
assert!(
|
|
595
|
+
type_str.contains("Net::HTTPError"),
|
|
596
|
+
"should contain Net::HTTPError: {}",
|
|
597
|
+
type_str
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
500
601
|
#[test]
|
|
501
602
|
fn test_empty_rescue_body_is_nil() {
|
|
502
603
|
let source = r#"
|