method-ray 0.1.4 → 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.
@@ -65,8 +65,17 @@ pub(crate) fn process_def_node(
65
65
  def_node: &ruby_prism::DefNode,
66
66
  ) -> Option<VertexId> {
67
67
  let method_name = String::from_utf8_lossy(def_node.name().as_slice()).to_string();
68
+
69
+ // Check if this is a class method (def self.foo)
70
+ let is_class_method = def_node
71
+ .receiver()
72
+ .map(|r| r.as_self_node().is_some())
73
+ .unwrap_or(false);
74
+
68
75
  install_method(genv, method_name.clone());
69
76
 
77
+ let merge_vtx = genv.scope_manager.current_method_return_vertex();
78
+
70
79
  // Process parameters BEFORE processing body
71
80
  let param_vtxs = if let Some(params_node) = def_node.parameters() {
72
81
  install_parameters(genv, lenv, changes, source, &params_node)
@@ -81,18 +90,27 @@ pub(crate) fn process_def_node(
81
90
  }
82
91
  }
83
92
 
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());
93
+ // Connect last expression to merge vertex so that implicit return
94
+ // (Ruby's last-expression-is-return-value) is included in the union type
95
+ if let (Some(last), Some(merge)) = (last_vtx, merge_vtx) {
96
+ genv.add_edge(last, merge);
97
+ }
98
+
99
+ // Register user-defined method with merge vertex as return vertex
100
+ let return_vtx = merge_vtx.or(last_vtx);
101
+ if let Some(ret_vtx) = return_vtx {
102
+ let recv_type_name = genv.scope_manager.current_qualified_name();
90
103
 
91
104
  if let Some(name) = recv_type_name {
105
+ let recv_type = if is_class_method {
106
+ Type::singleton(&name)
107
+ } else {
108
+ Type::instance(&name)
109
+ };
92
110
  genv.register_user_method(
93
- Type::instance(&name),
111
+ recv_type,
94
112
  &method_name,
95
- return_vtx,
113
+ ret_vtx,
96
114
  param_vtxs,
97
115
  );
98
116
  }
@@ -334,4 +352,363 @@ mod tests {
334
352
  .expect("User#name should be registered");
335
353
  assert!(info.return_vertex.is_some());
336
354
  }
355
+
356
+ #[test]
357
+ fn test_qualified_name_method_registration() {
358
+ let source = r#"
359
+ module Api
360
+ module V1
361
+ class User
362
+ def name
363
+ "Alice"
364
+ end
365
+ end
366
+ end
367
+ end
368
+ "#;
369
+ let session = ParseSession::new();
370
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
371
+ let root = parse_result.node();
372
+ let program = root.as_program_node().unwrap();
373
+
374
+ let mut genv = GlobalEnv::new();
375
+ let mut lenv = LocalEnv::new();
376
+ let mut changes = ChangeSet::new();
377
+
378
+ for stmt in &program.statements().body() {
379
+ crate::analyzer::install::install_node(&mut genv, &mut lenv, &mut changes, source, &stmt);
380
+ }
381
+
382
+ // Method should be registered with qualified name "Api::V1::User"
383
+ let info = genv
384
+ .resolve_method(&Type::instance("Api::V1::User"), "name")
385
+ .expect("Api::V1::User#name should be registered");
386
+ assert!(info.return_vertex.is_some());
387
+
388
+ // Should NOT be registered with simple name "User"
389
+ assert!(
390
+ genv.resolve_method(&Type::instance("User"), "name").is_none(),
391
+ "User#name should not exist — method should be registered under qualified name"
392
+ );
393
+ }
394
+
395
+ #[test]
396
+ fn test_same_class_name_different_namespace() {
397
+ let source = r#"
398
+ module Api
399
+ class User
400
+ def name
401
+ "api_user"
402
+ end
403
+ end
404
+ end
405
+
406
+ module Admin
407
+ class User
408
+ def name
409
+ "admin_user"
410
+ end
411
+ end
412
+ end
413
+ "#;
414
+ let session = ParseSession::new();
415
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
416
+ let root = parse_result.node();
417
+ let program = root.as_program_node().unwrap();
418
+
419
+ let mut genv = GlobalEnv::new();
420
+ let mut lenv = LocalEnv::new();
421
+ let mut changes = ChangeSet::new();
422
+
423
+ for stmt in &program.statements().body() {
424
+ crate::analyzer::install::install_node(&mut genv, &mut lenv, &mut changes, source, &stmt);
425
+ }
426
+
427
+ // Both should be registered separately
428
+ let api_info = genv
429
+ .resolve_method(&Type::instance("Api::User"), "name")
430
+ .expect("Api::User#name should be registered");
431
+ assert!(api_info.return_vertex.is_some());
432
+
433
+ let admin_info = genv
434
+ .resolve_method(&Type::instance("Admin::User"), "name")
435
+ .expect("Admin::User#name should be registered");
436
+ assert!(admin_info.return_vertex.is_some());
437
+
438
+ // Simple "User" should not resolve
439
+ assert!(
440
+ genv.resolve_method(&Type::instance("User"), "name").is_none(),
441
+ "User#name should not exist — both are under qualified names"
442
+ );
443
+ }
444
+
445
+ #[test]
446
+ fn test_class_method_registration() {
447
+ let source = r#"
448
+ class User
449
+ def self.create
450
+ "created"
451
+ end
452
+ end
453
+ "#;
454
+ let session = ParseSession::new();
455
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
456
+ let root = parse_result.node();
457
+ let program = root.as_program_node().unwrap();
458
+
459
+ let mut genv = GlobalEnv::new();
460
+ let mut lenv = LocalEnv::new();
461
+ let mut changes = ChangeSet::new();
462
+
463
+ for stmt in &program.statements().body() {
464
+ crate::analyzer::install::install_node(&mut genv, &mut lenv, &mut changes, source, &stmt);
465
+ }
466
+
467
+ // def self.create should be registered as singleton method
468
+ let info = genv
469
+ .resolve_method(&Type::singleton("User"), "create")
470
+ .expect("User.create should be registered as singleton method");
471
+ assert!(info.return_vertex.is_some());
472
+ }
473
+
474
+ #[test]
475
+ fn test_class_method_with_params() {
476
+ let source = r#"
477
+ class User
478
+ def self.find(id)
479
+ "user"
480
+ end
481
+ end
482
+ "#;
483
+ let session = ParseSession::new();
484
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
485
+ let root = parse_result.node();
486
+ let program = root.as_program_node().unwrap();
487
+
488
+ let mut genv = GlobalEnv::new();
489
+ let mut lenv = LocalEnv::new();
490
+ let mut changes = ChangeSet::new();
491
+
492
+ for stmt in &program.statements().body() {
493
+ crate::analyzer::install::install_node(&mut genv, &mut lenv, &mut changes, source, &stmt);
494
+ }
495
+
496
+ let info = genv
497
+ .resolve_method(&Type::singleton("User"), "find")
498
+ .expect("User.find should be registered");
499
+ assert!(info.return_vertex.is_some());
500
+ assert_eq!(info.param_vertices.as_ref().unwrap().len(), 1);
501
+ }
502
+
503
+ #[test]
504
+ fn test_class_method_in_qualified_namespace() {
505
+ let source = r#"
506
+ module Api
507
+ class User
508
+ def self.create
509
+ "created"
510
+ end
511
+ end
512
+ end
513
+ "#;
514
+ let session = ParseSession::new();
515
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
516
+ let root = parse_result.node();
517
+ let program = root.as_program_node().unwrap();
518
+
519
+ let mut genv = GlobalEnv::new();
520
+ let mut lenv = LocalEnv::new();
521
+ let mut changes = ChangeSet::new();
522
+
523
+ for stmt in &program.statements().body() {
524
+ crate::analyzer::install::install_node(&mut genv, &mut lenv, &mut changes, source, &stmt);
525
+ }
526
+
527
+ let info = genv
528
+ .resolve_method(&Type::singleton("Api::User"), "create")
529
+ .expect("Api::User.create should be registered");
530
+ assert!(info.return_vertex.is_some());
531
+ }
532
+
533
+ #[test]
534
+ fn test_class_method_not_registered_as_instance() {
535
+ let source = r#"
536
+ class User
537
+ def self.create
538
+ "created"
539
+ end
540
+ end
541
+ "#;
542
+ let session = ParseSession::new();
543
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
544
+ let root = parse_result.node();
545
+ let program = root.as_program_node().unwrap();
546
+
547
+ let mut genv = GlobalEnv::new();
548
+ let mut lenv = LocalEnv::new();
549
+ let mut changes = ChangeSet::new();
550
+
551
+ for stmt in &program.statements().body() {
552
+ crate::analyzer::install::install_node(&mut genv, &mut lenv, &mut changes, source, &stmt);
553
+ }
554
+
555
+ // def self.create should NOT be registered as instance method
556
+ assert!(
557
+ genv.resolve_method(&Type::instance("User"), "create").is_none(),
558
+ "User#create should not exist — it's a class method"
559
+ );
560
+ }
561
+
562
+ #[test]
563
+ fn test_non_self_receiver_not_treated_as_class_method() {
564
+ let source = r#"
565
+ class User
566
+ def other.foo
567
+ "test"
568
+ end
569
+ end
570
+ "#;
571
+ let session = ParseSession::new();
572
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
573
+ let root = parse_result.node();
574
+ let program = root.as_program_node().unwrap();
575
+
576
+ let mut genv = GlobalEnv::new();
577
+ let mut lenv = LocalEnv::new();
578
+ let mut changes = ChangeSet::new();
579
+
580
+ for stmt in &program.statements().body() {
581
+ crate::analyzer::install::install_node(&mut genv, &mut lenv, &mut changes, source, &stmt);
582
+ }
583
+
584
+ // def other.foo should NOT be registered as singleton method
585
+ assert!(
586
+ genv.resolve_method(&Type::singleton("User"), "foo").is_none(),
587
+ "User.foo should not exist — receiver is not self"
588
+ );
589
+ }
590
+
591
+ #[test]
592
+ fn test_class_method_return_type_inference() {
593
+ let source = r#"
594
+ class User
595
+ def self.create
596
+ "created"
597
+ end
598
+ end
599
+ "#;
600
+ let session = ParseSession::new();
601
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
602
+ let root = parse_result.node();
603
+ let program = root.as_program_node().unwrap();
604
+
605
+ let mut genv = GlobalEnv::new();
606
+ let mut lenv = LocalEnv::new();
607
+ let mut changes = ChangeSet::new();
608
+
609
+ for stmt in &program.statements().body() {
610
+ crate::analyzer::install::install_node(&mut genv, &mut lenv, &mut changes, source, &stmt);
611
+ }
612
+
613
+ let info = genv
614
+ .resolve_method(&Type::singleton("User"), "create")
615
+ .expect("User.create should be registered");
616
+ let ret_vtx = info.return_vertex.expect("should have return vertex");
617
+
618
+ // Run solver to propagate types
619
+ genv.apply_changes(changes);
620
+ genv.run_all();
621
+
622
+ let vertex = genv.get_vertex(ret_vtx).or_else(|| {
623
+ // return vertex might be a source
624
+ None
625
+ });
626
+ if let Some(v) = vertex {
627
+ assert_eq!(v.show(), "String");
628
+ } else {
629
+ // Check if it's a source
630
+ let src = genv.get_source(ret_vtx).expect("should have source or vertex");
631
+ assert_eq!(src.ty, Type::string());
632
+ }
633
+ }
634
+
635
+ #[test]
636
+ fn test_class_method_in_reopened_class() {
637
+ let source = r#"
638
+ class User
639
+ def self.create
640
+ "created"
641
+ end
642
+ end
643
+
644
+ class User
645
+ def self.destroy
646
+ "destroyed"
647
+ end
648
+ end
649
+ "#;
650
+ let session = ParseSession::new();
651
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
652
+ let root = parse_result.node();
653
+ let program = root.as_program_node().unwrap();
654
+
655
+ let mut genv = GlobalEnv::new();
656
+ let mut lenv = LocalEnv::new();
657
+ let mut changes = ChangeSet::new();
658
+
659
+ for stmt in &program.statements().body() {
660
+ crate::analyzer::install::install_node(&mut genv, &mut lenv, &mut changes, source, &stmt);
661
+ }
662
+
663
+ // Both class methods should be registered
664
+ assert!(
665
+ genv.resolve_method(&Type::singleton("User"), "create").is_some(),
666
+ "User.create should be registered"
667
+ );
668
+ assert!(
669
+ genv.resolve_method(&Type::singleton("User"), "destroy").is_some(),
670
+ "User.destroy should be registered"
671
+ );
672
+ }
673
+
674
+ #[test]
675
+ fn test_class_method_param_type_propagation() {
676
+ let source = r#"
677
+ class User
678
+ def self.find(id)
679
+ id
680
+ end
681
+ end
682
+
683
+ User.find(42)
684
+ "#;
685
+ let session = ParseSession::new();
686
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
687
+ let root = parse_result.node();
688
+ let program = root.as_program_node().unwrap();
689
+
690
+ let mut genv = GlobalEnv::new();
691
+ let mut lenv = LocalEnv::new();
692
+ let mut changes = ChangeSet::new();
693
+
694
+ for stmt in &program.statements().body() {
695
+ crate::analyzer::install::install_node(&mut genv, &mut lenv, &mut changes, source, &stmt);
696
+ }
697
+
698
+ let info = genv
699
+ .resolve_method(&Type::singleton("User"), "find")
700
+ .expect("User.find should be registered");
701
+ let param_vtxs = info.param_vertices.as_ref().expect("should have param vertices");
702
+ assert_eq!(param_vtxs.len(), 1);
703
+
704
+ let param_vtx = param_vtxs[0];
705
+
706
+ // Run solver to propagate argument types
707
+ genv.apply_changes(changes);
708
+ genv.run_all();
709
+
710
+ // Parameter should have Integer type propagated from call site
711
+ let vertex = genv.get_vertex(param_vtx).expect("param vertex should exist");
712
+ assert_eq!(vertex.show(), "Integer");
713
+ }
337
714
  }
