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
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
//! and dispatches them to specialized handlers.
|
|
5
5
|
|
|
6
6
|
use crate::env::{GlobalEnv, LocalEnv};
|
|
7
|
-
use crate::graph::{ChangeSet, VertexId};
|
|
7
|
+
use crate::graph::{BlockParameterTypeBox, ChangeSet, VertexId};
|
|
8
8
|
use crate::source_map::SourceLocation;
|
|
9
|
+
use crate::types::Type;
|
|
9
10
|
use ruby_prism::Node;
|
|
10
11
|
|
|
11
12
|
use super::calls::install_method_call;
|
|
@@ -14,6 +15,14 @@ use super::variables::{
|
|
|
14
15
|
install_self,
|
|
15
16
|
};
|
|
16
17
|
|
|
18
|
+
/// Kind of attr_* declaration
|
|
19
|
+
#[derive(Debug, Clone, Copy)]
|
|
20
|
+
pub enum AttrKind {
|
|
21
|
+
Reader,
|
|
22
|
+
Writer,
|
|
23
|
+
Accessor,
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
/// Result of dispatching a simple node (no child processing needed)
|
|
18
27
|
pub enum DispatchResult {
|
|
19
28
|
/// Node produced a vertex
|
|
@@ -35,6 +44,20 @@ pub enum NeedsChildKind<'a> {
|
|
|
35
44
|
location: SourceLocation,
|
|
36
45
|
/// Optional block attached to the method call
|
|
37
46
|
block: Option<Node<'a>>,
|
|
47
|
+
/// Arguments to the method call
|
|
48
|
+
arguments: Vec<Node<'a>>,
|
|
49
|
+
},
|
|
50
|
+
/// Implicit self method call: method call without explicit receiver (implicit self)
|
|
51
|
+
ImplicitSelfCall {
|
|
52
|
+
method_name: String,
|
|
53
|
+
location: SourceLocation,
|
|
54
|
+
block: Option<Node<'a>>,
|
|
55
|
+
arguments: Vec<Node<'a>>,
|
|
56
|
+
},
|
|
57
|
+
/// attr_reader / attr_writer / attr_accessor declaration
|
|
58
|
+
AttrDeclaration {
|
|
59
|
+
kind: AttrKind,
|
|
60
|
+
attr_names: Vec<String>,
|
|
38
61
|
},
|
|
39
62
|
}
|
|
40
63
|
|
|
@@ -66,9 +89,41 @@ pub fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &Node) -
|
|
|
66
89
|
};
|
|
67
90
|
}
|
|
68
91
|
|
|
92
|
+
// ConstantReadNode: User → Type::Singleton("User")
|
|
93
|
+
if let Some(const_read) = node.as_constant_read_node() {
|
|
94
|
+
let name = String::from_utf8_lossy(const_read.name().as_slice()).to_string();
|
|
95
|
+
let vtx = genv.new_source(Type::singleton(&name));
|
|
96
|
+
return DispatchResult::Vertex(vtx);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ConstantPathNode: Api::User → Type::Singleton("Api::User")
|
|
100
|
+
if node.as_constant_path_node().is_some() {
|
|
101
|
+
if let Some(name) = super::definitions::extract_constant_path(node) {
|
|
102
|
+
let vtx = genv.new_source(Type::singleton(&name));
|
|
103
|
+
return DispatchResult::Vertex(vtx);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
69
107
|
DispatchResult::NotHandled
|
|
70
108
|
}
|
|
71
109
|
|
|
110
|
+
/// Extract symbol names from attr_* arguments (e.g., `attr_reader :name, :email`)
|
|
111
|
+
fn extract_symbol_names(call_node: &ruby_prism::CallNode) -> Vec<String> {
|
|
112
|
+
call_node
|
|
113
|
+
.arguments()
|
|
114
|
+
.map(|args| {
|
|
115
|
+
args.arguments()
|
|
116
|
+
.iter()
|
|
117
|
+
.filter_map(|arg| {
|
|
118
|
+
arg.as_symbol_node().map(|sym| {
|
|
119
|
+
String::from_utf8_lossy(&sym.unescaped()).to_string()
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
.collect()
|
|
123
|
+
})
|
|
124
|
+
.unwrap_or_default()
|
|
125
|
+
}
|
|
126
|
+
|
|
72
127
|
/// Check if node needs child processing
|
|
73
128
|
pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsChildKind<'a>> {
|
|
74
129
|
// Instance variable write: @name = value
|
|
@@ -89,25 +144,57 @@ pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsCh
|
|
|
89
144
|
});
|
|
90
145
|
}
|
|
91
146
|
|
|
92
|
-
// Method call: x.upcase
|
|
147
|
+
// Method call: x.upcase, x.each { |i| ... }, or name (implicit self)
|
|
93
148
|
if let Some(call_node) = node.as_call_node() {
|
|
149
|
+
let method_name = String::from_utf8_lossy(call_node.name().as_slice()).to_string();
|
|
150
|
+
let block = call_node.block();
|
|
151
|
+
let arguments: Vec<Node<'a>> = call_node
|
|
152
|
+
.arguments()
|
|
153
|
+
.map(|args| args.arguments().iter().collect())
|
|
154
|
+
.unwrap_or_default();
|
|
155
|
+
|
|
94
156
|
if let Some(receiver) = call_node.receiver() {
|
|
95
|
-
|
|
96
|
-
// Use call_operator_loc (.) for error position, fallback to node location
|
|
157
|
+
// Explicit receiver: x.upcase, x.each { |i| ... }
|
|
97
158
|
let prism_location = call_node
|
|
98
159
|
.call_operator_loc()
|
|
99
160
|
.unwrap_or_else(|| node.location());
|
|
100
161
|
let location =
|
|
101
162
|
SourceLocation::from_prism_location_with_source(&prism_location, source);
|
|
102
163
|
|
|
103
|
-
// Get block if present (e.g., `x.each { |i| ... }`)
|
|
104
|
-
let block = call_node.block();
|
|
105
|
-
|
|
106
164
|
return Some(NeedsChildKind::MethodCall {
|
|
107
165
|
receiver,
|
|
108
166
|
method_name,
|
|
109
167
|
location,
|
|
110
168
|
block,
|
|
169
|
+
arguments,
|
|
170
|
+
});
|
|
171
|
+
} else {
|
|
172
|
+
// No receiver: implicit self method call (e.g., `name`, `puts "hello"`)
|
|
173
|
+
|
|
174
|
+
if let Some(kind) = match method_name.as_str() {
|
|
175
|
+
"attr_reader" => Some(AttrKind::Reader),
|
|
176
|
+
"attr_writer" => Some(AttrKind::Writer),
|
|
177
|
+
"attr_accessor" => Some(AttrKind::Accessor),
|
|
178
|
+
_ => None,
|
|
179
|
+
} {
|
|
180
|
+
let attr_names = extract_symbol_names(&call_node);
|
|
181
|
+
if !attr_names.is_empty() {
|
|
182
|
+
return Some(NeedsChildKind::AttrDeclaration { kind, attr_names });
|
|
183
|
+
}
|
|
184
|
+
return None;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let prism_location = call_node
|
|
188
|
+
.message_loc()
|
|
189
|
+
.unwrap_or_else(|| node.location());
|
|
190
|
+
let location =
|
|
191
|
+
SourceLocation::from_prism_location_with_source(&prism_location, source);
|
|
192
|
+
|
|
193
|
+
return Some(NeedsChildKind::ImplicitSelfCall {
|
|
194
|
+
method_name,
|
|
195
|
+
location,
|
|
196
|
+
block,
|
|
197
|
+
arguments,
|
|
111
198
|
});
|
|
112
199
|
}
|
|
113
200
|
}
|
|
@@ -115,13 +202,73 @@ pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsCh
|
|
|
115
202
|
None
|
|
116
203
|
}
|
|
117
204
|
|
|
205
|
+
/// Process a node that needs child processing
|
|
206
|
+
///
|
|
207
|
+
/// This function handles the second phase of two-phase dispatch:
|
|
208
|
+
/// 1. `dispatch_needs_child` identifies the node kind and extracts data
|
|
209
|
+
/// 2. `process_needs_child` processes child nodes and completes the operation
|
|
210
|
+
pub(crate) fn process_needs_child(
|
|
211
|
+
genv: &mut GlobalEnv,
|
|
212
|
+
lenv: &mut LocalEnv,
|
|
213
|
+
changes: &mut ChangeSet,
|
|
214
|
+
source: &str,
|
|
215
|
+
kind: NeedsChildKind,
|
|
216
|
+
) -> Option<VertexId> {
|
|
217
|
+
match kind {
|
|
218
|
+
NeedsChildKind::IvarWrite { ivar_name, value } => {
|
|
219
|
+
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
|
|
220
|
+
Some(finish_ivar_write(genv, ivar_name, value_vtx))
|
|
221
|
+
}
|
|
222
|
+
NeedsChildKind::LocalVarWrite { var_name, value } => {
|
|
223
|
+
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
|
|
224
|
+
Some(finish_local_var_write(genv, lenv, changes, var_name, value_vtx))
|
|
225
|
+
}
|
|
226
|
+
NeedsChildKind::MethodCall {
|
|
227
|
+
receiver,
|
|
228
|
+
method_name,
|
|
229
|
+
location,
|
|
230
|
+
block,
|
|
231
|
+
arguments,
|
|
232
|
+
} => {
|
|
233
|
+
let recv_vtx = super::install::install_node(genv, lenv, changes, source, &receiver)?;
|
|
234
|
+
process_method_call_common(
|
|
235
|
+
genv, lenv, changes, source, recv_vtx, method_name, location, block, arguments,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
NeedsChildKind::ImplicitSelfCall {
|
|
239
|
+
method_name,
|
|
240
|
+
location,
|
|
241
|
+
block,
|
|
242
|
+
arguments,
|
|
243
|
+
} => {
|
|
244
|
+
// Use the same naming as method registration in definitions.rs
|
|
245
|
+
let recv_type_name = genv
|
|
246
|
+
.scope_manager
|
|
247
|
+
.current_class_name()
|
|
248
|
+
.or_else(|| genv.scope_manager.current_module_name());
|
|
249
|
+
let recv_vtx = if let Some(name) = recv_type_name {
|
|
250
|
+
genv.new_source(Type::instance(&name))
|
|
251
|
+
} else {
|
|
252
|
+
genv.new_source(Type::instance("Object"))
|
|
253
|
+
};
|
|
254
|
+
process_method_call_common(
|
|
255
|
+
genv, lenv, changes, source, recv_vtx, method_name, location, block, arguments,
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
NeedsChildKind::AttrDeclaration { kind, attr_names } => {
|
|
259
|
+
super::attributes::process_attr_declaration(genv, kind, attr_names);
|
|
260
|
+
None
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
118
265
|
/// Finish instance variable write after child is processed
|
|
119
|
-
|
|
266
|
+
fn finish_ivar_write(genv: &mut GlobalEnv, ivar_name: String, value_vtx: VertexId) -> VertexId {
|
|
120
267
|
install_ivar_write(genv, ivar_name, value_vtx)
|
|
121
268
|
}
|
|
122
269
|
|
|
123
270
|
/// Finish local variable write after child is processed
|
|
124
|
-
|
|
271
|
+
fn finish_local_var_write(
|
|
125
272
|
genv: &mut GlobalEnv,
|
|
126
273
|
lenv: &mut LocalEnv,
|
|
127
274
|
changes: &mut ChangeSet,
|
|
@@ -131,12 +278,600 @@ pub fn finish_local_var_write(
|
|
|
131
278
|
install_local_var_write(genv, lenv, changes, var_name, value_vtx)
|
|
132
279
|
}
|
|
133
280
|
|
|
281
|
+
/// MethodCall / ImplicitSelfCall common processing:
|
|
282
|
+
/// Handles argument processing, block processing, and MethodCallBox creation after recv_vtx is obtained
|
|
283
|
+
fn process_method_call_common<'a>(
|
|
284
|
+
genv: &mut GlobalEnv,
|
|
285
|
+
lenv: &mut LocalEnv,
|
|
286
|
+
changes: &mut ChangeSet,
|
|
287
|
+
source: &str,
|
|
288
|
+
recv_vtx: VertexId,
|
|
289
|
+
method_name: String,
|
|
290
|
+
location: SourceLocation,
|
|
291
|
+
block: Option<Node<'a>>,
|
|
292
|
+
arguments: Vec<Node<'a>>,
|
|
293
|
+
) -> Option<VertexId> {
|
|
294
|
+
let arg_vtxs: Vec<VertexId> = arguments
|
|
295
|
+
.iter()
|
|
296
|
+
.filter_map(|arg| super::install::install_node(genv, lenv, changes, source, arg))
|
|
297
|
+
.collect();
|
|
298
|
+
|
|
299
|
+
if let Some(block_node) = block {
|
|
300
|
+
if let Some(block) = block_node.as_block_node() {
|
|
301
|
+
let param_vtxs = super::blocks::process_block_node_with_params(
|
|
302
|
+
genv, lenv, changes, source, &block,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
if !param_vtxs.is_empty() {
|
|
306
|
+
let box_id = genv.alloc_box_id();
|
|
307
|
+
let block_box = BlockParameterTypeBox::new(
|
|
308
|
+
box_id,
|
|
309
|
+
recv_vtx,
|
|
310
|
+
method_name.clone(),
|
|
311
|
+
param_vtxs,
|
|
312
|
+
);
|
|
313
|
+
genv.register_box(box_id, Box::new(block_box));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
Some(finish_method_call(
|
|
319
|
+
genv, recv_vtx, method_name, arg_vtxs, location,
|
|
320
|
+
))
|
|
321
|
+
}
|
|
322
|
+
|
|
134
323
|
/// Finish method call after receiver is processed
|
|
135
|
-
|
|
324
|
+
fn finish_method_call(
|
|
136
325
|
genv: &mut GlobalEnv,
|
|
137
326
|
recv_vtx: VertexId,
|
|
138
327
|
method_name: String,
|
|
328
|
+
arg_vtxs: Vec<VertexId>,
|
|
139
329
|
location: SourceLocation,
|
|
140
330
|
) -> VertexId {
|
|
141
|
-
install_method_call(genv, recv_vtx, method_name, Some(location))
|
|
331
|
+
install_method_call(genv, recv_vtx, method_name, arg_vtxs, Some(location))
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
#[cfg(test)]
|
|
335
|
+
mod tests {
|
|
336
|
+
use super::*;
|
|
337
|
+
use crate::analyzer::install::AstInstaller;
|
|
338
|
+
use crate::parser::ParseSession;
|
|
339
|
+
|
|
340
|
+
/// Helper: parse Ruby source, process with AstInstaller, and return GlobalEnv
|
|
341
|
+
fn analyze(source: &str) -> GlobalEnv {
|
|
342
|
+
let session = ParseSession::new();
|
|
343
|
+
let parse_result = session.parse_source(source, "test.rb").unwrap();
|
|
344
|
+
let root = parse_result.node();
|
|
345
|
+
let program = root.as_program_node().unwrap();
|
|
346
|
+
|
|
347
|
+
let mut genv = GlobalEnv::new();
|
|
348
|
+
let mut lenv = LocalEnv::new();
|
|
349
|
+
|
|
350
|
+
let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
|
|
351
|
+
for stmt in &program.statements().body() {
|
|
352
|
+
installer.install_node(&stmt);
|
|
353
|
+
}
|
|
354
|
+
installer.finish();
|
|
355
|
+
|
|
356
|
+
genv
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// Helper: get the type string for a vertex ID (checks both Vertex and Source)
|
|
360
|
+
fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
|
|
361
|
+
if let Some(vertex) = genv.get_vertex(vtx) {
|
|
362
|
+
vertex.show()
|
|
363
|
+
} else if let Some(source) = genv.get_source(vtx) {
|
|
364
|
+
source.ty.show()
|
|
365
|
+
} else {
|
|
366
|
+
panic!("vertex {:?} not found as either Vertex or Source", vtx);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Test 1: Receiverless method call type resolution
|
|
371
|
+
#[test]
|
|
372
|
+
fn test_implicit_self_call_type_resolution() {
|
|
373
|
+
let source = r#"
|
|
374
|
+
class User
|
|
375
|
+
def name
|
|
376
|
+
"Alice"
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def greet
|
|
380
|
+
name
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
"#;
|
|
384
|
+
let genv = analyze(source);
|
|
385
|
+
|
|
386
|
+
// User#greet should resolve to String via User#name
|
|
387
|
+
let info = genv
|
|
388
|
+
.resolve_method(&Type::instance("User"), "greet")
|
|
389
|
+
.expect("User#greet should be registered");
|
|
390
|
+
assert!(info.return_vertex.is_some());
|
|
391
|
+
|
|
392
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
393
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "String");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Test 2: Receiverless method call with arguments
|
|
397
|
+
#[test]
|
|
398
|
+
fn test_implicit_self_call_with_arguments() {
|
|
399
|
+
let source = r#"
|
|
400
|
+
class Calculator
|
|
401
|
+
def add(x, y)
|
|
402
|
+
x
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def compute
|
|
406
|
+
add(1, 2)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
"#;
|
|
410
|
+
let genv = analyze(source);
|
|
411
|
+
|
|
412
|
+
// Calculator#compute should resolve via Calculator#add
|
|
413
|
+
let info = genv
|
|
414
|
+
.resolve_method(&Type::instance("Calculator"), "compute")
|
|
415
|
+
.expect("Calculator#compute should be registered");
|
|
416
|
+
assert!(info.return_vertex.is_some());
|
|
417
|
+
|
|
418
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
419
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "Integer");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Test 3: Receiverless call in nested class
|
|
423
|
+
#[test]
|
|
424
|
+
fn test_implicit_self_call_in_nested_class() {
|
|
425
|
+
let source = r#"
|
|
426
|
+
module Api
|
|
427
|
+
module V1
|
|
428
|
+
class User
|
|
429
|
+
def name
|
|
430
|
+
"Alice"
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def greet
|
|
434
|
+
name
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
"#;
|
|
440
|
+
let genv = analyze(source);
|
|
441
|
+
|
|
442
|
+
// Method registered with simple class name "User" (current behavior of definitions.rs)
|
|
443
|
+
let info = genv
|
|
444
|
+
.resolve_method(&Type::instance("User"), "greet")
|
|
445
|
+
.expect("User#greet should be registered");
|
|
446
|
+
assert!(info.return_vertex.is_some());
|
|
447
|
+
|
|
448
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
449
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "String");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Test 4: Receiverless call in module
|
|
453
|
+
#[test]
|
|
454
|
+
fn test_implicit_self_call_in_module() {
|
|
455
|
+
let source = r#"
|
|
456
|
+
module Utils
|
|
457
|
+
def self.format(value)
|
|
458
|
+
value
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def self.run
|
|
462
|
+
format("test")
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
"#;
|
|
466
|
+
let genv = analyze(source);
|
|
467
|
+
|
|
468
|
+
// Utils.run should be registered
|
|
469
|
+
let info = genv
|
|
470
|
+
.resolve_method(&Type::instance("Utils"), "run")
|
|
471
|
+
.expect("Utils#run should be registered");
|
|
472
|
+
assert!(info.return_vertex.is_some());
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Test 5: Receiverless call from within block
|
|
476
|
+
#[test]
|
|
477
|
+
fn test_implicit_self_call_from_block() {
|
|
478
|
+
let source = r#"
|
|
479
|
+
class User
|
|
480
|
+
def name
|
|
481
|
+
"Alice"
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def greet
|
|
485
|
+
[1].each { name }
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
"#;
|
|
489
|
+
let genv = analyze(source);
|
|
490
|
+
|
|
491
|
+
// User#name should be registered and resolve to String
|
|
492
|
+
let name_info = genv
|
|
493
|
+
.resolve_method(&Type::instance("User"), "name")
|
|
494
|
+
.expect("User#name should be registered");
|
|
495
|
+
assert!(name_info.return_vertex.is_some());
|
|
496
|
+
assert_eq!(get_type_show(&genv, name_info.return_vertex.unwrap()), "String");
|
|
497
|
+
|
|
498
|
+
// User#greet should also be registered (block contains implicit self call)
|
|
499
|
+
let greet_info = genv
|
|
500
|
+
.resolve_method(&Type::instance("User"), "greet")
|
|
501
|
+
.expect("User#greet should be registered");
|
|
502
|
+
assert!(greet_info.return_vertex.is_some());
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Test 6: Top-level receiverless call (Object receiver)
|
|
506
|
+
#[test]
|
|
507
|
+
fn test_implicit_self_call_at_top_level() {
|
|
508
|
+
let source = r#"
|
|
509
|
+
def helper
|
|
510
|
+
"result"
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
helper
|
|
514
|
+
"#;
|
|
515
|
+
let genv = analyze(source);
|
|
516
|
+
|
|
517
|
+
// Should not panic; top-level call uses Object as receiver type
|
|
518
|
+
// NOTE: top-level def is not registered in method_registry yet,
|
|
519
|
+
// so this will produce a type error (false positive).
|
|
520
|
+
// The important thing is that it doesn't panic.
|
|
521
|
+
// No panic is the real assertion - top-level call should be processed without error
|
|
522
|
+
let _ = genv;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Test 7: attr_reader basic — getter type resolution
|
|
526
|
+
#[test]
|
|
527
|
+
fn test_attr_reader_basic() {
|
|
528
|
+
let source = r#"
|
|
529
|
+
class User
|
|
530
|
+
attr_reader :name
|
|
531
|
+
|
|
532
|
+
def initialize
|
|
533
|
+
@name = "Alice"
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
"#;
|
|
537
|
+
let genv = analyze(source);
|
|
538
|
+
|
|
539
|
+
// User#name should be registered and resolve to String via @name
|
|
540
|
+
let info = genv
|
|
541
|
+
.resolve_method(&Type::instance("User"), "name")
|
|
542
|
+
.expect("User#name should be registered");
|
|
543
|
+
assert!(info.return_vertex.is_some());
|
|
544
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
545
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "String");
|
|
546
|
+
|
|
547
|
+
// Other methods still work
|
|
548
|
+
let greet_src = r#"
|
|
549
|
+
class User
|
|
550
|
+
attr_reader :name
|
|
551
|
+
|
|
552
|
+
def greet
|
|
553
|
+
"hello"
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
"#;
|
|
557
|
+
let genv2 = analyze(greet_src);
|
|
558
|
+
let info2 = genv2
|
|
559
|
+
.resolve_method(&Type::instance("User"), "greet")
|
|
560
|
+
.expect("User#greet should be registered");
|
|
561
|
+
assert_eq!(get_type_show(&genv2, info2.return_vertex.unwrap()), "String");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Test 8: attr_reader + self.name method call
|
|
565
|
+
#[test]
|
|
566
|
+
fn test_attr_reader_with_self_call() {
|
|
567
|
+
let source = r#"
|
|
568
|
+
class User
|
|
569
|
+
attr_reader :name
|
|
570
|
+
|
|
571
|
+
def initialize
|
|
572
|
+
@name = "Alice"
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def greet
|
|
576
|
+
self.name
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
"#;
|
|
580
|
+
let genv = analyze(source);
|
|
581
|
+
|
|
582
|
+
// User#greet should resolve to String via User#name → @name
|
|
583
|
+
let info = genv
|
|
584
|
+
.resolve_method(&Type::instance("User"), "greet")
|
|
585
|
+
.expect("User#greet should be registered");
|
|
586
|
+
assert!(info.return_vertex.is_some());
|
|
587
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
588
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "String");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Test 9: attr_reader + receiverless name call (proposal D integration)
|
|
592
|
+
#[test]
|
|
593
|
+
fn test_attr_reader_receiverless_call() {
|
|
594
|
+
let source = r#"
|
|
595
|
+
class User
|
|
596
|
+
attr_reader :name
|
|
597
|
+
|
|
598
|
+
def initialize
|
|
599
|
+
@name = "Alice"
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def greet
|
|
603
|
+
name
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
"#;
|
|
607
|
+
let genv = analyze(source);
|
|
608
|
+
|
|
609
|
+
// User#greet should resolve to String via implicit self → User#name → @name
|
|
610
|
+
let info = genv
|
|
611
|
+
.resolve_method(&Type::instance("User"), "greet")
|
|
612
|
+
.expect("User#greet should be registered");
|
|
613
|
+
assert!(info.return_vertex.is_some());
|
|
614
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
615
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "String");
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Test 10: attr_accessor — both getter and setter
|
|
619
|
+
#[test]
|
|
620
|
+
fn test_attr_accessor() {
|
|
621
|
+
let source = r#"
|
|
622
|
+
class User
|
|
623
|
+
attr_accessor :age
|
|
624
|
+
|
|
625
|
+
def initialize
|
|
626
|
+
@age = 30
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
"#;
|
|
630
|
+
let genv = analyze(source);
|
|
631
|
+
|
|
632
|
+
// User#age (getter) should be registered and resolve to Integer
|
|
633
|
+
let getter = genv
|
|
634
|
+
.resolve_method(&Type::instance("User"), "age")
|
|
635
|
+
.expect("User#age getter should be registered");
|
|
636
|
+
assert!(getter.return_vertex.is_some());
|
|
637
|
+
assert_eq!(get_type_show(&genv, getter.return_vertex.unwrap()), "Integer");
|
|
638
|
+
|
|
639
|
+
// User#age= (setter) should also be registered
|
|
640
|
+
let setter = genv
|
|
641
|
+
.resolve_method(&Type::instance("User"), "age=")
|
|
642
|
+
.expect("User#age= setter should be registered");
|
|
643
|
+
assert!(setter.return_vertex.is_some());
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Test 11: multiple attributes in single declaration
|
|
647
|
+
#[test]
|
|
648
|
+
fn test_attr_reader_multiple() {
|
|
649
|
+
let source = r#"
|
|
650
|
+
class User
|
|
651
|
+
attr_reader :name, :email
|
|
652
|
+
|
|
653
|
+
def initialize
|
|
654
|
+
@name = "Alice"
|
|
655
|
+
@email = "alice@test.com"
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
"#;
|
|
659
|
+
let genv = analyze(source);
|
|
660
|
+
|
|
661
|
+
let name_info = genv
|
|
662
|
+
.resolve_method(&Type::instance("User"), "name")
|
|
663
|
+
.expect("User#name should be registered");
|
|
664
|
+
assert_eq!(get_type_show(&genv, name_info.return_vertex.unwrap()), "String");
|
|
665
|
+
|
|
666
|
+
let email_info = genv
|
|
667
|
+
.resolve_method(&Type::instance("User"), "email")
|
|
668
|
+
.expect("User#email should be registered");
|
|
669
|
+
assert_eq!(get_type_show(&genv, email_info.return_vertex.unwrap()), "String");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Test 12: attr_reader in nested class
|
|
673
|
+
#[test]
|
|
674
|
+
fn test_attr_reader_nested_class() {
|
|
675
|
+
let source = r#"
|
|
676
|
+
module Api
|
|
677
|
+
class User
|
|
678
|
+
attr_reader :name
|
|
679
|
+
|
|
680
|
+
def initialize
|
|
681
|
+
@name = "Alice"
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
"#;
|
|
686
|
+
let genv = analyze(source);
|
|
687
|
+
|
|
688
|
+
// Registered with simple class name "User"
|
|
689
|
+
let info = genv
|
|
690
|
+
.resolve_method(&Type::instance("User"), "name")
|
|
691
|
+
.expect("User#name should be registered");
|
|
692
|
+
assert!(info.return_vertex.is_some());
|
|
693
|
+
assert_eq!(get_type_show(&genv, info.return_vertex.unwrap()), "String");
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Test 13: attr_writer only — setter registered, getter not
|
|
697
|
+
#[test]
|
|
698
|
+
fn test_attr_writer_only() {
|
|
699
|
+
let source = r#"
|
|
700
|
+
class User
|
|
701
|
+
attr_writer :name
|
|
702
|
+
end
|
|
703
|
+
"#;
|
|
704
|
+
let genv = analyze(source);
|
|
705
|
+
|
|
706
|
+
// User#name= should be registered
|
|
707
|
+
let setter = genv.resolve_method(&Type::instance("User"), "name=");
|
|
708
|
+
assert!(setter.is_some(), "User#name= should be registered");
|
|
709
|
+
|
|
710
|
+
// User#name (getter) should NOT be registered
|
|
711
|
+
let getter = genv.resolve_method(&Type::instance("User"), "name");
|
|
712
|
+
assert!(getter.is_none(), "User#name getter should NOT be registered for attr_writer");
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Test 14: attr_reader with no assignment (empty vertex)
|
|
716
|
+
#[test]
|
|
717
|
+
fn test_attr_reader_unassigned() {
|
|
718
|
+
let source = r#"
|
|
719
|
+
class User
|
|
720
|
+
attr_reader :unknown
|
|
721
|
+
end
|
|
722
|
+
"#;
|
|
723
|
+
let genv = analyze(source);
|
|
724
|
+
|
|
725
|
+
// User#unknown should be registered (with empty vertex)
|
|
726
|
+
let info = genv
|
|
727
|
+
.resolve_method(&Type::instance("User"), "unknown")
|
|
728
|
+
.expect("User#unknown should be registered");
|
|
729
|
+
assert!(info.return_vertex.is_some());
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Test 15: super call independence (SuperNode is not CallNode)
|
|
733
|
+
#[test]
|
|
734
|
+
fn test_super_call_independence() {
|
|
735
|
+
let source = r#"
|
|
736
|
+
class Base
|
|
737
|
+
def greet
|
|
738
|
+
"hello"
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
class Child < Base
|
|
743
|
+
def greet
|
|
744
|
+
super
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
"#;
|
|
748
|
+
let genv = analyze(source);
|
|
749
|
+
|
|
750
|
+
// super is a SuperNode, not a CallNode, so ImplicitSelfCall should not be triggered.
|
|
751
|
+
// Base#greet should still work.
|
|
752
|
+
let info = genv
|
|
753
|
+
.resolve_method(&Type::instance("Base"), "greet")
|
|
754
|
+
.expect("Base#greet should be registered");
|
|
755
|
+
assert!(info.return_vertex.is_some());
|
|
756
|
+
|
|
757
|
+
let ret_vtx = info.return_vertex.unwrap();
|
|
758
|
+
assert_eq!(get_type_show(&genv, ret_vtx), "String");
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Test 16: User.new → instance(User)
|
|
762
|
+
#[test]
|
|
763
|
+
fn test_constant_read_user_new() {
|
|
764
|
+
let source = r#"
|
|
765
|
+
class User
|
|
766
|
+
def name
|
|
767
|
+
"Alice"
|
|
768
|
+
end
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
x = User.new
|
|
772
|
+
"#;
|
|
773
|
+
let genv = analyze(source);
|
|
774
|
+
assert!(
|
|
775
|
+
genv.type_errors.is_empty(),
|
|
776
|
+
"User.new should not produce type errors: {:?}",
|
|
777
|
+
genv.type_errors
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Test 17: User.new.name → String
|
|
782
|
+
#[test]
|
|
783
|
+
fn test_constant_read_user_new_method_chain() {
|
|
784
|
+
let source = r#"
|
|
785
|
+
class User
|
|
786
|
+
def name
|
|
787
|
+
"Alice"
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
x = User.new.name
|
|
792
|
+
"#;
|
|
793
|
+
let genv = analyze(source);
|
|
794
|
+
assert!(
|
|
795
|
+
genv.type_errors.is_empty(),
|
|
796
|
+
"User.new.name should not produce type errors: {:?}",
|
|
797
|
+
genv.type_errors
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Test 18: Api::User.new → instance(Api::User) (ConstantPathNode)
|
|
802
|
+
#[test]
|
|
803
|
+
fn test_constant_path_qualified_new() {
|
|
804
|
+
let source = r#"
|
|
805
|
+
class Api::User
|
|
806
|
+
def name
|
|
807
|
+
"Alice"
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
x = Api::User.new
|
|
812
|
+
"#;
|
|
813
|
+
let genv = analyze(source);
|
|
814
|
+
assert!(
|
|
815
|
+
genv.type_errors.is_empty(),
|
|
816
|
+
"Api::User.new should not produce type errors: {:?}",
|
|
817
|
+
genv.type_errors
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Test 19: User.new("Alice") → initialize parameter propagation
|
|
822
|
+
#[test]
|
|
823
|
+
fn test_constant_read_new_with_initialize_params() {
|
|
824
|
+
let source = r#"
|
|
825
|
+
class User
|
|
826
|
+
def initialize(name)
|
|
827
|
+
@name = name
|
|
828
|
+
end
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
x = User.new("Alice")
|
|
832
|
+
"#;
|
|
833
|
+
let genv = analyze(source);
|
|
834
|
+
assert!(genv.type_errors.is_empty());
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Test 20: user = User.new; user.name → String
|
|
838
|
+
#[test]
|
|
839
|
+
fn test_constant_read_assign_and_call() {
|
|
840
|
+
let source = r#"
|
|
841
|
+
class User
|
|
842
|
+
def name
|
|
843
|
+
"Alice"
|
|
844
|
+
end
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
user = User.new
|
|
848
|
+
user.name
|
|
849
|
+
"#;
|
|
850
|
+
let genv = analyze(source);
|
|
851
|
+
assert!(
|
|
852
|
+
genv.type_errors.is_empty(),
|
|
853
|
+
"user = User.new; user.name should not produce type errors: {:?}",
|
|
854
|
+
genv.type_errors
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Test 21: User.some_method should not produce type error (Singleton error suppression)
|
|
859
|
+
#[test]
|
|
860
|
+
fn test_constant_read_no_false_positive() {
|
|
861
|
+
let source = r#"
|
|
862
|
+
class User
|
|
863
|
+
def name
|
|
864
|
+
"Alice"
|
|
865
|
+
end
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
User.some_method
|
|
869
|
+
"#;
|
|
870
|
+
let genv = analyze(source);
|
|
871
|
+
assert!(
|
|
872
|
+
genv.type_errors.is_empty(),
|
|
873
|
+
"User.some_method should not produce type errors (Singleton suppression): {:?}",
|
|
874
|
+
genv.type_errors
|
|
875
|
+
);
|
|
876
|
+
}
|
|
142
877
|
}
|