method-ray 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/ext/Cargo.toml +1 -1
- data/lib/methodray/version.rb +1 -1
- data/rust/Cargo.toml +1 -1
- data/rust/src/analyzer/attributes.rs +57 -0
- data/rust/src/analyzer/blocks.rs +104 -23
- data/rust/src/analyzer/calls.rs +7 -4
- data/rust/src/analyzer/conditionals.rs +466 -0
- data/rust/src/analyzer/definitions.rs +126 -8
- data/rust/src/analyzer/dispatch.rs +746 -11
- data/rust/src/analyzer/install.rs +52 -870
- data/rust/src/analyzer/literals.rs +179 -30
- data/rust/src/analyzer/mod.rs +2 -0
- data/rust/src/analyzer/parameters.rs +64 -0
- data/rust/src/analyzer/variables.rs +12 -3
- data/rust/src/env/global_env.rs +12 -0
- data/rust/src/env/method_registry.rs +55 -0
- data/rust/src/graph/box.rs +145 -5
- metadata +3 -1
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
//! Conditionals - if/unless/case type inference
|
|
2
|
+
//!
|
|
3
|
+
//! Collects types from each branch and merges them into a Union
|
|
4
|
+
//! via edges into a single result Vertex.
|
|
5
|
+
|
|
6
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
7
|
+
use crate::graph::{ChangeSet, VertexId};
|
|
8
|
+
use crate::types::Type;
|
|
9
|
+
use ruby_prism::{CaseNode, ElseNode, IfNode, Node, UnlessNode, WhenNode};
|
|
10
|
+
|
|
11
|
+
use super::install::{install_node, install_statements};
|
|
12
|
+
|
|
13
|
+
/// Process IfNode: if/elsif/else chain
|
|
14
|
+
pub(crate) fn process_if_node(
|
|
15
|
+
genv: &mut GlobalEnv,
|
|
16
|
+
lenv: &mut LocalEnv,
|
|
17
|
+
changes: &mut ChangeSet,
|
|
18
|
+
source: &str,
|
|
19
|
+
if_node: &IfNode,
|
|
20
|
+
) -> Option<VertexId> {
|
|
21
|
+
// Process predicate for side effects
|
|
22
|
+
install_node(genv, lenv, changes, source, &if_node.predicate());
|
|
23
|
+
|
|
24
|
+
let result_vtx = genv.new_vertex();
|
|
25
|
+
|
|
26
|
+
// then branch
|
|
27
|
+
let vtx_then = if_node
|
|
28
|
+
.statements()
|
|
29
|
+
.and_then(|stmts| install_statements(genv, lenv, changes, source, &stmts));
|
|
30
|
+
if let Some(vtx) = vtx_then {
|
|
31
|
+
genv.add_edge(vtx, result_vtx);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// elsif/else branch (subsequent)
|
|
35
|
+
let has_else = if let Some(subsequent) = if_node.subsequent() {
|
|
36
|
+
let vtx_sub = process_subsequent(genv, lenv, changes, source, &subsequent);
|
|
37
|
+
if let Some(vtx) = vtx_sub {
|
|
38
|
+
genv.add_edge(vtx, result_vtx);
|
|
39
|
+
}
|
|
40
|
+
true
|
|
41
|
+
} else {
|
|
42
|
+
false
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// No else clause → add nil
|
|
46
|
+
if !has_else {
|
|
47
|
+
let nil_vtx = genv.new_source(Type::Nil);
|
|
48
|
+
genv.add_edge(nil_vtx, result_vtx);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Some(result_vtx)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Process UnlessNode: unless/else
|
|
55
|
+
pub(crate) fn process_unless_node(
|
|
56
|
+
genv: &mut GlobalEnv,
|
|
57
|
+
lenv: &mut LocalEnv,
|
|
58
|
+
changes: &mut ChangeSet,
|
|
59
|
+
source: &str,
|
|
60
|
+
unless_node: &UnlessNode,
|
|
61
|
+
) -> Option<VertexId> {
|
|
62
|
+
// Process predicate for side effects
|
|
63
|
+
install_node(genv, lenv, changes, source, &unless_node.predicate());
|
|
64
|
+
|
|
65
|
+
let result_vtx = genv.new_vertex();
|
|
66
|
+
|
|
67
|
+
// body branch
|
|
68
|
+
let vtx_body = unless_node
|
|
69
|
+
.statements()
|
|
70
|
+
.and_then(|stmts| install_statements(genv, lenv, changes, source, &stmts));
|
|
71
|
+
if let Some(vtx) = vtx_body {
|
|
72
|
+
genv.add_edge(vtx, result_vtx);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// else clause
|
|
76
|
+
let has_else = if let Some(else_node) = unless_node.else_clause() {
|
|
77
|
+
let vtx_else = process_else_clause(genv, lenv, changes, source, &else_node);
|
|
78
|
+
if let Some(vtx) = vtx_else {
|
|
79
|
+
genv.add_edge(vtx, result_vtx);
|
|
80
|
+
}
|
|
81
|
+
true
|
|
82
|
+
} else {
|
|
83
|
+
false
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// No else clause → add nil
|
|
87
|
+
if !has_else {
|
|
88
|
+
let nil_vtx = genv.new_source(Type::Nil);
|
|
89
|
+
genv.add_edge(nil_vtx, result_vtx);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Some(result_vtx)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Process CaseNode: case/when/else
|
|
96
|
+
pub(crate) fn process_case_node(
|
|
97
|
+
genv: &mut GlobalEnv,
|
|
98
|
+
lenv: &mut LocalEnv,
|
|
99
|
+
changes: &mut ChangeSet,
|
|
100
|
+
source: &str,
|
|
101
|
+
case_node: &CaseNode,
|
|
102
|
+
) -> Option<VertexId> {
|
|
103
|
+
// Process predicate for side effects
|
|
104
|
+
if let Some(pred) = case_node.predicate() {
|
|
105
|
+
install_node(genv, lenv, changes, source, &pred);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let result_vtx = genv.new_vertex();
|
|
109
|
+
|
|
110
|
+
// Process each when clause
|
|
111
|
+
for condition in &case_node.conditions() {
|
|
112
|
+
if let Some(when_node) = condition.as_when_node() {
|
|
113
|
+
let vtx_when = process_when_clause(genv, lenv, changes, source, &when_node);
|
|
114
|
+
if let Some(vtx) = vtx_when {
|
|
115
|
+
genv.add_edge(vtx, result_vtx);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// else clause
|
|
121
|
+
let has_else = if let Some(else_node) = case_node.else_clause() {
|
|
122
|
+
let vtx_else = process_else_clause(genv, lenv, changes, source, &else_node);
|
|
123
|
+
if let Some(vtx) = vtx_else {
|
|
124
|
+
genv.add_edge(vtx, result_vtx);
|
|
125
|
+
}
|
|
126
|
+
true
|
|
127
|
+
} else {
|
|
128
|
+
false
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// No else clause → add nil
|
|
132
|
+
if !has_else {
|
|
133
|
+
let nil_vtx = genv.new_source(Type::Nil);
|
|
134
|
+
genv.add_edge(nil_vtx, result_vtx);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
Some(result_vtx)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Process subsequent node (elsif chain or else)
|
|
141
|
+
fn process_subsequent(
|
|
142
|
+
genv: &mut GlobalEnv,
|
|
143
|
+
lenv: &mut LocalEnv,
|
|
144
|
+
changes: &mut ChangeSet,
|
|
145
|
+
source: &str,
|
|
146
|
+
node: &Node,
|
|
147
|
+
) -> Option<VertexId> {
|
|
148
|
+
// elsif: subsequent is another IfNode
|
|
149
|
+
if let Some(if_node) = node.as_if_node() {
|
|
150
|
+
return process_if_node(genv, lenv, changes, source, &if_node);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// else: subsequent is an ElseNode
|
|
154
|
+
if let Some(else_node) = node.as_else_node() {
|
|
155
|
+
return process_else_clause(genv, lenv, changes, source, &else_node);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
None
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Process ElseNode
|
|
162
|
+
fn process_else_clause(
|
|
163
|
+
genv: &mut GlobalEnv,
|
|
164
|
+
lenv: &mut LocalEnv,
|
|
165
|
+
changes: &mut ChangeSet,
|
|
166
|
+
source: &str,
|
|
167
|
+
else_node: &ElseNode,
|
|
168
|
+
) -> Option<VertexId> {
|
|
169
|
+
else_node
|
|
170
|
+
.statements()
|
|
171
|
+
.and_then(|stmts| install_statements(genv, lenv, changes, source, &stmts))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Process WhenNode
|
|
175
|
+
fn process_when_clause(
|
|
176
|
+
genv: &mut GlobalEnv,
|
|
177
|
+
lenv: &mut LocalEnv,
|
|
178
|
+
changes: &mut ChangeSet,
|
|
179
|
+
source: &str,
|
|
180
|
+
when_node: &WhenNode,
|
|
181
|
+
) -> Option<VertexId> {
|
|
182
|
+
// Process when conditions for side effects
|
|
183
|
+
for cond in &when_node.conditions() {
|
|
184
|
+
install_node(genv, lenv, changes, source, &cond);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
when_node
|
|
188
|
+
.statements()
|
|
189
|
+
.and_then(|stmts| install_statements(genv, lenv, changes, source, &stmts))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#[cfg(test)]
|
|
193
|
+
mod tests {
|
|
194
|
+
use crate::analyzer::install::AstInstaller;
|
|
195
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
196
|
+
use crate::graph::VertexId;
|
|
197
|
+
use crate::parser::ParseSession;
|
|
198
|
+
use crate::types::Type;
|
|
199
|
+
|
|
200
|
+
/// Helper: parse Ruby source, process with AstInstaller, and return GlobalEnv
|
|
201
|
+
fn analyze(source: &str) -> GlobalEnv {
|
|
202
|
+
let session = ParseSession::new();
|
|
203
|
+
let parse_result = session.parse_source(source, "test.rb").unwrap();
|
|
204
|
+
let root = parse_result.node();
|
|
205
|
+
let program = root.as_program_node().unwrap();
|
|
206
|
+
|
|
207
|
+
let mut genv = GlobalEnv::new();
|
|
208
|
+
let mut lenv = LocalEnv::new();
|
|
209
|
+
|
|
210
|
+
let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
|
|
211
|
+
for stmt in &program.statements().body() {
|
|
212
|
+
installer.install_node(&stmt);
|
|
213
|
+
}
|
|
214
|
+
installer.finish();
|
|
215
|
+
|
|
216
|
+
genv
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Helper: get the type string for a vertex ID (checks both Vertex and Source)
|
|
220
|
+
fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
|
|
221
|
+
if let Some(vertex) = genv.get_vertex(vtx) {
|
|
222
|
+
vertex.show()
|
|
223
|
+
} else if let Some(source) = genv.get_source(vtx) {
|
|
224
|
+
source.ty.show()
|
|
225
|
+
} else {
|
|
226
|
+
panic!("vertex {:?} not found as either Vertex or Source", vtx);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Test 1: if/else basic - different types in branches
|
|
231
|
+
#[test]
|
|
232
|
+
fn test_if_else_basic() {
|
|
233
|
+
let source = r#"
|
|
234
|
+
class Foo
|
|
235
|
+
def bar
|
|
236
|
+
if true
|
|
237
|
+
"hello"
|
|
238
|
+
else
|
|
239
|
+
42
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
"#;
|
|
244
|
+
let genv = analyze(source);
|
|
245
|
+
let info = genv
|
|
246
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
247
|
+
.expect("Foo#bar should be registered");
|
|
248
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
249
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "(Integer | String)");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Test 2: if only (no else) → includes nil
|
|
253
|
+
#[test]
|
|
254
|
+
fn test_if_without_else() {
|
|
255
|
+
let source = r#"
|
|
256
|
+
class Foo
|
|
257
|
+
def bar
|
|
258
|
+
if true
|
|
259
|
+
"hello"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
"#;
|
|
264
|
+
let genv = analyze(source);
|
|
265
|
+
let info = genv
|
|
266
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
267
|
+
.expect("Foo#bar should be registered");
|
|
268
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
269
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "(String | nil)");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Test 3: if/elsif/else chain → 3 types
|
|
273
|
+
#[test]
|
|
274
|
+
fn test_if_elsif_else() {
|
|
275
|
+
let source = r#"
|
|
276
|
+
class Foo
|
|
277
|
+
def bar
|
|
278
|
+
if true
|
|
279
|
+
"hello"
|
|
280
|
+
elsif false
|
|
281
|
+
42
|
|
282
|
+
else
|
|
283
|
+
true
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
"#;
|
|
288
|
+
let genv = analyze(source);
|
|
289
|
+
let info = genv
|
|
290
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
291
|
+
.expect("Foo#bar should be registered");
|
|
292
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
293
|
+
let type_str = get_type_show(&genv, ret_vtx);
|
|
294
|
+
assert!(type_str.contains("Integer"), "should contain Integer: {}", type_str);
|
|
295
|
+
assert!(type_str.contains("String"), "should contain String: {}", type_str);
|
|
296
|
+
assert!(type_str.contains("TrueClass"), "should contain TrueClass: {}", type_str);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Test 4: unless/else
|
|
300
|
+
#[test]
|
|
301
|
+
fn test_unless_else() {
|
|
302
|
+
let source = r#"
|
|
303
|
+
class Foo
|
|
304
|
+
def bar
|
|
305
|
+
unless true
|
|
306
|
+
"a"
|
|
307
|
+
else
|
|
308
|
+
1
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
"#;
|
|
313
|
+
let genv = analyze(source);
|
|
314
|
+
let info = genv
|
|
315
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
316
|
+
.expect("Foo#bar should be registered");
|
|
317
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
318
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "(Integer | String)");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Test 5: unless without else → includes nil
|
|
322
|
+
#[test]
|
|
323
|
+
fn test_unless_without_else() {
|
|
324
|
+
let source = r#"
|
|
325
|
+
class Foo
|
|
326
|
+
def bar
|
|
327
|
+
unless true
|
|
328
|
+
"hello"
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
"#;
|
|
333
|
+
let genv = analyze(source);
|
|
334
|
+
let info = genv
|
|
335
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
336
|
+
.expect("Foo#bar should be registered");
|
|
337
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
338
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "(String | nil)");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Test 6: case/when/else
|
|
342
|
+
#[test]
|
|
343
|
+
fn test_case_when_else() {
|
|
344
|
+
let source = r#"
|
|
345
|
+
class Foo
|
|
346
|
+
def bar
|
|
347
|
+
case :status
|
|
348
|
+
when :active
|
|
349
|
+
"active"
|
|
350
|
+
when :inactive
|
|
351
|
+
"inactive"
|
|
352
|
+
else
|
|
353
|
+
nil
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
"#;
|
|
358
|
+
let genv = analyze(source);
|
|
359
|
+
let info = genv
|
|
360
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
361
|
+
.expect("Foo#bar should be registered");
|
|
362
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
363
|
+
let type_str = get_type_show(&genv, ret_vtx);
|
|
364
|
+
assert!(type_str.contains("String"), "should contain String: {}", type_str);
|
|
365
|
+
assert!(type_str.contains("nil"), "should contain nil: {}", type_str);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Test 7: case without else → includes nil
|
|
369
|
+
#[test]
|
|
370
|
+
fn test_case_without_else() {
|
|
371
|
+
let source = r#"
|
|
372
|
+
class Foo
|
|
373
|
+
def bar
|
|
374
|
+
case :status
|
|
375
|
+
when :active
|
|
376
|
+
"active"
|
|
377
|
+
when :inactive
|
|
378
|
+
42
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
"#;
|
|
383
|
+
let genv = analyze(source);
|
|
384
|
+
let info = genv
|
|
385
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
386
|
+
.expect("Foo#bar should be registered");
|
|
387
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
388
|
+
let type_str = get_type_show(&genv, ret_vtx);
|
|
389
|
+
assert!(type_str.contains("Integer"), "should contain Integer: {}", type_str);
|
|
390
|
+
assert!(type_str.contains("String"), "should contain String: {}", type_str);
|
|
391
|
+
assert!(type_str.contains("nil"), "should contain nil: {}", type_str);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Test 8: conditional inside method → return type reflects union
|
|
395
|
+
#[test]
|
|
396
|
+
fn test_conditional_in_method_return() {
|
|
397
|
+
let source = r#"
|
|
398
|
+
class Converter
|
|
399
|
+
def convert(x)
|
|
400
|
+
if true
|
|
401
|
+
"text"
|
|
402
|
+
else
|
|
403
|
+
100
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
"#;
|
|
408
|
+
let genv = analyze(source);
|
|
409
|
+
let info = genv
|
|
410
|
+
.resolve_method(&Type::instance("Converter"), "convert")
|
|
411
|
+
.expect("Converter#convert should be registered");
|
|
412
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
413
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "(Integer | String)");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Test 9: nested conditionals
|
|
417
|
+
#[test]
|
|
418
|
+
fn test_nested_conditionals() {
|
|
419
|
+
let source = r#"
|
|
420
|
+
class Foo
|
|
421
|
+
def bar
|
|
422
|
+
if true
|
|
423
|
+
if false
|
|
424
|
+
"inner"
|
|
425
|
+
else
|
|
426
|
+
42
|
|
427
|
+
end
|
|
428
|
+
else
|
|
429
|
+
:sym
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
"#;
|
|
434
|
+
let genv = analyze(source);
|
|
435
|
+
let info = genv
|
|
436
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
437
|
+
.expect("Foo#bar should be registered");
|
|
438
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
439
|
+
let type_str = get_type_show(&genv, ret_vtx);
|
|
440
|
+
// Outer if: inner_if_result | Symbol
|
|
441
|
+
// Inner if: (Integer | String) → propagates through
|
|
442
|
+
assert!(type_str.contains("Symbol"), "should contain Symbol: {}", type_str);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Test 10: all branches same type → single type (not union)
|
|
446
|
+
#[test]
|
|
447
|
+
fn test_same_type_branches() {
|
|
448
|
+
let source = r#"
|
|
449
|
+
class Foo
|
|
450
|
+
def bar
|
|
451
|
+
if true
|
|
452
|
+
"hello"
|
|
453
|
+
else
|
|
454
|
+
"world"
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
"#;
|
|
459
|
+
let genv = analyze(source);
|
|
460
|
+
let info = genv
|
|
461
|
+
.resolve_method(&Type::instance("Foo"), "bar")
|
|
462
|
+
.expect("Foo#bar should be registered");
|
|
463
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
464
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "String");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
@@ -6,38 +6,131 @@
|
|
|
6
6
|
//! - Method definition scope management (def baz ... end)
|
|
7
7
|
//! - Extracting class/module names from AST nodes (including qualified names like Api::User)
|
|
8
8
|
|
|
9
|
-
use crate::env::GlobalEnv;
|
|
9
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
10
|
+
use crate::graph::{ChangeSet, VertexId};
|
|
11
|
+
use crate::types::Type;
|
|
10
12
|
use ruby_prism::Node;
|
|
11
13
|
|
|
14
|
+
use super::install::install_statements;
|
|
15
|
+
use super::parameters::install_parameters;
|
|
16
|
+
|
|
17
|
+
/// Process class definition node
|
|
18
|
+
pub(crate) fn process_class_node(
|
|
19
|
+
genv: &mut GlobalEnv,
|
|
20
|
+
lenv: &mut LocalEnv,
|
|
21
|
+
changes: &mut ChangeSet,
|
|
22
|
+
source: &str,
|
|
23
|
+
class_node: &ruby_prism::ClassNode,
|
|
24
|
+
) -> Option<VertexId> {
|
|
25
|
+
let class_name = extract_class_name(class_node);
|
|
26
|
+
install_class(genv, class_name);
|
|
27
|
+
|
|
28
|
+
if let Some(body) = class_node.body() {
|
|
29
|
+
if let Some(statements) = body.as_statements_node() {
|
|
30
|
+
install_statements(genv, lenv, changes, source, &statements);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
exit_scope(genv);
|
|
35
|
+
None
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Process module definition node
|
|
39
|
+
pub(crate) fn process_module_node(
|
|
40
|
+
genv: &mut GlobalEnv,
|
|
41
|
+
lenv: &mut LocalEnv,
|
|
42
|
+
changes: &mut ChangeSet,
|
|
43
|
+
source: &str,
|
|
44
|
+
module_node: &ruby_prism::ModuleNode,
|
|
45
|
+
) -> Option<VertexId> {
|
|
46
|
+
let module_name = extract_module_name(module_node);
|
|
47
|
+
install_module(genv, module_name);
|
|
48
|
+
|
|
49
|
+
if let Some(body) = module_node.body() {
|
|
50
|
+
if let Some(statements) = body.as_statements_node() {
|
|
51
|
+
install_statements(genv, lenv, changes, source, &statements);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
exit_scope(genv);
|
|
56
|
+
None
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Process method definition node
|
|
60
|
+
pub(crate) fn process_def_node(
|
|
61
|
+
genv: &mut GlobalEnv,
|
|
62
|
+
lenv: &mut LocalEnv,
|
|
63
|
+
changes: &mut ChangeSet,
|
|
64
|
+
source: &str,
|
|
65
|
+
def_node: &ruby_prism::DefNode,
|
|
66
|
+
) -> Option<VertexId> {
|
|
67
|
+
let method_name = String::from_utf8_lossy(def_node.name().as_slice()).to_string();
|
|
68
|
+
install_method(genv, method_name.clone());
|
|
69
|
+
|
|
70
|
+
// Process parameters BEFORE processing body
|
|
71
|
+
let param_vtxs = if let Some(params_node) = def_node.parameters() {
|
|
72
|
+
install_parameters(genv, lenv, changes, source, ¶ms_node)
|
|
73
|
+
} else {
|
|
74
|
+
vec![]
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let mut last_vtx = None;
|
|
78
|
+
if let Some(body) = def_node.body() {
|
|
79
|
+
if let Some(statements) = body.as_statements_node() {
|
|
80
|
+
last_vtx = install_statements(genv, lenv, changes, source, &statements);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Register user-defined method with return vertex and param vertices (before exiting scope)
|
|
85
|
+
if let Some(return_vtx) = last_vtx {
|
|
86
|
+
let recv_type_name = genv
|
|
87
|
+
.scope_manager
|
|
88
|
+
.current_class_name()
|
|
89
|
+
.or_else(|| genv.scope_manager.current_module_name());
|
|
90
|
+
|
|
91
|
+
if let Some(name) = recv_type_name {
|
|
92
|
+
genv.register_user_method(
|
|
93
|
+
Type::instance(&name),
|
|
94
|
+
&method_name,
|
|
95
|
+
return_vtx,
|
|
96
|
+
param_vtxs,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
exit_scope(genv);
|
|
102
|
+
None
|
|
103
|
+
}
|
|
104
|
+
|
|
12
105
|
/// Install class definition
|
|
13
|
-
|
|
106
|
+
fn install_class(genv: &mut GlobalEnv, class_name: String) {
|
|
14
107
|
genv.enter_class(class_name);
|
|
15
108
|
}
|
|
16
109
|
|
|
17
110
|
/// Install module definition
|
|
18
|
-
|
|
111
|
+
fn install_module(genv: &mut GlobalEnv, module_name: String) {
|
|
19
112
|
genv.enter_module(module_name);
|
|
20
113
|
}
|
|
21
114
|
|
|
22
115
|
/// Install method definition
|
|
23
|
-
|
|
116
|
+
fn install_method(genv: &mut GlobalEnv, method_name: String) {
|
|
24
117
|
genv.enter_method(method_name);
|
|
25
118
|
}
|
|
26
119
|
|
|
27
120
|
/// Exit current scope (class, module, or method)
|
|
28
|
-
|
|
121
|
+
fn exit_scope(genv: &mut GlobalEnv) {
|
|
29
122
|
genv.exit_scope();
|
|
30
123
|
}
|
|
31
124
|
|
|
32
125
|
/// Extract class name from ClassNode
|
|
33
126
|
/// Supports both simple names (User) and qualified names (Api::V1::User)
|
|
34
|
-
|
|
127
|
+
fn extract_class_name(class_node: &ruby_prism::ClassNode) -> String {
|
|
35
128
|
extract_constant_path(&class_node.constant_path()).unwrap_or_else(|| "UnknownClass".to_string())
|
|
36
129
|
}
|
|
37
130
|
|
|
38
131
|
/// Extract module name from ModuleNode
|
|
39
132
|
/// Supports both simple names (Utils) and qualified names (Api::V1::Utils)
|
|
40
|
-
|
|
133
|
+
fn extract_module_name(module_node: &ruby_prism::ModuleNode) -> String {
|
|
41
134
|
extract_constant_path(&module_node.constant_path())
|
|
42
135
|
.unwrap_or_else(|| "UnknownModule".to_string())
|
|
43
136
|
}
|
|
@@ -49,7 +142,7 @@ pub fn extract_module_name(module_node: &ruby_prism::ModuleNode) -> String {
|
|
|
49
142
|
/// - `Api::User` (ConstantPathNode) → "Api::User"
|
|
50
143
|
/// - `Api::V1::User` (nested ConstantPathNode) → "Api::V1::User"
|
|
51
144
|
/// - `::Api::User` (absolute path with COLON3) → "Api::User"
|
|
52
|
-
fn extract_constant_path(node: &Node) -> Option<String> {
|
|
145
|
+
pub(crate) fn extract_constant_path(node: &Node) -> Option<String> {
|
|
53
146
|
// Simple constant read: `User`
|
|
54
147
|
if let Some(constant_read) = node.as_constant_read_node() {
|
|
55
148
|
return Some(String::from_utf8_lossy(constant_read.name().as_slice()).to_string());
|
|
@@ -79,7 +172,9 @@ fn extract_constant_path(node: &Node) -> Option<String> {
|
|
|
79
172
|
#[cfg(test)]
|
|
80
173
|
mod tests {
|
|
81
174
|
use super::*;
|
|
175
|
+
use crate::graph::ChangeSet;
|
|
82
176
|
use crate::parser::ParseSession;
|
|
177
|
+
use crate::types::Type;
|
|
83
178
|
|
|
84
179
|
#[test]
|
|
85
180
|
fn test_enter_exit_class_scope() {
|
|
@@ -216,4 +311,27 @@ mod tests {
|
|
|
216
311
|
let name = extract_module_name(&module_node);
|
|
217
312
|
assert_eq!(name, "Api::V1");
|
|
218
313
|
}
|
|
314
|
+
|
|
315
|
+
#[test]
|
|
316
|
+
fn test_process_def_node_registers_user_method() {
|
|
317
|
+
let source = "class User; def name; \"Alice\"; end; end";
|
|
318
|
+
let session = ParseSession::new();
|
|
319
|
+
let parse_result = session.parse_source(source, "test.rb").unwrap();
|
|
320
|
+
let root = parse_result.node();
|
|
321
|
+
let program = root.as_program_node().unwrap();
|
|
322
|
+
|
|
323
|
+
let mut genv = GlobalEnv::new();
|
|
324
|
+
let mut lenv = LocalEnv::new();
|
|
325
|
+
let mut changes = ChangeSet::new();
|
|
326
|
+
|
|
327
|
+
let stmt = program.statements().body().first().unwrap();
|
|
328
|
+
let class_node = stmt.as_class_node().unwrap();
|
|
329
|
+
process_class_node(&mut genv, &mut lenv, &mut changes, source, &class_node);
|
|
330
|
+
|
|
331
|
+
// User#name should be registered as a user-defined method
|
|
332
|
+
let info = genv
|
|
333
|
+
.resolve_method(&Type::instance("User"), "name")
|
|
334
|
+
.expect("User#name should be registered");
|
|
335
|
+
assert!(info.return_vertex.is_some());
|
|
336
|
+
}
|
|
219
337
|
}
|