method-ray 0.1.7 → 0.1.9

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/{rust → core}/Cargo.toml +1 -1
  4. data/core/src/analyzer/assignments.rs +499 -0
  5. data/{rust → core}/src/analyzer/attributes.rs +2 -1
  6. data/{rust → core}/src/analyzer/blocks.rs +140 -0
  7. data/{rust → core}/src/analyzer/calls.rs +7 -3
  8. data/{rust → core}/src/analyzer/definitions.rs +12 -7
  9. data/{rust → core}/src/analyzer/dispatch.rs +431 -13
  10. data/core/src/analyzer/exceptions.rs +622 -0
  11. data/{rust → core}/src/analyzer/install.rs +37 -1
  12. data/{rust → core}/src/analyzer/literals.rs +3 -17
  13. data/core/src/analyzer/loops.rs +301 -0
  14. data/{rust → core}/src/analyzer/mod.rs +4 -0
  15. data/{rust → core}/src/analyzer/operators.rs +119 -27
  16. data/{rust → core}/src/analyzer/parameters.rs +214 -5
  17. data/core/src/analyzer/super_calls.rs +285 -0
  18. data/{rust → core}/src/cache/rbs_cache.rs +0 -1
  19. data/{rust → core}/src/cli/commands.rs +3 -3
  20. data/{rust → core}/src/diagnostics/diagnostic.rs +0 -3
  21. data/{rust → core}/src/diagnostics/formatter.rs +0 -1
  22. data/{rust → core}/src/env/box_manager.rs +2 -4
  23. data/{rust → core}/src/env/global_env.rs +28 -7
  24. data/{rust → core}/src/env/local_env.rs +35 -1
  25. data/{rust → core}/src/env/method_registry.rs +117 -25
  26. data/{rust → core}/src/env/scope.rs +91 -4
  27. data/{rust → core}/src/env/vertex_manager.rs +0 -1
  28. data/{rust → core}/src/graph/box.rs +134 -8
  29. data/{rust → core}/src/graph/change_set.rs +14 -0
  30. data/{rust → core}/src/lsp/server.rs +1 -1
  31. data/{rust → core}/src/rbs/loader.rs +1 -2
  32. data/{rust → core}/src/source_map.rs +0 -1
  33. data/{rust → core}/src/types.rs +11 -1
  34. data/ext/Cargo.toml +2 -2
  35. data/lib/methodray/binary_locator.rb +2 -2
  36. data/lib/methodray/commands.rb +1 -1
  37. data/lib/methodray/version.rb +1 -1
  38. metadata +54 -50
  39. /data/{rust → core}/src/analyzer/conditionals.rs +0 -0
  40. /data/{rust → core}/src/analyzer/parentheses.rs +0 -0
  41. /data/{rust → core}/src/analyzer/returns.rs +0 -0
  42. /data/{rust → core}/src/analyzer/variables.rs +0 -0
  43. /data/{rust → core}/src/cache/mod.rs +0 -0
  44. /data/{rust → core}/src/checker.rs +0 -0
  45. /data/{rust → core}/src/cli/args.rs +0 -0
  46. /data/{rust → core}/src/cli/mod.rs +0 -0
  47. /data/{rust → core}/src/diagnostics/mod.rs +0 -0
  48. /data/{rust → core}/src/env/mod.rs +0 -0
  49. /data/{rust → core}/src/env/type_error.rs +0 -0
  50. /data/{rust → core}/src/graph/mod.rs +0 -0
  51. /data/{rust → core}/src/graph/vertex.rs +0 -0
  52. /data/{rust → core}/src/lib.rs +0 -0
  53. /data/{rust → core}/src/lsp/diagnostics.rs +0 -0
  54. /data/{rust → core}/src/lsp/main.rs +0 -0
  55. /data/{rust → core}/src/lsp/mod.rs +0 -0
  56. /data/{rust → core}/src/main.rs +0 -0
  57. /data/{rust → core}/src/parser.rs +0 -0
  58. /data/{rust → core}/src/rbs/converter.rs +0 -0
  59. /data/{rust → core}/src/rbs/error.rs +0 -0
  60. /data/{rust → core}/src/rbs/mod.rs +0 -0
@@ -3,6 +3,8 @@
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;
@@ -16,6 +18,45 @@ use super::variables::{
16
18
  install_self,
17
19
  };
18
20
 