@@ -241,12 +241,8 @@ pub(crate) fn process_needs_child(
241
241
  block,
242
242
  arguments,
243
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 {
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() {
250
246
  genv.new_source(Type::instance(&name))
251
247
  } else {
252
248
  genv.new_source(Type::instance("Object"))
@@ -439,10 +435,10 @@ end
439
435
  "#;
440
436
  let genv = analyze(source);
441
437
 
442
- // Method registered with simple class name "User" (current behavior of definitions.rs)
438
+ // Method registered with qualified name "Api::V1::User"
443
439
  let info = genv
444
- .resolve_method(&Type::instance("User"), "greet")
445
- .expect("User#greet should be registered");
440
+ .resolve_method(&Type::instance("Api::V1::User"), "greet")
441
+ .expect("Api::V1::User#greet should be registered");
446
442
  assert!(info.return_vertex.is_some());
447
443
 
448
444
  let ret_vtx = info.return_vertex.unwrap();
@@ -685,10 +681,10 @@ end
685
681
  "#;
686
682
  let genv = analyze(source);
687
683
 
688
- // Registered with simple class name "User"
684
+ // Registered with qualified name "Api::User"
689
685
  let info = genv
690
- .resolve_method(&Type::instance("User"), "name")
691
- .expect("User#name should be registered");
686
+ .resolve_method(&Type::instance("Api::User"), "name")
687
+ .expect("Api::User#name should be registered");
692
688
  assert!(info.return_vertex.is_some());
693
689
  assert_eq!(get_type_show(&genv, info.return_vertex.unwrap()), "String");
694
690
  }
@@ -874,4 +870,27 @@ User.some_method
874
870
  genv.type_errors
875
871
  );
876
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
+ }
877
896
  }
