method-ray 0.1.7 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b5b60b4408b5e547cb40911fb0eb68d4dce182bc32fdf1ce3e6c8cd25f24975
4
- data.tar.gz: 44f0b585ad8f2c83568d0df520711b2c1d09affd6fded8e35fbc3f27b082f073
3
+ metadata.gz: f44709f8354439522722c8ea65da30463fae9de68d9be1aa07c8dac9627a03ce
4
+ data.tar.gz: 2a09885854020892cf93a1c85e83735a23be60240fcbee09f77fa8c9fbf58801
5
5
  SHA512:
6
- metadata.gz: 1a7988edfb76b4fe4d1e03b8558104cc378299ebf8651b79dfc743e0e48b9cd4a04d235be1ca054f3ad172f9b7a8b0689e4bf99250935c02ab5cedb0737f1340
7
- data.tar.gz: b6d94d5ce791e5b8317a880e986d60618a017434bc5823412dba3c8a04dcf3fb25089dff2a86e3662ecb7c2cdd13790cd32d5b18bc73960cf488ad8e8849a35b
6
+ metadata.gz: 4be6a23881adf54e88039c4e62255f09aa52005cf4085e6cbdc1910f642dae8e763ec7993445051bd3cff20693b5576e417f47b0864359c60db6becf1c21e204
7
+ data.tar.gz: cc9b502af18acc8463e9cddf95cc16b8ed6095ffc34d826642fe8193b2f4cc16aa7490a96f930493fe30fc4af21412cf2ff515f8f45e4d04bf9ec704f256ea26
data/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.8] - 2026-03-09
9
+
10
+ ### Added
11
+
12
+ - while/until loop support to type inference ([#46](https://github.com/dak2/method-ray/pull/46))
13
+ - Not operator (!) support to type inference ([#47](https://github.com/dak2/method-ray/pull/47))
14
+ - begin/rescue/ensure exception handling support to type inference ([#48](https://github.com/dak2/method-ray/pull/48))
15
+ - Keyword argument support to type inference ([#49](https://github.com/dak2/method-ray/pull/49))
16
+ - Multiple assignment support to type inference ([#50](https://github.com/dak2/method-ray/pull/50))
17
+
18
+ ### Changed
19
+
20
+ - Resolve all Clippy warnings for cleaner, more idiomatic Rust ([#51](https://github.com/dak2/method-ray/pull/51))
21
+
8
22
  ## [0.1.7] - 2026-03-07
9
23
 
10
24
  ### Added
@@ -109,6 +123,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
109
123
  - Initial release
110
124
  - `methodray check` - Static type checking for Ruby files
111
125
 
126
+ [0.1.8]: https://github.com/dak2/method-ray/releases/tag/v0.1.8
112
127
  [0.1.7]: https://github.com/dak2/method-ray/releases/tag/v0.1.7
113
128
  [0.1.6]: https://github.com/dak2/method-ray/releases/tag/v0.1.6
114
129
  [0.1.5]: https://github.com/dak2/method-ray/releases/tag/v0.1.5
data/ext/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "methodray"
3
- version = "0.1.6"
3
+ version = "0.1.8"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MethodRay
4
- VERSION = '0.1.7'
4
+ VERSION = '0.1.8'
5
5
  end
data/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "methodray-core"
3
- version = "0.1.7"
3
+ version = "0.1.8"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -0,0 +1,152 @@
1
+ //! Multiple Assignment Handlers - Processing Ruby multiple assignment
2
+ //!
3
+ //! v0.1.8 scope: Only RHS as ArrayNode (multiple literal values) is supported.
4
+ //! TODO: Support RHS as single expression (array decomposition)
5
+ //! TODO: Support splat target (*rest) as Array type
6
+ //! TODO: Support RHS as method return value decomposition
7
+ //! TODO: When LHS is longer than RHS, register trailing targets as NilClass
8
+
9
+ use crate::env::{GlobalEnv, LocalEnv};
10
+ use crate::graph::{ChangeSet, VertexId};
11
+
12
+ use super::bytes_to_name;
13
+ use super::variables::install_local_var_write;
14
+
15
+ /// Process multiple assignment node (e.g., `a, b = 1, "hello"`)
16
+ pub(crate) fn process_multi_write_node(
17
+ genv: &mut GlobalEnv,
18
+ lenv: &mut LocalEnv,
19
+ changes: &mut ChangeSet,
20
+ source: &str,
21
+ node: &ruby_prism::MultiWriteNode,
22
+ ) -> Option<VertexId> {
23
+ let value = node.value();
24
+ let mut last_vtx = None;
25
+
26
+ if let Some(array_node) = value.as_array_node() {
27
+ for (target, rhs_elem) in node.lefts().iter().zip(array_node.elements().iter()) {
28
+ if let Some(target_node) = target.as_local_variable_target_node() {
29
+ let var_name = bytes_to_name(target_node.name().as_slice());
30
+ let rhs_vtx =
31
+ super::install::install_node(genv, lenv, changes, source, &rhs_elem);
32
+ if let Some(rv) = rhs_vtx {
33
+ last_vtx = Some(install_local_var_write(genv, lenv, changes, var_name, rv));
34
+ } else {
35
+ let var_vtx = genv.new_vertex();
36
+ lenv.new_var(var_name, var_vtx);
37
+ last_vtx = Some(var_vtx);
38
+ }
39
+ }
40
+ }
41
+ } else {
42
+ for target in node.lefts().iter() {
43
+ if let Some(target_node) = target.as_local_variable_target_node() {
44
+ let var_name = bytes_to_name(target_node.name().as_slice());
45
+ let var_vtx = genv.new_vertex();
46
+ lenv.new_var(var_name, var_vtx);
47
+ last_vtx = Some(var_vtx);
48
+ }
49
+ }
50
+ }
51
+
52
+ last_vtx
53
+ }
54
+
55
+ #[cfg(test)]
56
+ mod tests {
57
+ use crate::analyzer::install::AstInstaller;
58
+ use crate::env::{GlobalEnv, LocalEnv};
59
+ use crate::graph::VertexId;
60
+ use crate::parser::ParseSession;
61
+
62
+ fn analyze(source: &str) -> (GlobalEnv, LocalEnv) {
63
+ let session = ParseSession::new();
64
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
65
+ let root = parse_result.node();
66
+ let program = root.as_program_node().unwrap();
67
+
68
+ let mut genv = GlobalEnv::new();
69
+ let mut lenv = LocalEnv::new();
70
+
71
+ let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
72
+ for stmt in &program.statements().body() {
73
+ installer.install_node(&stmt);
74
+ }
75
+ installer.finish();
76
+
77
+ (genv, lenv)
78
+ }
79
+
80
+ fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
81
+ if let Some(vertex) = genv.get_vertex(vtx) {
82
+ vertex.show()
83
+ } else if let Some(source) = genv.get_source(vtx) {
84
+ source.ty.show()
85
+ } else {
86
+ panic!("vertex {:?} not found as either Vertex or Source", vtx);
87
+ }
88
+ }
89
+
90
+ #[test]
91
+ fn test_multi_write_integer_and_string() {
92
+ let source = r#"a, b = 1, "hello""#;
93
+ let (genv, lenv) = analyze(source);
94
+
95
+ let a_vtx = lenv.get_var("a").expect("a should be registered");
96
+ assert_eq!(get_type_show(&genv, a_vtx), "Integer");
97
+
98
+ let b_vtx = lenv.get_var("b").expect("b should be registered");
99
+ assert_eq!(get_type_show(&genv, b_vtx), "String");
100
+ }
101
+
102
+ #[test]
103
+ fn test_multi_write_all_integer() {
104
+ let source = "a, b, c = 1, 2, 3";
105
+ let (genv, lenv) = analyze(source);
106
+
107
+ let a_vtx = lenv.get_var("a").expect("a should be registered");
108
+ assert_eq!(get_type_show(&genv, a_vtx), "Integer");
109
+
110
+ let b_vtx = lenv.get_var("b").expect("b should be registered");
111
+ assert_eq!(get_type_show(&genv, b_vtx), "Integer");
112
+
113
+ let c_vtx = lenv.get_var("c").expect("c should be registered");
114
+ assert_eq!(get_type_show(&genv, c_vtx), "Integer");
115
+ }
116
+
117
+ #[test]
118
+ fn test_multi_write_variable_reference_after_assignment() {
119
+ let source = r#"
120
+ a, b = 1, "hello"
121
+ x = a
122
+ "#;
123
+ let (genv, lenv) = analyze(source);
124
+
125
+ let x_vtx = lenv.get_var("x").expect("x should be registered");
126
+ assert_eq!(get_type_show(&genv, x_vtx), "Integer");
127
+ }
128
+
129
+ #[test]
130
+ fn test_multi_write_lhs_longer_than_rhs() {
131
+ let source = "a, b, c = 1, 2";
132
+ let (_, lenv) = analyze(source);
133
+
134
+ assert!(lenv.get_var("a").is_some(), "a should be registered");
135
+ assert!(lenv.get_var("b").is_some(), "b should be registered");
136
+ // KNOWN LIMITATION (v0.1.8): In Ruby, c receives nil, but zip skips it here
137
+ assert!(
138
+ lenv.get_var("c").is_none(),
139
+ "c should not be registered (zip skips)"
140
+ );
141
+ }
142
+
143
+ #[test]
144
+ fn test_multi_write_does_not_panic_on_non_array_rhs() {
145
+ let source = "a, b = some_expr";
146
+ let (_, lenv) = analyze(source);
147
+
148
+ // Variables should be registered (untyped) without panic
149
+ assert!(lenv.get_var("a").is_some(), "a should be registered");
150
+ assert!(lenv.get_var("b").is_some(), "b should be registered");
151
+ }
152
+ }
@@ -39,7 +39,7 @@ pub(crate) fn process_attr_declaration(
39
39
 
40
40
  // Register getter (attr_reader / attr_accessor)
41
41
  if matches!(kind, AttrKind::Reader | AttrKind::Accessor) {
42
- genv.register_user_method(recv_ty.clone(), &attr_name, ivar_vtx, vec![]);
42
+ genv.register_user_method(recv_ty.clone(), &attr_name, ivar_vtx, vec![], None);
43
43
  }
44
44
 
45
45
  // Register setter (attr_writer / attr_accessor)
@@ -51,6 +51,7 @@ pub(crate) fn process_attr_declaration(
51
51
  &format!("{}=", attr_name),
52
52
  ivar_vtx,
53
53
  vec![param_vtx],
54
+ None,
54
55
  );
55
56
  }
56
57
  }
@@ -5,6 +5,8 @@
5
5
  //! - Managing return value vertices
6
6
  //! - Attaching source location for error reporting
7
7
 
8
+ use std::collections::HashMap;
9
+
8
10
  use crate::env::GlobalEnv;
9
11
  use crate::graph::{MethodCallBox, VertexId};
10
12
  use crate::source_map::SourceLocation;
@@ -15,6 +17,7 @@ pub fn install_method_call(
15
17
  recv_vtx: VertexId,
16
18
  method_name: String,
17
19
  arg_vtxs: Vec<VertexId>,
20
+ kwarg_vtxs: Option<HashMap<String, VertexId>>,
18
21
  location: Option<SourceLocation>,
19
22
  ) -> VertexId {
20
23
  // Create Vertex for return value
@@ -22,7 +25,8 @@ pub fn install_method_call(
22
25
 
23
26
  // Create MethodCallBox with location and argument vertices
24
27
  let box_id = genv.alloc_box_id();
25
- let call_box = MethodCallBox::new(box_id, recv_vtx, method_name, ret_vtx, arg_vtxs, location);
28
+ let call_box =
29
+ MethodCallBox::new(box_id, recv_vtx, method_name, ret_vtx, arg_vtxs, kwarg_vtxs, location);
26
30
  genv.register_box(box_id, Box::new(call_box));
27
31
 
28
32
  ret_vtx
@@ -39,7 +43,7 @@ mod tests {
39
43
 
40
44
  let recv_vtx = genv.new_source(Type::string());
41
45
  let ret_vtx =
42
- install_method_call(&mut genv, recv_vtx, "upcase".to_string(), vec![], None);
46
+ install_method_call(&mut genv, recv_vtx, "upcase".to_string(), vec![], None, None);
43
47
 
44
48
  // Return vertex should exist
45
49
  assert!(genv.get_vertex(ret_vtx).is_some());
@@ -51,7 +55,7 @@ mod tests {
51
55
 
52
56
  let recv_vtx = genv.new_source(Type::string());
53
57
  let _ret_vtx =
54
- install_method_call(&mut genv, recv_vtx, "upcase".to_string(), vec![], None);
58
+ install_method_call(&mut genv, recv_vtx, "upcase".to_string(), vec![], None, None);
55
59
 
56
60
  // Box should be added
57
61
  assert_eq!(genv.box_count(), 1);
@@ -6,6 +6,8 @@
6
6
  //! - Method definition scope management (def baz ... end)
7
7
  //! - Extracting class/module names from AST nodes (including qualified names like Api::User)
8
8
 
9
+ use std::collections::HashMap;
10
+
9
11
  use crate::env::{GlobalEnv, LocalEnv};
10
12
  use crate::graph::{ChangeSet, VertexId};
11
13
  use crate::types::Type;
@@ -78,10 +80,10 @@ pub(crate) fn process_def_node(
78
80
  let merge_vtx = genv.scope_manager.current_method_return_vertex();
79
81
 
80
82
  // Process parameters BEFORE processing body
81
- let param_vtxs = if let Some(params_node) = def_node.parameters() {
83
+ let (param_vtxs, keyword_param_vtxs) = if let Some(params_node) = def_node.parameters() {
82
84
  install_parameters(genv, lenv, changes, source, &params_node)
83
85
  } else {
84
- vec![]
86
+ (vec![], HashMap::new())
85
87
  };
86
88
 
87
89
  let mut last_vtx = None;
@@ -108,11 +110,13 @@ pub(crate) fn process_def_node(
108
110
  } else {
109
111
  Type::instance(&name)
110
112
  };
113
+ let kw_params = (!keyword_param_vtxs.is_empty()).then_some(keyword_param_vtxs);
111
114
  genv.register_user_method(
112
115
  recv_type,
113
116
  &method_name,
114
117
  ret_vtx,
115
118
  param_vtxs,
119
+ kw_params,
116
120
  );
117
121
  }
118
122
  }
@@ -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;
@@ -235,7 +237,8 @@ pub(crate) fn process_needs_child(
235
237
  } => {
236
238
  let recv_vtx = super::install::install_node(genv, lenv, changes, source, &receiver)?;
237
239
  process_method_call_common(
238
- 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 },
239
242
  )
240
243
  }
241
244
  NeedsChildKind::ImplicitSelfCall {
@@ -251,7 +254,8 @@ pub(crate) fn process_needs_child(
251
254
  genv.new_source(Type::instance("Object"))
252
255
  };
253
256
  process_method_call_common(
254
- 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 },
255
259
  )
256
260
  }
257
261
  NeedsChildKind::AttrDeclaration { kind, attr_names } => {
@@ -277,6 +281,15 @@ fn finish_local_var_write(
277
281
  install_local_var_write(genv, lenv, changes, var_name, value_vtx)
278
282
  }
279
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
+
280
293
  /// MethodCall / ImplicitSelfCall common processing:
281
294
  /// Handles argument processing, block processing, and MethodCallBox creation after recv_vtx is obtained
282
295
  fn process_method_call_common<'a>(
@@ -284,16 +297,44 @@ fn process_method_call_common<'a>(
284
297
  lenv: &mut LocalEnv,
285
298
  changes: &mut ChangeSet,
286
299
  source: &str,
287
- recv_vtx: VertexId,
288
- method_name: String,
289
- location: SourceLocation,
290
- block: Option<Node<'a>>,
291
- arguments: Vec<Node<'a>>,
300
+ ctx: MethodCallContext<'a>,
292
301
  ) -> 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();
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
+ }
297
338
 
298
339
  if let Some(block_node) = block {
299
340
  if let Some(block) = block_node.as_block_node() {
@@ -314,8 +355,19 @@ fn process_method_call_common<'a>(
314
355
  }
315
356
  }
316
357
 
358
+ let kwarg_vtxs = if keyword_arg_vtxs.is_empty() {
359
+ None
360
+ } else {
361
+ Some(keyword_arg_vtxs)
362
+ };
363
+
317
364
  Some(finish_method_call(
318
- 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,
319
371
  ))
320
372
  }
321
373
 
@@ -325,9 +377,10 @@ fn finish_method_call(
325
377
  recv_vtx: VertexId,
326
378
  method_name: String,
327
379
  arg_vtxs: Vec<VertexId>,
380
+ kwarg_vtxs: Option<HashMap<String, VertexId>>,
328
381
  location: SourceLocation,
329
382
  ) -> VertexId {
330
- 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))
331
384
  }
332
385
 
333
386
  #[cfg(test)]
@@ -980,4 +1033,105 @@ end
980
1033
  genv.type_errors
981
1034
  );
982
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
+ }
983
1137
  }