21
+ /// Collect positional and keyword arguments from AST argument nodes.
22
+ ///
23
+ /// Shared by method calls (`dispatch.rs`) and super calls (`super_calls.rs`).
24
+ pub(crate) fn collect_arguments<'a>(
25
+ genv: &mut GlobalEnv,
26
+ lenv: &mut LocalEnv,
27
+ changes: &mut ChangeSet,
28
+ source: &str,
29
+ args: impl Iterator<Item = ruby_prism::Node<'a>>,
30
+ ) -> (Vec<VertexId>, Option<HashMap<String, VertexId>>) {
31
+ let mut positional: Vec<VertexId> = Vec::new();
32
+ let mut keyword: HashMap<String, VertexId> = HashMap::new();
33
+
34
+ for arg in args {
35
+ if let Some(kw_hash) = arg.as_keyword_hash_node() {
36
+ for element in kw_hash.elements().iter() {
37
+ let assoc = match element.as_assoc_node() {
38
+ Some(a) => a,
39
+ None => continue,
40
+ };
41
+ let name = match assoc.key().as_symbol_node() {
42
+ Some(sym) => bytes_to_name(sym.unescaped()),
43
+ None => continue,
44
+ };
45
+ if let Some(vtx) =
46
+ super::install::install_node(genv, lenv, changes, source, &assoc.value())
47
+ {
48
+ keyword.insert(name, vtx);
49
+ }
50
+ }
51
+ } else if let Some(vtx) = super::install::install_node(genv, lenv, changes, source, &arg) {
52
+ positional.push(vtx);
53
+ }
54
+ }
55
+
56
+ let kw = (!keyword.is_empty()).then_some(keyword);
57
+ (positional, kw)
58
+ }
59
+
19
60
  /// Kind of attr_* declaration
20
61
  #[derive(Debug, Clone, Copy)]
21
62
  pub enum AttrKind {
@@ -60,6 +101,10 @@ pub enum NeedsChildKind<'a> {
60
101
  kind: AttrKind,
61
102
  attr_names: Vec<String>,
62
103
  },
104
+ /// include declaration: `include Greetable, Enumerable`
105
+ IncludeDeclaration {
106
+ module_names: Vec<String>,
107
+ },
63
108
  }
64
109
 
65
110
  /// First pass: check if node can be handled immediately without child processing
@@ -187,6 +232,25 @@ pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsCh
187
232
  return None;
188
233
  }
189
234
 
235
+ if method_name == "include" {
236
+ let module_names: Vec<String> = call_node
237
+ .arguments()
238
+ .map(|args| {
239
+ args.arguments()
240
+ .iter()
241
+ .filter_map(|arg| {
242
+ super::definitions::extract_constant_path(&arg)
243
+ })
244
+ .collect()
245
+ })
246
+ .unwrap_or_default();
247
+
248
+ if !module_names.is_empty() {
249
+ return Some(NeedsChildKind::IncludeDeclaration { module_names });
250
+ }
251
+ return None;
252
+ }
253
+
190
254
  let prism_location = call_node
191
255
  .message_loc()
192
256
  .unwrap_or_else(|| node.location());
@@ -235,7 +299,8 @@ pub(crate) fn process_needs_child(
235
299
  } => {
236
300
  let recv_vtx = super::install::install_node(genv, lenv, changes, source, &receiver)?;
237
301
  process_method_call_common(
238
- genv, lenv, changes, source, recv_vtx, method_name, location, block, arguments,
302
+ genv, lenv, changes, source,
303
+ MethodCallContext { recv_vtx, method_name, location, block, arguments },
239
304
  )
240
305
  }
241
306
  NeedsChildKind::ImplicitSelfCall {
@@ -251,13 +316,24 @@ pub(crate) fn process_needs_child(
251
316
  genv.new_source(Type::instance("Object"))
252
317
  };
253
318
  process_method_call_common(
254
- genv, lenv, changes, source, recv_vtx, method_name, location, block, arguments,
319
+ genv, lenv, changes, source,
320
+ MethodCallContext { recv_vtx, method_name, location, block, arguments },
255
321
  )
256
322
  }
257
323
  NeedsChildKind::AttrDeclaration { kind, attr_names } => {
258
324
  super::attributes::process_attr_declaration(genv, kind, attr_names);
259
325
  None
260
326
  }
327
+ NeedsChildKind::IncludeDeclaration { module_names } => {
328
+ if let Some(class_name) = genv.scope_manager.current_qualified_name() {
329
+ // Ruby processes `include A, B` right-to-left (B first, then A on top),
330
+ // so A ends up with higher MRO priority. Reverse to match this behavior.
331
+ for module_name in module_names.iter().rev() {
332
+ genv.record_include(&class_name, module_name);
333
+ }
334
+ }
335
+ None
336
+ }
261
337
  }
