method-ray 0.1.3 → 0.1.5

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.
@@ -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 or x.each { |i| ... }
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
- let method_name = String::from_utf8_lossy(call_node.name().as_slice()).to_string();
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,69 @@ 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 qualified name to match method registration in definitions.rs
245
+ let recv_vtx = if let Some(name) = genv.scope_manager.current_qualified_name() {
246
+ genv.new_source(Type::instance(&name))
247
+ } else {
248
+ genv.new_source(Type::instance("Object"))
249
+ };
250
+ process_method_call_common(
251
+ genv, lenv, changes, source, recv_vtx, method_name, location, block, arguments,
252
+ )
253
+ }
254
+ NeedsChildKind::AttrDeclaration { kind, attr_names } => {
255
+ super::attributes::process_attr_declaration(genv, kind, attr_names);
256
+ None
257
+ }
258
+ }
259
+ }
260
+
118
261
  /// Finish instance variable write after child is processed
119
- pub fn finish_ivar_write(genv: &mut GlobalEnv, ivar_name: String, value_vtx: VertexId) -> VertexId {
262
+ fn finish_ivar_write(genv: &mut GlobalEnv, ivar_name: String, value_vtx: VertexId) -> VertexId {
120
263
  install_ivar_write(genv, ivar_name, value_vtx)
121
264
  }
122
265
 
123
266
  /// Finish local variable write after child is processed
124
- pub fn finish_local_var_write(
267
+ fn finish_local_var_write(
125
268
  genv: &mut GlobalEnv,
126
269
  lenv: &mut LocalEnv,
127
270
  changes: &mut ChangeSet,
@@ -131,12 +274,623 @@ pub fn finish_local_var_write(
131
274
  install_local_var_write(genv, lenv, changes, var_name, value_vtx)
132
275
  }
133
276
 
277
+ /// MethodCall / ImplicitSelfCall common processing:
278
+ /// Handles argument processing, block processing, and MethodCallBox creation after recv_vtx is obtained
279
+ fn process_method_call_common<'a>(
280
+ genv: &mut GlobalEnv,
281
+ lenv: &mut LocalEnv,
282
+ changes: &mut ChangeSet,
283
+ source: &str,
284
+ recv_vtx: VertexId,
285
+ method_name: String,
286
+ location: SourceLocation,
287
+ block: Option<Node<'a>>,
288
+ arguments: Vec<Node<'a>>,
289
+ ) -> Option<VertexId> {
290
+ let arg_vtxs: Vec<VertexId> = arguments
291
+ .iter()
292
+ .filter_map(|arg| super::install::install_node(genv, lenv, changes, source, arg))
293
+ .collect();
294
+
295
+ if let Some(block_node) = block {
296
+ if let Some(block) = block_node.as_block_node() {
297
+ let param_vtxs = super::blocks::process_block_node_with_params(
298
+ genv, lenv, changes, source, &block,
299
+ );
300
+
301
+ if !param_vtxs.is_empty() {
302
+ let box_id = genv.alloc_box_id();
303
+ let block_box = BlockParameterTypeBox::new(
304
+ box_id,
305
+ recv_vtx,
306
+ method_name.clone(),
307
+ param_vtxs,
308
+ );
309
+ genv.register_box(box_id, Box::new(block_box));
310
+ }
311
+ }
312
+ }
313
+
314
+ Some(finish_method_call(
315
+ genv, recv_vtx, method_name, arg_vtxs, location,
316
+ ))
317
+ }
318
+
134
319
  /// Finish method call after receiver is processed
135
- pub fn finish_method_call(
320
+ fn finish_method_call(
136
321
  genv: &mut GlobalEnv,
137
322
  recv_vtx: VertexId,
138
323
  method_name: String,
324
+ arg_vtxs: Vec<VertexId>,
139
325
  location: SourceLocation,
140
326
  ) -> VertexId {
141
- install_method_call(genv, recv_vtx, method_name, Some(location))
327
+ install_method_call(genv, recv_vtx, method_name, arg_vtxs, Some(location))
328
+ }
329
+
330
+ #[cfg(test)]
331
+ mod tests {
332
+ use super::*;
333
+ use crate::analyzer::install::AstInstaller;
334
+ use crate::parser::ParseSession;
335
+
336
+ /// Helper: parse Ruby source, process with AstInstaller, and return GlobalEnv
337
+ fn analyze(source: &str) -> GlobalEnv {
338
+ let session = ParseSession::new();
339
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
340
+ let root = parse_result.node();
341
+ let program = root.as_program_node().unwrap();
342
+
343
+ let mut genv = GlobalEnv::new();
344
+ let mut lenv = LocalEnv::new();
345
+
346
+ let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
347
+ for stmt in &program.statements().body() {
348
+ installer.install_node(&stmt);
349
+ }
350
+ installer.finish();
351
+
352
+ genv
353
+ }
354
+
355
+ /// Helper: get the type string for a vertex ID (checks both Vertex and Source)
356
+ fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
357
+ if let Some(vertex) = genv.get_vertex(vtx) {
358
+ vertex.show()
359
+ } else if let Some(source) = genv.get_source(vtx) {
360
+ source.ty.show()
361
+ } else {
362
+ panic!("vertex {:?} not found as either Vertex or Source", vtx);
363
+ }
364
+ }
365
+
366
+ // Test 1: Receiverless method call type resolution
367
+ #[test]
368
+ fn test_implicit_self_call_type_resolution() {
369
+ let source = r#"
370
+ class User
371
+ def name
372
+ "Alice"
373
+ end
374
+
375
+ def greet
376
+ name
377
+ end
378
+ end
379
+ "#;
380
+ let genv = analyze(source);
381
+
382
+ // User#greet should resolve to String via User#name
383
+ let info = genv
384
+ .resolve_method(&Type::instance("User"), "greet")
385
+ .expect("User#greet should be registered");
386
+ assert!(info.return_vertex.is_some());
387
+
388
+ let ret_vtx = info.return_vertex.unwrap();
389
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
390
+ }
391
+
392
+ // Test 2: Receiverless method call with arguments
393
+ #[test]
394
+ fn test_implicit_self_call_with_arguments() {
395
+ let source = r#"
396
+ class Calculator
397
+ def add(x, y)
398
+ x
399
+ end
400
+
401
+ def compute
402
+ add(1, 2)
403
+ end
404
+ end
405
+ "#;
406
+ let genv = analyze(source);
407
+
408
+ // Calculator#compute should resolve via Calculator#add
409
+ let info = genv
410
+ .resolve_method(&Type::instance("Calculator"), "compute")
411
+ .expect("Calculator#compute should be registered");
412
+ assert!(info.return_vertex.is_some());
413
+
414
+ let ret_vtx = info.return_vertex.unwrap();
415
+ assert_eq!(get_type_show(&genv, ret_vtx), "Integer");
416
+ }
417
+
418
+ // Test 3: Receiverless call in nested class
419
+ #[test]
420
+ fn test_implicit_self_call_in_nested_class() {
421
+ let source = r#"
422
+ module Api
423
+ module V1
424
+ class User
425
+ def name
426
+ "Alice"
427
+ end
428
+
429
+ def greet
430
+ name
431
+ end
432
+ end
433
+ end
434
+ end
435
+ "#;
436
+ let genv = analyze(source);
437
+
438
+ // Method registered with qualified name "Api::V1::User"
439
+ let info = genv
440
+ .resolve_method(&Type::instance("Api::V1::User"), "greet")
441
+ .expect("Api::V1::User#greet should be registered");
442
+ assert!(info.return_vertex.is_some());
443
+
444
+ let ret_vtx = info.return_vertex.unwrap();
445
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
446
+ }
447
+
448
+ // Test 4: Receiverless call in module
449
+ #[test]
450
+ fn test_implicit_self_call_in_module() {
451
+ let source = r#"
452
+ module Utils
453
+ def self.format(value)
454
+ value
455
+ end
456
+
457
+ def self.run
458
+ format("test")
459
+ end
460
+ end
461
+ "#;
462
+ let genv = analyze(source);
463
+
464
+ // Utils.run should be registered
465
+ let info = genv
466
+ .resolve_method(&Type::instance("Utils"), "run")
467
+ .expect("Utils#run should be registered");
468
+ assert!(info.return_vertex.is_some());
469
+ }
470
+
471
+ // Test 5: Receiverless call from within block
472
+ #[test]
473
+ fn test_implicit_self_call_from_block() {
474
+ let source = r#"
475
+ class User
476
+ def name
477
+ "Alice"
478
+ end
479
+
480
+ def greet
481
+ [1].each { name }
482
+ end
483
+ end
484
+ "#;
485
+ let genv = analyze(source);
486
+
487
+ // User#name should be registered and resolve to String
488
+ let name_info = genv
489
+ .resolve_method(&Type::instance("User"), "name")
490
+ .expect("User#name should be registered");
491
+ assert!(name_info.return_vertex.is_some());
492
+ assert_eq!(get_type_show(&genv, name_info.return_vertex.unwrap()), "String");
493
+
494
+ // User#greet should also be registered (block contains implicit self call)
495
+ let greet_info = genv
496
+ .resolve_method(&Type::instance("User"), "greet")
497
+ .expect("User#greet should be registered");
498
+ assert!(greet_info.return_vertex.is_some());
499
+ }
500
+
501
+ // Test 6: Top-level receiverless call (Object receiver)
502
+ #[test]
503
+ fn test_implicit_self_call_at_top_level() {
504
+ let source = r#"
505
+ def helper
506
+ "result"
507
+ end
508
+
509
+ helper
510
+ "#;
511
+ let genv = analyze(source);
512
+
513
+ // Should not panic; top-level call uses Object as receiver type
514
+ // NOTE: top-level def is not registered in method_registry yet,
515
+ // so this will produce a type error (false positive).
516
+ // The important thing is that it doesn't panic.
517
+ // No panic is the real assertion - top-level call should be processed without error
518
+ let _ = genv;
519
+ }
520
+
521
+ // Test 7: attr_reader basic — getter type resolution
522
+ #[test]
523
+ fn test_attr_reader_basic() {
524
+ let source = r#"
525
+ class User
526
+ attr_reader :name
527
+
528
+ def initialize
529
+ @name = "Alice"
530
+ end
531
+ end
532
+ "#;
533
+ let genv = analyze(source);
534
+
535
+ // User#name should be registered and resolve to String via @name
536
+ let info = genv
537
+ .resolve_method(&Type::instance("User"), "name")
538
+ .expect("User#name should be registered");
539
+ assert!(info.return_vertex.is_some());
540
+ let ret_vtx = info.return_vertex.unwrap();
541
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
542
+
543
+ // Other methods still work
544
+ let greet_src = r#"
545
+ class User
546
+ attr_reader :name
547
+
548
+ def greet
549
+ "hello"
550
+ end
551
+ end
552
+ "#;
553
+ let genv2 = analyze(greet_src);
554
+ let info2 = genv2
555
+ .resolve_method(&Type::instance("User"), "greet")
556
+ .expect("User#greet should be registered");
557
+ assert_eq!(get_type_show(&genv2, info2.return_vertex.unwrap()), "String");
558
+ }
559
+
560
+ // Test 8: attr_reader + self.name method call
561
+ #[test]
562
+ fn test_attr_reader_with_self_call() {
563
+ let source = r#"
564
+ class User
565
+ attr_reader :name
566
+
567
+ def initialize
568
+ @name = "Alice"
569
+ end
570
+
571
+ def greet
572
+ self.name
573
+ end
574
+ end
575
+ "#;
576
+ let genv = analyze(source);
577
+
578
+ // User#greet should resolve to String via User#name → @name
579
+ let info = genv
580
+ .resolve_method(&Type::instance("User"), "greet")
581
+ .expect("User#greet should be registered");
582
+ assert!(info.return_vertex.is_some());
583
+ let ret_vtx = info.return_vertex.unwrap();
584
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
585
+ }
586
+
587
+ // Test 9: attr_reader + receiverless name call (proposal D integration)
588
+ #[test]
589
+ fn test_attr_reader_receiverless_call() {
590
+ let source = r#"
591
+ class User
592
+ attr_reader :name
593
+
594
+ def initialize
595
+ @name = "Alice"
596
+ end
597
+
598
+ def greet
599
+ name
600
+ end
601
+ end
602
+ "#;
603
+ let genv = analyze(source);
604
+
605
+ // User#greet should resolve to String via implicit self → User#name → @name
606
+ let info = genv
607
+ .resolve_method(&Type::instance("User"), "greet")
608
+ .expect("User#greet should be registered");
609
+ assert!(info.return_vertex.is_some());
610
+ let ret_vtx = info.return_vertex.unwrap();
611
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
612
+ }
613
+
614
+ // Test 10: attr_accessor — both getter and setter
615
+ #[test]
616
+ fn test_attr_accessor() {
617
+ let source = r#"
618
+ class User
619
+ attr_accessor :age
620
+
621
+ def initialize
622
+ @age = 30
623
+ end
624
+ end
625
+ "#;
626
+ let genv = analyze(source);
627
+
628
+ // User#age (getter) should be registered and resolve to Integer
629
+ let getter = genv
630
+ .resolve_method(&Type::instance("User"), "age")
631
+ .expect("User#age getter should be registered");
632
+ assert!(getter.return_vertex.is_some());
633
+ assert_eq!(get_type_show(&genv, getter.return_vertex.unwrap()), "Integer");
634
+
635
+ // User#age= (setter) should also be registered
636
+ let setter = genv
637
+ .resolve_method(&Type::instance("User"), "age=")
638
+ .expect("User#age= setter should be registered");
639
+ assert!(setter.return_vertex.is_some());
640
+ }
641
+
642
+ // Test 11: multiple attributes in single declaration
643
+ #[test]
644
+ fn test_attr_reader_multiple() {
645
+ let source = r#"
646
+ class User
647
+ attr_reader :name, :email
648
+
649
+ def initialize
650
+ @name = "Alice"
651
+ @email = "alice@test.com"
652
+ end
653
+ end
654
+ "#;
655
+ let genv = analyze(source);
656
+
657
+ let name_info = genv
658
+ .resolve_method(&Type::instance("User"), "name")
659
+ .expect("User#name should be registered");
660
+ assert_eq!(get_type_show(&genv, name_info.return_vertex.unwrap()), "String");
661
+
662
+ let email_info = genv
663
+ .resolve_method(&Type::instance("User"), "email")
664
+ .expect("User#email should be registered");
665
+ assert_eq!(get_type_show(&genv, email_info.return_vertex.unwrap()), "String");
666
+ }
667
+
668
+ // Test 12: attr_reader in nested class
669
+ #[test]
670
+ fn test_attr_reader_nested_class() {
671
+ let source = r#"
672
+ module Api
673
+ class User
674
+ attr_reader :name
675
+
676
+ def initialize
677
+ @name = "Alice"
678
+ end
679
+ end
680
+ end
681
+ "#;
682
+ let genv = analyze(source);
683
+
684
+ // Registered with qualified name "Api::User"
685
+ let info = genv
686
+ .resolve_method(&Type::instance("Api::User"), "name")
687
+ .expect("Api::User#name should be registered");
688
+ assert!(info.return_vertex.is_some());
689
+ assert_eq!(get_type_show(&genv, info.return_vertex.unwrap()), "String");
690
+ }
691
+
692
+ // Test 13: attr_writer only — setter registered, getter not
693
+ #[test]
694
+ fn test_attr_writer_only() {
695
+ let source = r#"
696
+ class User
697
+ attr_writer :name
698
+ end
699
+ "#;
700
+ let genv = analyze(source);
701
+
702
+ // User#name= should be registered
703
+ let setter = genv.resolve_method(&Type::instance("User"), "name=");
704
+ assert!(setter.is_some(), "User#name= should be registered");
705
+
706
+ // User#name (getter) should NOT be registered
707
+ let getter = genv.resolve_method(&Type::instance("User"), "name");
708
+ assert!(getter.is_none(), "User#name getter should NOT be registered for attr_writer");
709
+ }
710
+
711
+ // Test 14: attr_reader with no assignment (empty vertex)
712
+ #[test]
713
+ fn test_attr_reader_unassigned() {
714
+ let source = r#"
715
+ class User
716
+ attr_reader :unknown
717
+ end
718
+ "#;
719
+ let genv = analyze(source);
720
+
721
+ // User#unknown should be registered (with empty vertex)
722
+ let info = genv
723
+ .resolve_method(&Type::instance("User"), "unknown")
724
+ .expect("User#unknown should be registered");
725
+ assert!(info.return_vertex.is_some());
726
+ }
727
+
728
+ // Test 15: super call independence (SuperNode is not CallNode)
729
+ #[test]
730
+ fn test_super_call_independence() {
731
+ let source = r#"
732
+ class Base
733
+ def greet
734
+ "hello"
735
+ end
736
+ end
737
+
738
+ class Child < Base
739
+ def greet
740
+ super
741
+ end
742
+ end
743
+ "#;
744
+ let genv = analyze(source);
745
+
746
+ // super is a SuperNode, not a CallNode, so ImplicitSelfCall should not be triggered.
747
+ // Base#greet should still work.
748
+ let info = genv
749
+ .resolve_method(&Type::instance("Base"), "greet")
750
+ .expect("Base#greet should be registered");
751
+ assert!(info.return_vertex.is_some());
752
+
753
+ let ret_vtx = info.return_vertex.unwrap();
754
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
755
+ }
756
+
757
+ // Test 16: User.new → instance(User)
758
+ #[test]
759
+ fn test_constant_read_user_new() {
760
+ let source = r#"
761
+ class User
762
+ def name
763
+ "Alice"
764
+ end
765
+ end
766
+
767
+ x = User.new
768
+ "#;
769
+ let genv = analyze(source);
770
+ assert!(
771
+ genv.type_errors.is_empty(),
772
+ "User.new should not produce type errors: {:?}",
773
+ genv.type_errors
774
+ );
775
+ }
776
+
777
+ // Test 17: User.new.name → String
778
+ #[test]
779
+ fn test_constant_read_user_new_method_chain() {
780
+ let source = r#"
781
+ class User
782
+ def name
783
+ "Alice"
784
+ end
785
+ end
786
+
787
+ x = User.new.name
788
+ "#;
789
+ let genv = analyze(source);
790
+ assert!(
791
+ genv.type_errors.is_empty(),
792
+ "User.new.name should not produce type errors: {:?}",
793
+ genv.type_errors
794
+ );
795
+ }
796
+
797
+ // Test 18: Api::User.new → instance(Api::User) (ConstantPathNode)
798
+ #[test]
799
+ fn test_constant_path_qualified_new() {
800
+ let source = r#"
801
+ class Api::User
802
+ def name
803
+ "Alice"
804
+ end
805
+ end
806
+
807
+ x = Api::User.new
808
+ "#;
809
+ let genv = analyze(source);
810
+ assert!(
811
+ genv.type_errors.is_empty(),
812
+ "Api::User.new should not produce type errors: {:?}",
813
+ genv.type_errors
814
+ );
815
+ }
816
+
817
+ // Test 19: User.new("Alice") → initialize parameter propagation
818
+ #[test]
819
+ fn test_constant_read_new_with_initialize_params() {
820
+ let source = r#"
821
+ class User
822
+ def initialize(name)
823
+ @name = name
824
+ end
825
+ end
826
+
827
+ x = User.new("Alice")
828
+ "#;
829
+ let genv = analyze(source);
830
+ assert!(genv.type_errors.is_empty());
831
+ }
832
+
833
+ // Test 20: user = User.new; user.name → String
834
+ #[test]
835
+ fn test_constant_read_assign_and_call() {
836
+ let source = r#"
837
+ class User
838
+ def name
839
+ "Alice"
840
+ end
841
+ end
842
+
843
+ user = User.new
844
+ user.name
845
+ "#;
846
+ let genv = analyze(source);
847
+ assert!(
848
+ genv.type_errors.is_empty(),
849
+ "user = User.new; user.name should not produce type errors: {:?}",
850
+ genv.type_errors
851
+ );
852
+ }
853
+
854
+ // Test 21: User.some_method should not produce type error (Singleton error suppression)
855
+ #[test]
856
+ fn test_constant_read_no_false_positive() {
857
+ let source = r#"
858
+ class User
859
+ def name
860
+ "Alice"
861
+ end
862
+ end
863
+
864
+ User.some_method
865
+ "#;
866
+ let genv = analyze(source);
867
+ assert!(
868
+ genv.type_errors.is_empty(),
869
+ "User.some_method should not produce type errors (Singleton suppression): {:?}",
870
+ genv.type_errors
871
+ );
872
+ }
873
+
874
+ // Test: ConstantPathNode.new resolves with qualified method name
875
+ #[test]
876
+ fn test_constant_path_new_with_qualified_method() {
877
+ let source = r#"
878
+ module Api
879
+ class User
880
+ def name
881
+ "Alice"
882
+ end
883
+ end
884
+ end
885
+
886
+ Api::User.new.name
887
+ "#;
888
+ let genv = analyze(source);
889
+ // Api::User.new.name should resolve correctly — no type errors
890
+ assert!(
891
+ genv.type_errors.is_empty(),
892
+ "Api::User.new.name should not produce type errors: {:?}",
893
+ genv.type_errors
894
+ );
895
+ }
142
896
  }