method-ray 0.1.3 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c950d346d7e01dfcf74b912ee1677450b9ea3d95c3baac937062cc8bf4363163
4
- data.tar.gz: d9804ea668007c228bb12078ebaa3ec7e4c3b37daa372b2a801765b90978136a
3
+ metadata.gz: 1d261070d86a9c7277779eeccc80ac967aeac41ac66fb760e2f76bcc485a1679
4
+ data.tar.gz: 7bd048bc66105308344fff40a99af2afca5ce15d3550affff034918e50044ca4
5
5
  SHA512:
6
- metadata.gz: 1a572ea77523293d586c6acd284994206f1b4b678ede730f50fe619f98762f1c215daf5b8e899400f3872769078aa033bd6cea701515533acfe11d7695e0fd26
7
- data.tar.gz: 2f20387f762e32766281f6813971b3f546f8cb6476482cbe13d3cfb1fe03e6761e2304be3a5eeade1cb8261d7e5197a1416e3f0cfad280450b54a16acdddacb1
6
+ metadata.gz: 5279ea150a536357cceded2ef2576f1a459b51b218b23b9972582b9763609220567d0cfaa870736a446a926420a3d908dc96fa437d56be49fdc9c1f7a8877c63
7
+ data.tar.gz: f2f80b3c71ba3190221098c048c5d8b4c576f3c35e1dae0f16022dcea873a577cdc64d2e522866e6290419198e7f63a089dd91e2b3220c5486069d389fec9cbd
data/CHANGELOG.md CHANGED
@@ -5,6 +5,41 @@ 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.5] - 2026-02-23
9
+
10
+ ### Added
11
+
12
+ - String interpolation type inference (`InterpolatedStringNode`, `InterpolatedSymbolNode`, `InterpolatedRegularExpressionNode`) ([#26](https://github.com/dak2/method-ray/pull/26))
13
+ - Parentheses node type inference for parenthesized expressions ([#27](https://github.com/dak2/method-ray/pull/27))
14
+ - Qualified name method registration to resolve namespace conflicts ([#28](https://github.com/dak2/method-ray/pull/28))
15
+ - Return statement type inference with merge vertex pattern ([#29](https://github.com/dak2/method-ray/pull/29))
16
+ - Ternary operator type inference tests ([#30](https://github.com/dak2/method-ray/pull/30))
17
+ - Logical operator (`&&`/`||`) type inference with union type approximation ([#31](https://github.com/dak2/method-ray/pull/31))
18
+ - Class method (`def self.foo`) type registration and checking ([#32](https://github.com/dak2/method-ray/pull/32))
19
+
20
+ ### Changed
21
+
22
+ - Removed stateless `Analyzer` class and simplified Ruby FFI surface to module functions ([#33](https://github.com/dak2/method-ray/pull/33))
23
+
24
+ ### Deprecated
25
+
26
+ - `clear_cache` command ([#25](https://github.com/dak2/method-ray/pull/25))
27
+
28
+ ## [0.1.4] - 2026-02-16
29
+
30
+ ### Added
31
+
32
+ - Method return type inference for user-defined methods ([#18](https://github.com/dak2/method-ray/pull/18))
33
+ - Parameter type propagation from call-site arguments to method parameters ([#19](https://github.com/dak2/method-ray/pull/19))
34
+ - Receiver-less method call support (ImplicitSelfCall) ([#20](https://github.com/dak2/method-ray/pull/20))
35
+ - `attr_reader`/`attr_writer`/`attr_accessor` support for type inference ([#21](https://github.com/dak2/method-ray/pull/21))
36
+ - `if`/`unless`/`case` conditional type inference ([#22](https://github.com/dak2/method-ray/pull/22))
37
+ - `ConstantReadNode`/`ConstantPathNode` support for type inference ([#23](https://github.com/dak2/method-ray/pull/23))
38
+
39
+ ### Changed
40
+
41
+ - Split install.rs and integration tests into focused modules ([#17](https://github.com/dak2/method-ray/pull/17))
42
+
8
43
  ## [0.1.3] - 2025-02-08
9
44
 
10
45
  ### Added
@@ -54,6 +89,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
54
89
  - Initial release
55
90
  - `methodray check` - Static type checking for Ruby files
56
91
 
92
+ [0.1.5]: https://github.com/dak2/method-ray/releases/tag/v0.1.5
93
+ [0.1.4]: https://github.com/dak2/method-ray/releases/tag/v0.1.4
57
94
  [0.1.3]: https://github.com/dak2/method-ray/releases/tag/v0.1.3
58
95
  [0.1.2]: https://github.com/dak2/method-ray/releases/tag/v0.1.2
59
96
  [0.1.1]: https://github.com/dak2/method-ray/releases/tag/v0.1.1
data/ext/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "methodray"
3
- version = "0.1.3"
3
+ version = "0.1.5"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
data/ext/src/lib.rs CHANGED
@@ -2,7 +2,7 @@
2
2
  //!
3
3
  //! This module provides the Ruby gem interface using magnus.
4
4
 
5
- use magnus::{function, method, prelude::*, Error, Ruby};
5
+ use magnus::{function, prelude::*, Error, Ruby};
6
6
  use methodray_core::{
7
7
  analyzer::AstInstaller,
8
8
  env::{GlobalEnv, LocalEnv},
@@ -10,86 +10,65 @@ use methodray_core::{
10
10
  rbs,
11
11
  };
12
12
 
13
- #[magnus::wrap(class = "MethodRay::Analyzer")]
14
- pub struct Analyzer {
15
- #[allow(dead_code)]
16
- path: String,
17
- }
18
-
19
- impl Analyzer {
20
- fn new(path: String) -> Self {
21
- Self { path }
22
- }
23
-
24
- fn version(&self) -> String {
25
- "0.1.0".to_string()
26
- }
27
-
28
- /// Execute type inference
29
- fn infer_types(&self, source: String) -> Result<String, Error> {
30
- // Parse
31
- let session = ParseSession::new();
32
- let parse_result = session.parse_source(&source, "source.rb").map_err(|e| {
33
- let ruby = unsafe { Ruby::get_unchecked() };
34
- Error::new(ruby.exception_runtime_error(), e.to_string())
35
- })?;
13
+ /// Type inference (public module function)
14
+ fn infer_types(source: String) -> Result<String, Error> {
15
+ // Parse
16
+ let session = ParseSession::new();
17
+ let parse_result = session.parse_source(&source, "source.rb").map_err(|e| {
18
+ let ruby = unsafe { Ruby::get_unchecked() };
19
+ Error::new(ruby.exception_runtime_error(), e.to_string())
20
+ })?;
36
21
 
37
- // Build graph
38
- let mut genv = GlobalEnv::new();
22
+ // Build graph
23
+ let mut genv = GlobalEnv::new();
39
24
 
40
- // Register built-in methods from RBS
41
- let ruby = unsafe { Ruby::get_unchecked() };
42
- rbs::register_rbs_methods(&mut genv, &ruby)?;
25
+ // Register built-in methods from RBS
26
+ let ruby = unsafe { Ruby::get_unchecked() };
27
+ rbs::register_rbs_methods(&mut genv, &ruby)?;
43
28
 
44
- let mut lenv = LocalEnv::new();
45
- let mut installer = AstInstaller::new(&mut genv, &mut lenv, &source);
29
+ let mut lenv = LocalEnv::new();
30
+ let mut installer = AstInstaller::new(&mut genv, &mut lenv, &source);
46
31
 
47
- // Process AST
48
- let root = parse_result.node();
49
- if let Some(program_node) = root.as_program_node() {
50
- let statements = program_node.statements();
51
- for stmt in &statements.body() {
52
- installer.install_node(&stmt);
53
- }
32
+ // Process AST
33
+ let root = parse_result.node();
34
+ if let Some(program_node) = root.as_program_node() {
35
+ let statements = program_node.statements();
36
+ for stmt in &statements.body() {
37
+ installer.install_node(&stmt);
54
38
  }
39
+ }
55
40
 
56
- installer.finish();
41
+ installer.finish();
57
42
 
58
- // Return results as string
59
- let mut results = Vec::new();
60
- for (var_name, vtx_id) in lenv.all_vars() {
61
- if let Some(vtx) = genv.get_vertex(*vtx_id) {
62
- results.push(format!("{}: {}", var_name, vtx.show()));
63
- }
43
+ // Return results as string
44
+ let mut results = Vec::new();
45
+ for (var_name, vtx_id) in lenv.all_vars() {
46
+ if let Some(vtx) = genv.get_vertex(*vtx_id) {
47
+ results.push(format!("{}: {}", var_name, vtx.show()));
64
48
  }
65
-
66
- Ok(results.join("\n"))
67
49
  }
50
+
51
+ Ok(results.join("\n"))
68
52
  }
69
53
 
70
- /// Setup function that only generates RBS cache
71
- /// This is used during gem build to pre-generate the cache
72
- fn setup() -> Result<String, Error> {
54
+ /// RBS cache generation (executed when requiring methodray/setup)
55
+ fn setup() -> Result<(), Error> {
73
56
  let ruby = unsafe { Ruby::get_unchecked() };
74
57
  let mut genv = GlobalEnv::new();
75
58
 
76
59
  // This will load RBS and save to cache if not already cached
77
- let count = rbs::register_rbs_methods(&mut genv, &ruby)?;
60
+ rbs::register_rbs_methods(&mut genv, &ruby)?;
78
61
 
79
- Ok(format!("RBS cache generated with {} methods", count))
62
+ Ok(())
80
63
  }
81
64
 
82
65
  #[magnus::init]
83
66
  fn init(ruby: &Ruby) -> Result<(), Error> {
84
67
  let module = ruby.define_module("MethodRay")?;
85
- let class = module.define_class("Analyzer", ruby.class_object())?;
86
-
87
- class.define_singleton_method("new", function!(Analyzer::new, 1))?;
88
- class.define_method("version", method!(Analyzer::version, 0))?;
89
- class.define_method("infer_types", method!(Analyzer::infer_types, 1))?;
68
+ module.define_singleton_method("infer_types", function!(infer_types, 1))?;
90
69
 
91
- // Module-level setup function for cache generation
92
- module.define_singleton_method("setup", function!(setup, 0))?;
70
+ // require methodray/setup to generate RBS cache on Ruby side
71
+ setup()?;
93
72
 
94
73
  Ok(())
95
74
  }
data/lib/methodray/cli.rb CHANGED
@@ -17,6 +17,7 @@ module MethodRay
17
17
  when 'watch'
18
18
  Commands.watch(args)
19
19
  when 'clear-cache'
20
+ warn "WARNING: 'clear-cache' is deprecated and will be removed in a future version."
20
21
  Commands.clear_cache(args)
21
22
  else
22
23
  puts "Unknown command: #{command}"
@@ -14,7 +14,6 @@ module MethodRay
14
14
  methodray version # Show version
15
15
  methodray check [FILE] [OPTIONS] # Type check a Ruby file
16
16
  methodray watch FILE # Watch file for changes and auto-check
17
- methodray clear-cache # Clear RBS method cache
18
17
 
19
18
  Examples:
20
19
  methodray check app/models/user.rb
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MethodRay
4
- VERSION = '0.1.3'
4
+ VERSION = '0.1.5'
5
5
  end
data/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "methodray-core"
3
- version = "0.1.3"
3
+ version = "0.1.5"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -0,0 +1,57 @@
1
+ //! Attribute accessor support - synthesize getter/setter methods for attr_reader/attr_writer/attr_accessor
2
+
3
+ use crate::env::GlobalEnv;
4
+ use crate::types::Type;
5
+
6
+ use super::dispatch::AttrKind;
7
+
8
+ /// Register synthesized getter/setter methods for attr_reader/attr_writer/attr_accessor.
9
+ ///
10
+ /// - attr_reader :name → registers User#name (getter, return = @name vertex)
11
+ /// - attr_writer :name → registers User#name= (setter, param → @name edge)
12
+ /// - attr_accessor :name → registers both getter and setter
13
+ ///
14
+ /// If @name has no VertexId yet, one is pre-allocated so that later assignments
15
+ /// (e.g., `@name = "Alice"` in initialize) propagate into the same vertex.
16
+ pub(crate) fn process_attr_declaration(
17
+ genv: &mut GlobalEnv,
18
+ kind: AttrKind,
19
+ attr_names: Vec<String>,
20
+ ) {
21
+ let Some(qualified_name) = genv.scope_manager.current_qualified_name() else {
22
+ return;
23
+ };
24
+ let recv_ty = Type::instance(&qualified_name);
25
+
26
+ for attr_name in attr_names {
27
+ let ivar_name = format!("@{}", attr_name);
28
+
29
+ // Get or pre-allocate VertexId for @name
30
+ let ivar_vtx = match genv.scope_manager.lookup_instance_var(&ivar_name) {
31
+ Some(vtx) => vtx,
32
+ None => {
33
+ let vtx = genv.new_vertex();
34
+ genv.scope_manager
35
+ .set_instance_var_in_class(ivar_name, vtx);
36
+ vtx
37
+ }
38
+ };
39
+
40
+ // Register getter (attr_reader / attr_accessor)
41
+ if matches!(kind, AttrKind::Reader | AttrKind::Accessor) {
42
+ genv.register_user_method(recv_ty.clone(), &attr_name, ivar_vtx, vec![]);
43
+ }
44
+
45
+ // Register setter (attr_writer / attr_accessor)
46
+ if matches!(kind, AttrKind::Writer | AttrKind::Accessor) {
47
+ let param_vtx = genv.new_vertex();
48
+ genv.add_edge(param_vtx, ivar_vtx);
49
+ genv.register_user_method(
50
+ recv_ty.clone(),
51
+ &format!("{}=", attr_name),
52
+ ivar_vtx,
53
+ vec![param_vtx],
54
+ );
55
+ }
56
+ }
57
+ }
@@ -6,35 +6,121 @@
6
6
  //! - Managing block scope
7
7
 
8
8
  use crate::env::{GlobalEnv, LocalEnv, ScopeKind};
9
- use crate::graph::VertexId;
9
+ use crate::graph::{ChangeSet, VertexId};
10
+
11
+ use super::parameters::{install_optional_parameter, install_required_parameter, install_rest_parameter};
12
+
13
+ /// Process block node
14
+ pub(crate) fn process_block_node(
15
+ genv: &mut GlobalEnv,
16
+ lenv: &mut LocalEnv,
17
+ changes: &mut ChangeSet,
18
+ source: &str,
19
+ block_node: &ruby_prism::BlockNode,
20
+ ) -> Option<VertexId> {
21
+ process_block_node_with_params(genv, lenv, changes, source, block_node);
22
+ None
23
+ }
24
+
25
+ /// Process block node and return block parameter vertex IDs
26
+ pub(crate) fn process_block_node_with_params(
27
+ genv: &mut GlobalEnv,
28
+ lenv: &mut LocalEnv,
29
+ changes: &mut ChangeSet,
30
+ source: &str,
31
+ block_node: &ruby_prism::BlockNode,
32
+ ) -> Vec<VertexId> {
33
+ enter_block_scope(genv);
34
+
35
+ let mut param_vtxs = Vec::new();
36
+
37
+ if let Some(params_node) = block_node.parameters() {
38
+ if let Some(block_params) = params_node.as_block_parameters_node() {
39
+ param_vtxs =
40
+ install_block_parameters_with_vtxs(genv, lenv, changes, source, &block_params);
41
+ }
42
+ }
43
+
44
+ if let Some(body) = block_node.body() {
45
+ if let Some(statements) = body.as_statements_node() {
46
+ super::install::install_statements(genv, lenv, changes, source, &statements);
47
+ } else {
48
+ super::install::install_node(genv, lenv, changes, source, &body);
49
+ }
50
+ }
51
+
52
+ exit_block_scope(genv);
10
53
 
11
- use super::parameters::install_required_parameter;
54
+ param_vtxs
55
+ }
56
+
57
+ /// Install block parameters and return their vertex IDs
58
+ fn install_block_parameters_with_vtxs(
59
+ genv: &mut GlobalEnv,
60
+ lenv: &mut LocalEnv,
61
+ changes: &mut ChangeSet,
62
+ source: &str,
63
+ block_params: &ruby_prism::BlockParametersNode,
64
+ ) -> Vec<VertexId> {
65
+ let mut vtxs = Vec::new();
66
+
67
+ if let Some(params) = block_params.parameters() {
68
+ // Required parameters (most common in blocks)
69
+ for node in params.requireds().iter() {
70
+ 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 vtx = install_block_parameter(genv, lenv, name);
73
+ vtxs.push(vtx);
74
+ }
75
+ }
76
+
77
+ // Optional parameters: { |x = 1| ... }
78
+ for node in params.optionals().iter() {
79
+ 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 default_value = opt_param.value();
82
+
83
+ if let Some(default_vtx) =
84
+ super::install::install_node(genv, lenv, changes, source, &default_value)
85
+ {
86
+ let vtx =
87
+ install_optional_parameter(genv, lenv, changes, name, default_vtx);
88
+ vtxs.push(vtx);
89
+ } else {
90
+ let vtx = install_block_parameter(genv, lenv, name);
91
+ vtxs.push(vtx);
92
+ }
93
+ }
94
+ }
95
+
96
+ // Rest parameter: { |*args| ... }
97
+ if let Some(rest_node) = params.rest() {
98
+ if let Some(rest_param) = rest_node.as_rest_parameter_node() {
99
+ if let Some(name_id) = rest_param.name() {
100
+ let name = String::from_utf8_lossy(name_id.as_slice()).to_string();
101
+ let vtx = install_rest_parameter(genv, lenv, name);
102
+ vtxs.push(vtx);
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ vtxs
109
+ }
12
110
 
13
111
  /// Enter a new block scope
14
- ///
15
- /// Creates a new scope for the block and enters it.
16
- /// Block scopes inherit variables from parent scopes.
17
- pub fn enter_block_scope(genv: &mut GlobalEnv) {
112
+ fn enter_block_scope(genv: &mut GlobalEnv) {
18
113
  let block_scope_id = genv.scope_manager.new_scope(ScopeKind::Block);
19
114
  genv.scope_manager.enter_scope(block_scope_id);
20
115
  }
21
116
 
22
117
  /// Exit the current block scope
23
- pub fn exit_block_scope(genv: &mut GlobalEnv) {
118
+ fn exit_block_scope(genv: &mut GlobalEnv) {
24
119
  genv.scope_manager.exit_scope();
25
120
  }
26
121
 
27
- /// Install block parameters as local variables
28
- ///
29
- /// Block parameters are registered as Bot (untyped) type since we don't
30
- /// know what type will be passed from the iterator method.
31
- ///
32
- /// # Example
33
- /// ```ruby
34
- /// [1, 2, 3].each { |x| x.to_s } # 'x' is a block parameter
35
- /// ```
36
- pub fn install_block_parameter(genv: &mut GlobalEnv, lenv: &mut LocalEnv, name: String) -> VertexId {
37
- // Reuse required parameter logic (Bot type)
122
+ /// Install block parameter as a local variable (Bot type)
123
+ fn install_block_parameter(genv: &mut GlobalEnv, lenv: &mut LocalEnv, name: String) -> VertexId {
38
124
  install_required_parameter(genv, lenv, name)
39
125
  }
40
126
 
@@ -51,12 +137,10 @@ mod tests {
51
137
  enter_block_scope(&mut genv);
52
138
  let block_scope_id = genv.scope_manager.current_scope().id;
53
139
 
54
- // Should be in a new scope
55
140
  assert_ne!(initial_scope_id, block_scope_id);
56
141
 
57
142
  exit_block_scope(&mut genv);
58
143
 
59
- // Should be back to initial scope
60
144
  assert_eq!(genv.scope_manager.current_scope().id, initial_scope_id);
61
145
  }
62
146
 
@@ -69,7 +153,6 @@ mod tests {
69
153
 
70
154
  let vtx = install_block_parameter(&mut genv, &mut lenv, "x".to_string());
71
155
 
72
- // Parameter should be registered in LocalEnv
73
156
  assert_eq!(lenv.get_var("x"), Some(vtx));
74
157
 
75
158
  exit_block_scope(&mut genv);
@@ -79,14 +162,12 @@ mod tests {
79
162
  fn test_block_inherits_parent_scope_vars() {
80
163
  let mut genv = GlobalEnv::new();
81
164
 
82
- // Set variable in top-level scope
83
165
  genv.scope_manager
84
166
  .current_scope_mut()
85
167
  .set_local_var("outer".to_string(), VertexId(100));
86
168
 
87
169
  enter_block_scope(&mut genv);
88
170
 
89
- // Block should be able to lookup parent scope variables
90
171
  assert_eq!(genv.scope_manager.lookup_var("outer"), Some(VertexId(100)));
91
172
 
92
173
  exit_block_scope(&mut genv);
@@ -14,14 +14,15 @@ pub fn install_method_call(
14
14
  genv: &mut GlobalEnv,
15
15
  recv_vtx: VertexId,
16
16
  method_name: String,
17
+ arg_vtxs: Vec<VertexId>,
17
18
  location: Option<SourceLocation>,
18
19
  ) -> VertexId {
19
20
  // Create Vertex for return value
20
21
  let ret_vtx = genv.new_vertex();
21
22
 
22
- // Create MethodCallBox with location
23
+ // Create MethodCallBox with location and argument vertices
23
24
  let box_id = genv.alloc_box_id();
24
- let call_box = MethodCallBox::new(box_id, recv_vtx, method_name, ret_vtx, location);
25
+ let call_box = MethodCallBox::new(box_id, recv_vtx, method_name, ret_vtx, arg_vtxs, location);
25
26
  genv.register_box(box_id, Box::new(call_box));
26
27
 
27
28
  ret_vtx
@@ -37,7 +38,8 @@ mod tests {
37
38
  let mut genv = GlobalEnv::new();
38
39
 
39
40
  let recv_vtx = genv.new_source(Type::string());
40
- let ret_vtx = install_method_call(&mut genv, recv_vtx, "upcase".to_string(), None);
41
+ let ret_vtx =
42
+ install_method_call(&mut genv, recv_vtx, "upcase".to_string(), vec![], None);
41
43
 
42
44
  // Return vertex should exist
43
45
  assert!(genv.get_vertex(ret_vtx).is_some());
@@ -48,7 +50,8 @@ mod tests {
48
50
  let mut genv = GlobalEnv::new();
49
51
 
50
52
  let recv_vtx = genv.new_source(Type::string());
51
- let _ret_vtx = install_method_call(&mut genv, recv_vtx, "upcase".to_string(), None);
53
+ let _ret_vtx =
54
+ install_method_call(&mut genv, recv_vtx, "upcase".to_string(), vec![], None);
52
55
 
53
56
  // Box should be added
54
57
  assert_eq!(genv.box_count(), 1);