262
338
  }
263
339
 
@@ -277,6 +353,15 @@ fn finish_local_var_write(
277
353
  install_local_var_write(genv, lenv, changes, var_name, value_vtx)
278
354
  }
279
355
 
356
+ /// Bundled parameters for method call processing
357
+ struct MethodCallContext<'a> {
358
+ recv_vtx: VertexId,
359
+ method_name: String,
360
+ location: SourceLocation,
361
+ block: Option<Node<'a>>,
362
+ arguments: Vec<Node<'a>>,
363
+ }
364
+
280
365
  /// MethodCall / ImplicitSelfCall common processing:
281
366
  /// Handles argument processing, block processing, and MethodCallBox creation after recv_vtx is obtained
282
367
  fn process_method_call_common<'a>(
@@ -284,16 +369,21 @@ fn process_method_call_common<'a>(
284
369
  lenv: &mut LocalEnv,
285
370
  changes: &mut ChangeSet,
286
371
  source: &str,
287
- recv_vtx: VertexId,
288
- method_name: String,
289
- location: SourceLocation,
290
- block: Option<Node<'a>>,
291
- arguments: Vec<Node<'a>>,
372
+ ctx: MethodCallContext<'a>,
292
373
  ) -> Option<VertexId> {
293
- let arg_vtxs: Vec<VertexId> = arguments
294
- .iter()
295
- .filter_map(|arg| super::install::install_node(genv, lenv, changes, source, arg))
296
- .collect();
374
+ let MethodCallContext {
375
+ recv_vtx,
376
+ method_name,
377
+ location,
378
+ block,
379
+ arguments,
380
+ } = ctx;
381
+ if method_name == "!" {
382
+ return Some(super::operators::process_not_operator(genv));
383
+ }
384
+
385
+ let (positional_arg_vtxs, kwarg_vtxs) =
386
+ collect_arguments(genv, lenv, changes, source, arguments.into_iter());
297
387
 
298
388
  if let Some(block_node) = block {
299
389
  if let Some(block) = block_node.as_block_node() {
@@ -315,7 +405,12 @@ fn process_method_call_common<'a>(
315
405
  }
316
406
 
317
407
  Some(finish_method_call(
318
- genv, recv_vtx, method_name, arg_vtxs, location,
408
+ genv,
409
+ recv_vtx,
410
+ method_name,
411
+ positional_arg_vtxs,
412
+ kwarg_vtxs,
413
+ location,
319
414
  ))
320
415
  }
321
416
 
@@ -325,9 +420,10 @@ fn finish_method_call(
325
420
  recv_vtx: VertexId,
326
421
  method_name: String,
327
422
  arg_vtxs: Vec<VertexId>,
423
+ kwarg_vtxs: Option<HashMap<String, VertexId>>,
328
424
  location: SourceLocation,
329
425
  ) -> VertexId {
330
- install_method_call(genv, recv_vtx, method_name, arg_vtxs, Some(location))
426
+ install_method_call(genv, recv_vtx, method_name, arg_vtxs, kwarg_vtxs, Some(location))
331
427
  }
332
428
 
333
429
  #[cfg(test)]
@@ -980,4 +1076,326 @@ end
980
1076
  genv.type_errors
981
1077
  );
982
1078
  }