@@ -13,6 +13,9 @@ use super::conditionals::{process_case_node, process_if_node, process_unless_nod
13
13
  use super::definitions::{process_class_node, process_def_node, process_module_node};
14
14
  use super::dispatch::{dispatch_needs_child, dispatch_simple, process_needs_child, DispatchResult};
15
15
  use super::literals::install_literal_node;
16
+ use super::parentheses::process_parentheses_node;
17
+ use super::operators::{process_and_node, process_or_node};
18
+ use super::returns::process_return_node;
16
19
 
17
20
  /// Build graph from AST (public API wrapper)
18
21
  pub struct AstInstaller<'a> {
@@ -76,6 +79,21 @@ pub(crate) fn install_node(
76
79
  return process_case_node(genv, lenv, changes, source, &case_node);
77
80
  }
78
81
 
82
+ if let Some(paren_node) = node.as_parentheses_node() {
83
+ return process_parentheses_node(genv, lenv, changes, source, &paren_node);
84
+ }
85
+
86
+ if let Some(return_node) = node.as_return_node() {
87
+ return process_return_node(genv, lenv, changes, source, &return_node);
88
+ }
89
+
90
+ if let Some(and_node) = node.as_and_node() {
91
+ return process_and_node(genv, lenv, changes, source, &and_node);
92
+ }
93
+ if let Some(or_node) = node.as_or_node() {
94
+ return process_or_node(genv, lenv, changes, source, &or_node);
95
+ }
96
+
79
97
  match dispatch_simple(genv, lenv, node) {
80
98
  DispatchResult::Vertex(vtx) => return Some(vtx),
81
99
  DispatchResult::NotHandled => {}
@@ -34,6 +34,30 @@ pub(crate) fn install_literal_node(
34
34
  return install_range_literal(genv, lenv, changes, source, &range_node);
35
35
  }
36
36
 
37
+ // InterpolatedStringNode: "Hello, #{expr}" → String
38
+ if let Some(interp) = node.as_interpolated_string_node() {
39
+ for part in &interp.parts() {
40
+ super::install::install_node(genv, lenv, changes, source, &part);
41
+ }
42
+ return Some(genv.new_source(Type::string()));
43
+ }
44
+
45
+ // InterpolatedSymbolNode: :"hello_#{expr}" → Symbol
46
+ if let Some(interp) = node.as_interpolated_symbol_node() {
47
+ for part in &interp.parts() {
48
+ super::install::install_node(genv, lenv, changes, source, &part);
49
+ }
50
+ return Some(genv.new_source(Type::symbol()));
51
+ }
52
+
53
+ // InterpolatedRegularExpressionNode: /hello #{expr}/ → Regexp
54
+ if let Some(interp) = node.as_interpolated_regular_expression_node() {
55
+ for part in &interp.parts() {
56
+ super::install::install_node(genv, lenv, changes, source, &part);
57
+ }
58
+ return Some(genv.new_source(Type::regexp()));
59
+ }
60
+
37
61
  install_simple_literal(genv, node)
38
62
  }
39
63
 
@@ -246,4 +270,25 @@ mod tests {
246
270
  let vtx = genv.new_source(Type::regexp());
247
271
  assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Regexp");
248
272
  }
273
+
274
+ #[test]
275
+ fn test_install_interpolated_string_literal() {
276
+ let mut genv = GlobalEnv::new();
277
+ let vtx = genv.new_source(Type::string());
278
+ assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "String");
279
+ }
280
+
281
+ #[test]
282
+ fn test_install_interpolated_symbol_literal() {
283
+ let mut genv = GlobalEnv::new();
284
+ let vtx = genv.new_source(Type::symbol());
285
+ assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Symbol");
286
+ }
287
+
288
+ #[test]
289
+ fn test_install_interpolated_regexp_literal() {
290
+ let mut genv = GlobalEnv::new();
291
+ let vtx = genv.new_source(Type::regexp());
292
+ assert_eq!(genv.get_source(vtx).unwrap().ty.show(), "Regexp");
293
+ }
249
294
  }
@@ -6,7 +6,10 @@ mod definitions;
6
6
  mod dispatch;
7
7
  mod install;
8
8
  mod literals;
9
+ mod operators;
9
10
  mod parameters;
11
+ mod parentheses;
12
+ mod returns;
10
13
  mod variables;
11
14
 
12
15
  pub use install::AstInstaller;