method-ray 0.1.6 → 0.1.8

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.
@@ -3,12 +3,15 @@
3
3
  //! This module handles the pattern matching of Ruby AST nodes
4
4
  //! and dispatches them to specialized handlers.
5
5
 
6
+ use std::collections::HashMap;
7
+
6
8
  use crate::env::{GlobalEnv, LocalEnv};
7
9
  use crate::graph::{BlockParameterTypeBox, ChangeSet, VertexId};
8
10
  use crate::source_map::SourceLocation;
9
11
  use crate::types::Type;
10
12
  use ruby_prism::Node;
11
13
 
14
+ use super::bytes_to_name;
12
15
  use super::calls::install_method_call;
13
16
  use super::variables::{
14
17
  install_ivar_read, install_ivar_write, install_local_var_read, install_local_var_write,
@@ -68,7 +71,7 @@ pub enum NeedsChildKind<'a> {
68
71
  pub fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &Node) -> DispatchResult {
69
72
  // Instance variable read: @name
70
73
  if let Some(ivar_read) = node.as_instance_variable_read_node() {
71
- let ivar_name = String::from_utf8_lossy(ivar_read.name().as_slice()).to_string();
74
+ let ivar_name = bytes_to_name(ivar_read.name().as_slice());
72
75
  return match install_ivar_read(genv, &ivar_name) {
73
76
  Some(vtx) => DispatchResult::Vertex(vtx),
74
77
  None => DispatchResult::NotHandled,
@@ -82,17 +85,19 @@ pub fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &Node) -
82
85
 
83
86
  // Local variable read: x
84
87
  if let Some(read_node) = node.as_local_variable_read_node() {
85
- let var_name = String::from_utf8_lossy(read_node.name().as_slice()).to_string();
88
+ let var_name = bytes_to_name(read_node.name().as_slice());
86
89
  return match install_local_var_read(lenv, &var_name) {
87
90
  Some(vtx) => DispatchResult::Vertex(vtx),
88
91
  None => DispatchResult::NotHandled,
89
92
  };
90
93
  }
91
94
 
92
- // ConstantReadNode: User → Type::Singleton("User")
95
+ // ConstantReadNode: User → Type::Singleton("User") or Type::Singleton("Api::User")
93
96
  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));
97
+ let name = bytes_to_name(const_read.name().as_slice());
98
+ let resolved_name = genv.scope_manager.lookup_constant(&name)
99
+ .unwrap_or(name);
100
+ let vtx = genv.new_source(Type::singleton(&resolved_name));
96
101
  return DispatchResult::Vertex(vtx);
97
102
  }
98
103
 
