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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee779223feab7110e29256fdc4860ba480d13d676d18e85284fd0765eef2cc4b
4
- data.tar.gz: 249087d8f6b7772aa7428a4828ffc35eacb1b0aa8b27d5cd909b803176d6a683
3
+ metadata.gz: f44709f8354439522722c8ea65da30463fae9de68d9be1aa07c8dac9627a03ce
4
+ data.tar.gz: 2a09885854020892cf93a1c85e83735a23be60240fcbee09f77fa8c9fbf58801
5
5
  SHA512:
6
- metadata.gz: 8d6534a955eaf4937c5ebc4c525e07f5f7cd8599861f128cbb10161e00a6a932f1b47760e1283e7e2f48940ea0a89bb5c038ac83cdd8ce8a629f8f91b05e534a
7
- data.tar.gz: cbe0d247ffcbd0ce4965f9e20a2293336aca3d7a6eeb96878665cb7692e11ea4366d3c1f2a87ea32711dbcd00a404d4a41da3e84d34829d40568ec0bb37d74b4
6
+ metadata.gz: 4be6a23881adf54e88039c4e62255f09aa52005cf4085e6cbdc1910f642dae8e763ec7993445051bd3cff20693b5576e417f47b0864359c60db6becf1c21e204
7
+ data.tar.gz: cc9b502af18acc8463e9cddf95cc16b8ed6095ffc34d826642fe8193b2f4cc16aa7490a96f930493fe30fc4af21412cf2ff515f8f45e4d04bf9ec704f256ea26
data/CHANGELOG.md CHANGED
@@ -5,6 +5,34 @@ 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
+
22
+ ## [0.1.7] - 2026-03-07
23
+
24
+ ### Added
25
+
26
+ - Kernel/Object methods loaded from RBS to reduce false positives ([#39](https://github.com/dak2/method-ray/pull/39))
27
+ - Object/Kernel fallback chain for method resolution ([#40](https://github.com/dak2/method-ray/pull/40))
28
+ - Constant namespace resolution for ConstantReadNode in nested scopes ([#41](https://github.com/dak2/method-ray/pull/41))
29
+ - Cargo test added to CI workflow ([#38](https://github.com/dak2/method-ray/pull/38))
30
+
31
+ ### Changed
32
+
33
+ - Extract `bytes_to_name` helper to consolidate 17 UTF-8 conversion sites ([#42](https://github.com/dak2/method-ray/pull/42))
34
+ - Refactor MethodCallBox by extracting helper methods ([#43](https://github.com/dak2/method-ray/pull/43))
35
+
8
36
  ## [0.1.6] - 2026-02-23
9
37
 
10
38
  ### Fixed
@@ -95,6 +123,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
95
123
  - Initial release
96
124
  - `methodray check` - Static type checking for Ruby files
97
125
 
126
+ [0.1.8]: https://github.com/dak2/method-ray/releases/tag/v0.1.8
127
+ [0.1.7]: https://github.com/dak2/method-ray/releases/tag/v0.1.7
98
128
  [0.1.6]: https://github.com/dak2/method-ray/releases/tag/v0.1.6
99
129
  [0.1.5]: https://github.com/dak2/method-ray/releases/tag/v0.1.5
100
130
  [0.1.4]: https://github.com/dak2/method-ray/releases/tag/v0.1.4
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.6'
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.6"
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
  }
@@ -8,6 +8,7 @@
8
8
  use crate::env::{GlobalEnv, LocalEnv, ScopeKind};
9
9
  use crate::graph::{ChangeSet, VertexId};
10
10
 
11
+ use super::bytes_to_name;
11
12
  use super::parameters::{install_optional_parameter, install_required_parameter, install_rest_parameter};
12
13
 
13
14
  /// Process block node
@@ -68,7 +69,7 @@ fn install_block_parameters_with_vtxs(
68
69
  // Required parameters (most common in blocks)
69
70
  for node in params.requireds().iter() {
70
71
  if let Some(req_param) = node.as_required_parameter_node() {
71
- let name = String::from_utf8_lossy(req_param.name().as_slice()).to_string();
72
+ let name = bytes_to_name(req_param.name().as_slice());
72
73
  let vtx = install_block_parameter(genv, lenv, name);
73
74
  vtxs.push(vtx);
74
75
  }
@@ -77,7 +78,7 @@ fn install_block_parameters_with_vtxs(
77
78
  // Optional parameters: { |x = 1| ... }
78
79
  for node in params.optionals().iter() {
79
80
  if let Some(opt_param) = node.as_optional_parameter_node() {
80
- let name = String::from_utf8_lossy(opt_param.name().as_slice()).to_string();
81
+ let name = bytes_to_name(opt_param.name().as_slice());
81
82
  let default_value = opt_param.value();
82
83
 
83
84
  if let Some(default_vtx) =
@@ -97,7 +98,7 @@ fn install_block_parameters_with_vtxs(
97
98
  if let Some(rest_node) = params.rest() {
98
99
  if let Some(rest_param) = rest_node.as_rest_parameter_node() {
99
100
  if let Some(name_id) = rest_param.name() {
100
- let name = String::from_utf8_lossy(name_id.as_slice()).to_string();
101
+ let name = bytes_to_name(name_id.as_slice());
101
102
  let vtx = install_rest_parameter(genv, lenv, name);
102
103
  vtxs.push(vtx);
103
104
  }
@@ -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,11 +6,14 @@
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;
12
14
  use ruby_prism::Node;
13
15
 
16
+ use super::bytes_to_name;
14
17
  use super::install::install_statements;
15
18
  use super::parameters::install_parameters;
16
19
 
@@ -64,7 +67,7 @@ pub(crate) fn process_def_node(
64
67
  source: &str,
65
68
  def_node: &ruby_prism::DefNode,
66
69
  ) -> Option<VertexId> {
67
- let method_name = String::from_utf8_lossy(def_node.name().as_slice()).to_string();
70
+ let method_name = bytes_to_name(def_node.name().as_slice());
68
71
 
69
72
  // Check if this is a class method (def self.foo)
70
73
  let is_class_method = def_node
@@ -77,10 +80,10 @@ pub(crate) fn process_def_node(
77
80
  let merge_vtx = genv.scope_manager.current_method_return_vertex();
78
81
 
79
82
  // Process parameters BEFORE processing body
80
- 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() {
81
84
  install_parameters(genv, lenv, changes, source, &params_node)
82
85
  } else {
83
- vec![]
86
+ (vec![], HashMap::new())
84
87
  };
85
88
 
86
89
  let mut last_vtx = None;
@@ -107,11 +110,13 @@ pub(crate) fn process_def_node(
107
110
  } else {
108
111
  Type::instance(&name)
109
112
  };
113
+ let kw_params = (!keyword_param_vtxs.is_empty()).then_some(keyword_param_vtxs);
110
114
  genv.register_user_method(
111
115
  recv_type,
112
116
  &method_name,
113
117
  ret_vtx,
114
118
  param_vtxs,
119
+ kw_params,
115
120
  );
116
121
  }
117
122
  }
@@ -163,7 +168,7 @@ fn extract_module_name(module_node: &ruby_prism::ModuleNode) -> String {
163
168
  pub(crate) fn extract_constant_path(node: &Node) -> Option<String> {
164
169
  // Simple constant read: `User`
165
170
  if let Some(constant_read) = node.as_constant_read_node() {
166
- return Some(String::from_utf8_lossy(constant_read.name().as_slice()).to_string());
171
+ return Some(bytes_to_name(constant_read.name().as_slice()));
167
172
  }
168
173
 
169
174
  // Constant path: `Api::User` or `Api::V1::User`
@@ -171,7 +176,7 @@ pub(crate) fn extract_constant_path(node: &Node) -> Option<String> {
171
176
  // name() returns Option<ConstantId>, use as_slice() to get &[u8]
172
177
  let name = constant_path
173
178
  .name()
174
- .map(|id| String::from_utf8_lossy(id.as_slice()).to_string())?;
179
+ .map(|id| bytes_to_name(id.as_slice()))?;
175
180
 
176
181
  // Get parent path if exists
177
182
  if let Some(parent_node) = constant_path.parent() {