1079
+
1080
+ // === Keyword argument tests ===
1081
+
1082
+ // Test 26: Required keyword argument type propagation
1083
+ #[test]
1084
+ fn test_keyword_arg_required_propagation() {
1085
+ let source = r#"
1086
+ class Greeter
1087
+ def greet(name:)
1088
+ name
1089
+ end
1090
+ end
1091
+
1092
+ Greeter.new.greet(name: "Alice")
1093
+ "#;
1094
+ let genv = analyze(source);
1095
+
1096
+ let info = genv
1097
+ .resolve_method(&Type::instance("Greeter"), "greet")
1098
+ .expect("Greeter#greet should be registered");
1099
+ let ret_vtx = info.return_vertex.unwrap();
1100
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
1101
+ }
1102
+
1103
+ // Test 27: Optional keyword argument with default type
1104
+ #[test]
1105
+ fn test_keyword_arg_optional_default_type() {
1106
+ let source = r#"
1107
+ class Counter
1108
+ def count(step: 1)
1109
+ step
1110
+ end
1111
+ end
1112
+ "#;
1113
+ let genv = analyze(source);
1114
+
1115
+ let info = genv
1116
+ .resolve_method(&Type::instance("Counter"), "count")
1117
+ .expect("Counter#count should be registered");
1118
+ let ret_vtx = info.return_vertex.unwrap();
1119
+ // step has Integer type from default value
1120
+ assert_eq!(get_type_show(&genv, ret_vtx), "Integer");
1121
+ }
1122
+
1123
+ // Test 28: Positional and keyword arguments mixed
1124
+ #[test]
1125
+ fn test_positional_and_keyword_mixed() {
1126
+ let source = r#"
1127
+ class User
1128
+ def initialize(id, name:)
1129
+ @id = id
1130
+ @name = name
1131
+ end
1132
+ end
1133
+
1134
+ User.new(1, name: "Alice")
1135
+ "#;
1136
+ let genv = analyze(source);
1137
+ assert!(genv.type_errors.is_empty());
1138
+ }
1139
+
1140
+ // Test 29: Keyword argument via .new propagation to initialize
1141
+ #[test]
1142
+ fn test_keyword_arg_via_new_to_initialize() {
1143
+ let source = r#"
1144
+ class Config
1145
+ def initialize(debug:)
1146
+ @debug = debug
1147
+ end
1148
+
1149
+ def debug?
1150
+ @debug
1151
+ end
1152
+ end
1153
+
1154
+ Config.new(debug: true)
1155
+ "#;
1156
+ let genv = analyze(source);
1157
+ assert!(genv.type_errors.is_empty());
1158
+ }
1159
+
1160
+ // Test 30: Multiple keyword arguments
1161
+ #[test]
1162
+ fn test_multiple_keyword_args() {
1163
+ let source = r#"
1164
+ class User
1165
+ def profile(name:, age:)
1166
+ name
1167
+ end
1168
+ end
1169
+
1170
+ User.new.profile(name: "Alice", age: 30)
1171
+ "#;
1172
+ let genv = analyze(source);
1173
+
1174
+ let info = genv
1175
+ .resolve_method(&Type::instance("User"), "profile")
1176
+ .expect("User#profile should be registered");
1177
+ let ret_vtx = info.return_vertex.unwrap();
1178
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
1179
+ }
1180
+
1181
+ // === Include (mixin) tests ===
1182
+
1183
+ // Test 31: Basic include — module method resolved on class instance
1184
+ #[test]
1185
+ fn test_include_basic() {
1186
+ let source = r#"
1187
+ module Greetable
1188
+ def greet
1189
+ "Hello!"
1190
+ end
1191
+ end
1192
+
1193
+ class User
1194
+ include Greetable
1195
+ end
1196
+
1197
+ User.new.greet
1198
+ "#;
1199
+ let genv = analyze(source);
1200
+ assert!(
1201
+ genv.type_errors.is_empty(),
1202
+ "include should resolve Greetable#greet: {:?}",
1203
+ genv.type_errors
1204
+ );
1205
+ }
1206
+
1207
+ // Test 32: Include method return type inference
1208
+ #[test]
1209
+ fn test_include_method_return_type() {
1210
+ let source = r#"
1211
+ module Greetable
1212
+ def greet
1213
+ "Hello!"
1214
+ end
1215
+ end
1216
+
1217
+ class User
1218
+ include Greetable
1219
+
1220
+ def say_hello
1221
+ greet
1222
+ end
1223
+ end
1224
+ "#;
1225
+ let genv = analyze(source);
1226
+
1227
+ let info = genv
1228
+ .resolve_method(&Type::instance("User"), "say_hello")
1229
+ .expect("User#say_hello should be registered");
1230
+ let ret_vtx = info.return_vertex.unwrap();
1231
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
1232
+ }
1233
+
1234
+ // Test 33: Multiple includes — last included module has priority
1235
+ #[test]
1236
+ fn test_include_multiple_modules() {
1237
+ let source = r#"
1238
+ module A
1239
+ def foo
1240
+ "from A"
1241
+ end
1242
+ end
1243
+
1244
+ module B
1245
+ def foo
1246
+ 42
1247
+ end
1248
+ end
1249
+
1250
+ class User
1251
+ include A
1252
+ include B
1253
+ end
1254
+
1255
+ User.new.foo
1256
+ "#;
1257
+ let genv = analyze(source);
1258
+ assert!(genv.type_errors.is_empty());
1259
+
1260
+ // B is included last → B#foo (Integer) should be resolved
1261
+ let info = genv
1262
+ .resolve_method(&Type::instance("User"), "foo")
1263
+ .expect("User#foo should be resolved via include");
1264
+ let ret_vtx = info.return_vertex.unwrap();
1265
+ assert_eq!(get_type_show(&genv, ret_vtx), "Integer");
1266
+ }
1267
+
1268
+ // Test 34: Class's own method takes priority over included module
1269
+ #[test]
1270
+ fn test_include_class_method_priority() {
1271
+ let source = r#"
1272
+ module Greetable
1273
+ def greet
1274
+ "Hello from module!"
1275
+ end
1276
+ end
1277
+
1278
+ class User
1279
+ include Greetable
1280
+
1281
+ def greet
1282
+ 42
1283
+ end
1284
+ end
1285
+
1286
+ User.new.greet
1287
+ "#;
1288
+ let genv = analyze(source);
1289
+
1290
+ let info = genv
1291
+ .resolve_method(&Type::instance("User"), "greet")
1292
+ .expect("User#greet should be resolved");
1293
+ let ret_vtx = info.return_vertex.unwrap();
1294
+ // Class's own method (Integer) takes priority
1295
+ assert_eq!(get_type_show(&genv, ret_vtx), "Integer");
1296
+ }
1297
+
1298
+ // Test 35: Include with qualified module name
1299
+ #[test]
1300
+ fn test_include_qualified_module() {
1301
+ let source = r#"
1302
+ module Api
1303
+ module Helpers
1304
+ def help
1305
+ "help"
1306
+ end
1307
+ end
1308
+ end
1309
+
1310
+ class User
1311
+ include Api::Helpers
1312
+ end
1313
+
1314
+ User.new.help
1315
+ "#;
1316
+ let genv = analyze(source);
1317
+ assert!(
1318
+ genv.type_errors.is_empty(),
1319
+ "include Api::Helpers should resolve: {:?}",
1320
+ genv.type_errors
1321
+ );
1322
+ }
1323
+
1324
+ // Test 36: Include with simultaneous multiple modules
1325
+ #[test]
1326
+ fn test_include_simultaneous_multiple() {
1327
+ let source = r#"
1328
+ module A
1329
+ def a_method
1330
+ "a"
1331
+ end
1332
+ end
1333
+
1334
+ module B
1335
+ def b_method
1336
+ 42
1337
+ end
1338
+ end
1339
+
1340
+ class User
1341
+ include A, B
1342
+ end
1343
+
1344
+ User.new.a_method
1345
+ User.new.b_method
1346
+ "#;
1347
+ let genv = analyze(source);
1348
+ assert!(
1349
+ genv.type_errors.is_empty(),
1350
+ "include A, B should resolve both modules: {:?}",
1351
+ genv.type_errors
1352
+ );
1353
+ }
1354
+
1355
+ // Test 37: Include with unknown module (no crash)
1356
+ #[test]
1357
+ fn test_include_unknown_module() {
1358
+ let source = r#"
1359
+ class User
1360
+ include UnknownModule
1361
+ end
1362
+ "#;
1363
+ let genv = analyze(source);
1364
+ // Should not panic; unknown module is recorded but methods won't resolve
1365
+ let _ = genv;
1366
+ }
1367
+
1368
+ // Test 38: Simultaneous include A, B — A has higher MRO priority (Ruby semantics)
1369
+ #[test]
1370
+ fn test_include_simultaneous_order() {
1371
+ let source = r#"
1372
+ module A
1373
+ def foo
1374
+ "from A"
1375
+ end
1376
+ end
1377
+
1378
+ module B
1379
+ def foo
1380
+ 42
1381
+ end
1382
+ end
1383
+
1384
+ class User
1385
+ include A, B
1386
+ end
1387
+
1388
+ User.new.foo
1389
+ "#;
1390
+ let genv = analyze(source);
1391
+ assert!(genv.type_errors.is_empty());
1392
+
1393
+ // Ruby's `include A, B` processes right-to-left: B first, then A on top.
1394
+ // A has higher MRO priority → A#foo (String) should be resolved.
1395
+ let info = genv
1396
+ .resolve_method(&Type::instance("User"), "foo")
1397
+ .expect("User#foo should be resolved via include");
1398
+ let ret_vtx = info.return_vertex.unwrap();
1399
+ assert_eq!(get_type_show(&genv, ret_vtx), "String");
1400
+ }
983
1401
  }