@@ -116,7 +121,7 @@ fn extract_symbol_names(call_node: &ruby_prism::CallNode) -> Vec<String> {
116
121
  .iter()
117
122
  .filter_map(|arg| {
118
123
  arg.as_symbol_node().map(|sym| {
119
- String::from_utf8_lossy(&sym.unescaped()).to_string()
124
+ bytes_to_name(sym.unescaped())
120
125
  })
121
126
  })
122
127
  .collect()
@@ -128,7 +133,7 @@ fn extract_symbol_names(call_node: &ruby_prism::CallNode) -> Vec<String> {
128
133
  pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsChildKind<'a>> {
129
134
  // Instance variable write: @name = value
130
135
  if let Some(ivar_write) = node.as_instance_variable_write_node() {
131
- let ivar_name = String::from_utf8_lossy(ivar_write.name().as_slice()).to_string();
136
+ let ivar_name = bytes_to_name(ivar_write.name().as_slice());
132
137
  return Some(NeedsChildKind::IvarWrite {
133
138
  ivar_name,
134
139
  value: ivar_write.value(),
@@ -137,7 +142,7 @@ pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsCh
137
142
 
138
143
  // Local variable write: x = value
139
144
  if let Some(write_node) = node.as_local_variable_write_node() {
140
- let var_name = String::from_utf8_lossy(write_node.name().as_slice()).to_string();
145
+ let var_name = bytes_to_name(write_node.name().as_slice());
141
146
  return Some(NeedsChildKind::LocalVarWrite {
142
147
  var_name,
143
148
  value: write_node.value(),
@@ -146,7 +151,7 @@ pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsCh
146
151
 
147
152
  // Method call: x.upcase, x.each { |i| ... }, or name (implicit self)
148
153
  if let Some(call_node) = node.as_call_node() {
149
- let method_name = String::from_utf8_lossy(call_node.name().as_slice()).to_string();
154
+ let method_name = bytes_to_name(call_node.name().as_slice());
150
155
  let block = call_node.block();
151
156
  let arguments: Vec<Node<'a>> = call_node
152
157
  .arguments()
@@ -232,7 +237,8 @@ pub(crate) fn process_needs_child(
232
237
  } => {
233
238
  let recv_vtx = super::install::install_node(genv, lenv, changes, source, &receiver)?;
234
239
  process_method_call_common(
235
- genv, lenv, changes, source, recv_vtx, method_name, location, block, arguments,
240
+ genv, lenv, changes, source,
241
+ MethodCallContext { recv_vtx, method_name, location, block, arguments },
236
242
  )
237
243
  }
238
244
  NeedsChildKind::ImplicitSelfCall {
@@ -248,7 +254,8 @@ pub(crate) fn process_needs_child(
248
254
  genv.new_source(Type::instance("Object"))
249
255
  };
250
256
  process_method_call_common(
251
- genv, lenv, changes, source, recv_vtx, method_name, location, block, arguments,
257
+ genv, lenv, changes, source,
258
+ MethodCallContext { recv_vtx, method_name, location, block, arguments },
252
259
  )
253
260
  }
254
261
  NeedsChildKind::AttrDeclaration { kind, attr_names } => {
@@ -274,6 +281,15 @@ fn finish_local_var_write(
274
281
  install_local_var_write(genv, lenv, changes, var_name, value_vtx)
275
282
  }
276
283
 
284
+ /// Bundled parameters for method call processing
285
+ struct MethodCallContext<'a> {
286
+ recv_vtx: VertexId,
287
+ method_name: String,
288
+ location: SourceLocation,
289
+ block: Option<Node<'a>>,
290
+ arguments: Vec<Node<'a>>,
291
+ }
292
+
277
293
  /// MethodCall / ImplicitSelfCall common processing:
278
294
  /// Handles argument processing, block processing, and MethodCallBox creation after recv_vtx is obtained
279
295
  fn process_method_call_common<'a>(
@@ -281,16 +297,44 @@ fn process_method_call_common<'a>(
281
297
  lenv: &mut LocalEnv,
282
298
  changes: &mut ChangeSet,
283
299
  source: &str,
284
- recv_vtx: VertexId,
285
- method_name: String,
286
- location: SourceLocation,
287
- block: Option<Node<'a>>,
288
- arguments: Vec<Node<'a>>,
300
+ ctx: MethodCallContext<'a>,
289
301
  ) -> 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();
302
+ let MethodCallContext {
303
+ recv_vtx,
304
+ method_name,
305
+ location,
306
+ block,
307
+ arguments,
308
+ } = ctx;
309
+ if method_name == "!" {
310
+ return Some(super::operators::process_not_operator(genv));
311
+ }
312
+
313
+ // Separate positional arguments and keyword arguments
314
+ let mut positional_arg_vtxs: Vec<VertexId> = Vec::new();
315
+ let mut keyword_arg_vtxs: HashMap<String, VertexId> = HashMap::new();
316
+
317
+ for arg in &arguments {
318
+ if let Some(kw_hash) = arg.as_keyword_hash_node() {
319
+ for element in kw_hash.elements().iter() {
320
+ let assoc = match element.as_assoc_node() {
321
+ Some(a) => a,
322
+ None => continue,
323
+ };
324
+ let name = match assoc.key().as_symbol_node() {
325
+ Some(sym) => bytes_to_name(sym.unescaped()),
326
+ None => continue,
327
+ };
328
+ if let Some(vtx) =
329
+ super::install::install_node(genv, lenv, changes, source, &assoc.value())
330
+ {
331
+ keyword_arg_vtxs.insert(name, vtx);
332
+ }
333
+ }
334
+ } else if let Some(vtx) = super::install::install_node(genv, lenv, changes, source, arg) {
335
+ positional_arg_vtxs.push(vtx);
336
+ }
337
+ }
294
338
 
295
339
  if let Some(block_node) = block {
296
340
  if let Some(block) = block_node.as_block_node() {
@@ -311,8 +355,19 @@ fn process_method_call_common<'a>(
311
355
  }
312
356
  }
313
357
 
358
+ let kwarg_vtxs = if keyword_arg_vtxs.is_empty() {
359
+ None
360
+ } else {
361
+ Some(keyword_arg_vtxs)
362
+ };
363
+
314
364
  Some(finish_method_call(
315
- genv, recv_vtx, method_name, arg_vtxs, location,
365
+ genv,
366
+ recv_vtx,
367
+ method_name,
368
+ positional_arg_vtxs,
369
+ kwarg_vtxs,
370
+ location,
316
371
  ))
317
372
  }
318
373
 
@@ -322,9 +377,10 @@ fn finish_method_call(
322
377
  recv_vtx: VertexId,
323
378
  method_name: String,
324
379
  arg_vtxs: Vec<VertexId>,
380
+ kwarg_vtxs: Option<HashMap<String, VertexId>>,
325
381
  location: SourceLocation,
326
382
  ) -> VertexId {
327
- install_method_call(genv, recv_vtx, method_name, arg_vtxs, Some(location))
383
+ install_method_call(genv, recv_vtx, method_name, arg_vtxs, kwarg_vtxs, Some(location))
328
384
  }
329
385
 
330
386
  #[cfg(test)]
@@ -463,8 +519,8 @@ end
463
519
 
464
520
  // Utils.run should be registered
465
521
  let info = genv
466
- .resolve_method(&Type::instance("Utils"), "run")
467
- .expect("Utils#run should be registered");
522
+ .resolve_method(&Type::singleton("Utils"), "run")
523
+ .expect("Utils.run should be registered");
468
524
  assert!(info.return_vertex.is_some());
469
525
  }
470
526
 
@@ -893,4 +949,189 @@ Api::User.new.name
893
949
  genv.type_errors
894
950
  );
895
951
  }
952
+
953
+ // Test 23: ConstantReadNode inside module resolves to qualified name
954
+ #[test]
955
+ fn test_constant_read_inside_module_resolves_qualified() {
956
+ let source = r#"
957
+ module Api
958
+ class User
959
+ def name
960
+ "Alice"
961
+ end
962
+ end
963
+
964
+ class Service
965
+ def run
966
+ User.new.name
967
+ end
968
+ end
969
+ end
970
+ "#;
971
+ let genv = analyze(source);
972
+ assert!(
973
+ genv.type_errors.is_empty(),
974
+ "User.new inside module Api should resolve to Api::User: {:?}",
975
+ genv.type_errors
976
+ );
977
+ }
978
+
979
+ // Test 24: ConstantReadNode in deeply nested modules
980
+ #[test]
981
+ fn test_constant_read_deeply_nested() {
982
+ let source = r#"
983
+ module Api
984
+ module V1
985
+ class User
986
+ def name
987
+ "Alice"
988
+ end
989
+ end
990
+
991
+ class Service
992
+ def run
993
+ User.new.name
994
+ end
995
+ end
996
+ end
997
+ end
998
+ "#;
999
+ let genv = analyze(source);
1000
+ assert!(
1001
+ genv.type_errors.is_empty(),
1002
+ "User.new inside Api::V1 should resolve to Api::V1::User: {:?}",
1003
+ genv.type_errors
1004
+ );
1005
+ }
1006
+
1007
+ // Test 25: Same constant name in different modules
1008
+ #[test]
1009
+ fn test_constant_read_same_name_different_modules() {
1010
+ let source = r#"
1011
+ module Api
1012
+ class User
1013
+ def name; "Api User"; end
1014
+ end
1015
+ end
1016
+
1017
+ module Admin
1018
+ class User
1019
+ def name; "Admin User"; end
1020
+ end
1021
+
1022
+ class Service
1023
+ def run
1024
+ User.new.name
1025
+ end
1026
+ end
1027
+ end
1028
+ "#;
1029
+ let genv = analyze(source);
1030
+ assert!(
1031
+ genv.type_errors.is_empty(),
1032
+ "User.new inside Admin should resolve to Admin::User: {:?}",
1033
+ genv.type_errors
1034
+ );
1035
+ }
1036
+
1037
+ // === Keyword argument tests ===
1038
+
1039
+ // Test 26: Required keyword argument type propagation
1040
+ #[test]
1041
+ fn test_keyword_arg_required_propagation() {
1042
+ let source = r#"
1043
+ class Greeter
1044
+ def greet(name:)
1045
+ name
1046
+ end
1047
+ end
1048
+
1049
+ Greeter.new.greet(name: "Alice")
1050
+ "#;
1051
+ let genv = analyze(source);
1052
+
1053
+ let info = genv
1054
+ .resolve_method(&Type::instance("Greeter"), "greet")
1055
+ .expect("Greeter#greet should be registered");
1056
+ let ret_vtx = info.return_vertex.unwrap();
1057
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
1058
+ }
1059
+
1060
+ // Test 27: Optional keyword argument with default type
1061
+ #[test]
1062
+ fn test_keyword_arg_optional_default_type() {
1063
+ let source = r#"
1064
+ class Counter
1065
+ def count(step: 1)
1066
+ step
1067
+ end
1068
+ end
1069
+ "#;
1070
+ let genv = analyze(source);
1071
+
1072
+ let info = genv
1073
+ .resolve_method(&Type::instance("Counter"), "count")
1074
+ .expect("Counter#count should be registered");
1075
+ let ret_vtx = info.return_vertex.unwrap();
1076
+ // step has Integer type from default value
1077
+ assert_eq!(get_type_show(&genv, ret_vtx), "Integer");
1078
+ }
1079
+
1080
+ // Test 28: Positional and keyword arguments mixed
1081
+ #[test]
1082
+ fn test_positional_and_keyword_mixed() {
1083
+ let source = r#"
1084
+ class User
1085
+ def initialize(id, name:)
1086
+ @id = id
1087
+ @name = name
1088
+ end
1089
+ end
1090
+
1091
+ User.new(1, name: "Alice")
1092
+ "#;
1093
+ let genv = analyze(source);
1094
+ assert!(genv.type_errors.is_empty());
1095
+ }
1096
+
1097
+ // Test 29: Keyword argument via .new propagation to initialize
1098
+ #[test]
1099
+ fn test_keyword_arg_via_new_to_initialize() {
1100
+ let source = r#"
1101
+ class Config
1102
+ def initialize(debug:)
1103
+ @debug = debug
1104
+ end
1105
+
1106
+ def debug?
1107
+ @debug
1108
+ end
1109
+ end
1110
+
1111
+ Config.new(debug: true)
1112
+ "#;
1113
+ let genv = analyze(source);
1114
+ assert!(genv.type_errors.is_empty());
1115
+ }
1116
+
1117
+ // Test 30: Multiple keyword arguments
1118
+ #[test]
1119
+ fn test_multiple_keyword_args() {
1120
+ let source = r#"
1121
+ class User
1122
+ def profile(name:, age:)
1123
+ name
1124
+ end
1125
+ end
1126
+
1127
+ User.new.profile(name: "Alice", age: 30)
1128
+ "#;
1129
+ let genv = analyze(source);
1130
+
1131
+ let info = genv
1132
+ .resolve_method(&Type::instance("User"), "profile")
1133
+ .expect("User#profile should be registered");
1134
+ let ret_vtx = info.return_vertex.unwrap();
1135
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
1136
+ }
896
1137
  }