method-ray 0.1.2 → 0.1.3

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: b03f7efedd583d2cda9e59cd651ded78d5357c53eb06eac8980aae25db0529de
4
- data.tar.gz: c02996cc2a511f800d34cec0ce46744c6ae2bddb8974e361f994a332b898c467
3
+ metadata.gz: c950d346d7e01dfcf74b912ee1677450b9ea3d95c3baac937062cc8bf4363163
4
+ data.tar.gz: d9804ea668007c228bb12078ebaa3ec7e4c3b37daa372b2a801765b90978136a
5
5
  SHA512:
6
- metadata.gz: 3aa19e8a26274f5d8c7b5b746e67f10d4e47ac281852251c737ebed4f933b5426e8b842d575f26110ee279fc48079a1dc382e78b2a184e0d4211d241dabae77a
7
- data.tar.gz: '08e6b8bf49475c874538c007b52fec615459eb0c376357e1b2429868a67885eb695d261ba02d078b7fb0835321b92e84f06a6ac5366066379229b0e78072d786'
6
+ metadata.gz: 1a572ea77523293d586c6acd284994206f1b4b678ede730f50fe619f98762f1c215daf5b8e899400f3872769078aa033bd6cea701515533acfe11d7695e0fd26
7
+ data.tar.gz: 2f20387f762e32766281f6813971b3f546f8cb6476482cbe13d3cfb1fe03e6761e2304be3a5eeade1cb8261d7e5197a1416e3f0cfad280450b54a16acdddacb1
data/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ 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.3] - 2025-02-08
9
+
10
+ ### Added
11
+
12
+ - Method parameter type inference support ([#3](https://github.com/dak2/method-ray/pull/3))
13
+ - Block parameter type variable resolution ([#4](https://github.com/dak2/method-ray/pull/4))
14
+ - Module scope support ([#6](https://github.com/dak2/method-ray/pull/6))
15
+ - Fully qualified name support for nested classes/modules ([#7](https://github.com/dak2/method-ray/pull/7))
16
+ - Float type support ([#8](https://github.com/dak2/method-ray/pull/8))
17
+ - Regexp type support ([#9](https://github.com/dak2/method-ray/pull/9))
18
+ - Range type support ([#10](https://github.com/dak2/method-ray/pull/10))
19
+ - Generic type inference for Range, Hash, and nested Array ([#11](https://github.com/dak2/method-ray/pull/11))
20
+
21
+ ### Fixed
22
+
23
+ - Call operator location ([#12](https://github.com/dak2/method-ray/pull/12))
24
+ - Memory leak ([#13](https://github.com/dak2/method-ray/pull/13))
25
+
26
+ ### Changed
27
+
28
+ - Extract BinaryLocator class from Commands module ([#5](https://github.com/dak2/method-ray/pull/5))
29
+ - Migrate Rust integration tests to Ruby CLI and Rust unit tests ([#14](https://github.com/dak2/method-ray/pull/14))
30
+ - Remove unnecessary test files and logs ([#1](https://github.com/dak2/method-ray/pull/1), [#15](https://github.com/dak2/method-ray/pull/15))
31
+
8
32
  ## [0.1.2] - 2025-01-19
9
33
 
10
34
  ### Added
@@ -30,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
30
54
  - Initial release
31
55
  - `methodray check` - Static type checking for Ruby files
32
56
 
57
+ [0.1.3]: https://github.com/dak2/method-ray/releases/tag/v0.1.3
33
58
  [0.1.2]: https://github.com/dak2/method-ray/releases/tag/v0.1.2
34
59
  [0.1.1]: https://github.com/dak2/method-ray/releases/tag/v0.1.1
35
60
  [0.1.0]: https://github.com/dak2/method-ray/releases/tag/v0.1.0
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A fast static callable method checker for Ruby code.
4
4
 
5
+ No type annotations required, just check callable methods in your Ruby files.
6
+
5
7
  ## Requirements
6
8
 
7
9
  Method-Ray supports Ruby 3.4 or later.
@@ -14,7 +16,7 @@ gem install methodray
14
16
 
15
17
  ## Quick Start
16
18
 
17
- ### VSCode Extension
19
+ ### VSCode Extension (under development)
18
20
 
19
21
  1. Install the [Method-Ray VSCode extension](https://github.com/dak2/method-ray-vscode)
20
22
  2. Open a Ruby file in VSCode
@@ -30,6 +32,30 @@ bundle exec methodray check app/models/user.rb
30
32
  bundle exec methodray watch app/models/user.rb
31
33
  ```
32
34
 
35
+ #### Example
36
+
37
+ `methodray check <file>`: Performs static type checking on the specified Ruby file.
38
+
39
+
40
+ ```ruby
41
+ class User
42
+ def greeting
43
+ name = "Alice"
44
+ message = name.abs
45
+ message
46
+ end
47
+ end
48
+ ```
49
+
50
+ This will output:
51
+
52
+ ```
53
+ $ bundle exec methodray check app/models/user.rb
54
+ app/models/user.rb:4:19: error: undefined method `abs` for String
55
+ message = name.abs
56
+ ^
57
+ ```
58
+
33
59
  ## Contributing
34
60
 
35
61
  Bug reports and pull requests are welcome on GitHub at this repository!
data/ext/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "methodray"
3
- version = "0.1.0"
3
+ version = "0.1.3"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
data/ext/src/lib.rs CHANGED
@@ -6,7 +6,8 @@ use magnus::{function, method, prelude::*, Error, Ruby};
6
6
  use methodray_core::{
7
7
  analyzer::AstInstaller,
8
8
  env::{GlobalEnv, LocalEnv},
9
- parser, rbs,
9
+ parser::ParseSession,
10
+ rbs,
10
11
  };
11
12
 
12
13
  #[magnus::wrap(class = "MethodRay::Analyzer")]
@@ -27,11 +28,11 @@ impl Analyzer {
27
28
  /// Execute type inference
28
29
  fn infer_types(&self, source: String) -> Result<String, Error> {
29
30
  // Parse
30
- let parse_result =
31
- parser::parse_ruby_source(&source, "source.rb".to_string()).map_err(|e| {
32
- let ruby = unsafe { Ruby::get_unchecked() };
33
- Error::new(ruby.exception_runtime_error(), e.to_string())
34
- })?;
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
+ })?;
35
36
 
36
37
  // Build graph
37
38
  let mut genv = GlobalEnv::new();
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MethodRay
4
+ class BinaryLocator
5
+ LIB_DIR = __dir__
6
+
7
+ def initialize
8
+ @binary_name = Gem.win_platform? ? 'methodray-cli.exe' : 'methodray-cli'
9
+ @legacy_binary_name = Gem.win_platform? ? 'methodray.exe' : 'methodray'
10
+ end
11
+
12
+ def find
13
+ candidates.find { |path| File.executable?(path) }
14
+ end
15
+
16
+ private
17
+
18
+ def candidates
19
+ [
20
+ # CLI binary built during gem install (lib/methodray directory)
21
+ File.expand_path(@binary_name, LIB_DIR),
22
+ # Development: target/release (project root)
23
+ File.expand_path("../../target/release/#{@binary_name}", LIB_DIR),
24
+ # Development: rust/target/release (legacy standalone binary)
25
+ File.expand_path("../../rust/target/release/#{@legacy_binary_name}", LIB_DIR)
26
+ ]
27
+ end
28
+ end
29
+ end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'binary_locator'
4
+
3
5
  module MethodRay
4
6
  module Commands
5
- COMMANDS_DIR = __dir__
6
-
7
7
  class << self
8
8
  def help
9
9
  puts <<~HELP
@@ -41,7 +41,7 @@ module MethodRay
41
41
  private
42
42
 
43
43
  def exec_rust_cli(command, args)
44
- binary_path = find_rust_binary
44
+ binary_path = BinaryLocator.new.find
45
45
 
46
46
  unless binary_path
47
47
  warn 'Error: CLI binary not found.'
@@ -56,23 +56,6 @@ module MethodRay
56
56
 
57
57
  exec(binary_path, command, *args)
58
58
  end
59
-
60
- def find_rust_binary
61
- # Platform-specific binary name
62
- cli_binary = Gem.win_platform? ? 'methodray-cli.exe' : 'methodray-cli'
63
- legacy_binary = Gem.win_platform? ? 'methodray.exe' : 'methodray'
64
-
65
- candidates = [
66
- # CLI binary built during gem install (lib/methodray directory)
67
- File.expand_path(cli_binary, COMMANDS_DIR),
68
- # Development: target/release (project root)
69
- File.expand_path("../../target/release/#{cli_binary}", COMMANDS_DIR),
70
- # Development: rust/target/release (legacy standalone binary)
71
- File.expand_path("../../rust/target/release/#{legacy_binary}", COMMANDS_DIR)
72
- ]
73
-
74
- candidates.find { |path| File.executable?(path) }
75
- end
76
59
  end
77
60
  end
78
61
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MethodRay
4
- VERSION = '0.1.2'
4
+ VERSION = '0.1.3'
5
5
  end
data/lib/methodray.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'rbs'
4
4
  require_relative 'methodray/version'
5
- require_relative 'methodray/methodray' # ネイティブ拡張
5
+ require_relative 'methodray/methodray'
6
6
 
7
7
  module MethodRay
8
8
  class Error < StandardError; end
data/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "methodray-core"
3
- version = "0.1.0"
3
+ version = "0.1.3"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -18,6 +18,8 @@ path = "src/lsp/main.rs"
18
18
  required-features = ["lsp"]
19
19
 
20
20
  [dependencies]
21
+ bumpalo = "3.14"
22
+ smallvec = "1.13"
21
23
  rayon = "1.10"
22
24
  walkdir = "2.5"
23
25
  anyhow = "1.0"
@@ -0,0 +1,94 @@
1
+ //! Block Handlers - Processing Ruby blocks
2
+ //!
3
+ //! This module is responsible for:
4
+ //! - Processing BlockNode (e.g., `{ |x| x.to_s }` or `do |x| x.to_s end`)
5
+ //! - Registering block parameters as local variables
6
+ //! - Managing block scope
7
+
8
+ use crate::env::{GlobalEnv, LocalEnv, ScopeKind};
9
+ use crate::graph::VertexId;
10
+
11
+ use super::parameters::install_required_parameter;
12
+
13
+ /// 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) {
18
+ let block_scope_id = genv.scope_manager.new_scope(ScopeKind::Block);
19
+ genv.scope_manager.enter_scope(block_scope_id);
20
+ }
21
+
22
+ /// Exit the current block scope
23
+ pub fn exit_block_scope(genv: &mut GlobalEnv) {
24
+ genv.scope_manager.exit_scope();
25
+ }
26
+
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)
38
+ install_required_parameter(genv, lenv, name)
39
+ }
40
+
41
+ #[cfg(test)]
42
+ mod tests {
43
+ use super::*;
44
+
45
+ #[test]
46
+ fn test_enter_exit_block_scope() {
47
+ let mut genv = GlobalEnv::new();
48
+
49
+ let initial_scope_id = genv.scope_manager.current_scope().id;
50
+
51
+ enter_block_scope(&mut genv);
52
+ let block_scope_id = genv.scope_manager.current_scope().id;
53
+
54
+ // Should be in a new scope
55
+ assert_ne!(initial_scope_id, block_scope_id);
56
+
57
+ exit_block_scope(&mut genv);
58
+
59
+ // Should be back to initial scope
60
+ assert_eq!(genv.scope_manager.current_scope().id, initial_scope_id);
61
+ }
62
+
63
+ #[test]
64
+ fn test_install_block_parameter() {
65
+ let mut genv = GlobalEnv::new();
66
+ let mut lenv = LocalEnv::new();
67
+
68
+ enter_block_scope(&mut genv);
69
+
70
+ let vtx = install_block_parameter(&mut genv, &mut lenv, "x".to_string());
71
+
72
+ // Parameter should be registered in LocalEnv
73
+ assert_eq!(lenv.get_var("x"), Some(vtx));
74
+
75
+ exit_block_scope(&mut genv);
76
+ }
77
+
78
+ #[test]
79
+ fn test_block_inherits_parent_scope_vars() {
80
+ let mut genv = GlobalEnv::new();
81
+
82
+ // Set variable in top-level scope
83
+ genv.scope_manager
84
+ .current_scope_mut()
85
+ .set_local_var("outer".to_string(), VertexId(100));
86
+
87
+ enter_block_scope(&mut genv);
88
+
89
+ // Block should be able to lookup parent scope variables
90
+ assert_eq!(genv.scope_manager.lookup_var("outer"), Some(VertexId(100)));
91
+
92
+ exit_block_scope(&mut genv);
93
+ }
94
+ }
@@ -1,39 +1,85 @@
1
- //! Definition Handlers - Processing Ruby class/method definitions
1
+ //! Definition Handlers - Processing Ruby class/method/module definitions
2
2
  //!
3
3
  //! This module is responsible for:
4
4
  //! - Class definition scope management (class Foo ... end)
5
- //! - Method definition scope management (def bar ... end)
6
- //! - Extracting class names from AST nodes
5
+ //! - Module definition scope management (module Bar ... end)
6
+ //! - Method definition scope management (def baz ... end)
7
+ //! - Extracting class/module names from AST nodes (including qualified names like Api::User)
7
8
 
8
9
  use crate::env::GlobalEnv;
10
+ use ruby_prism::Node;
9
11
 
10
12
  /// Install class definition
11
13
  pub fn install_class(genv: &mut GlobalEnv, class_name: String) {
12
14
  genv.enter_class(class_name);
13
15
  }
14
16
 
17
+ /// Install module definition
18
+ pub fn install_module(genv: &mut GlobalEnv, module_name: String) {
19
+ genv.enter_module(module_name);
20
+ }
21
+
15
22
  /// Install method definition
16
23
  pub fn install_method(genv: &mut GlobalEnv, method_name: String) {
17
24
  genv.enter_method(method_name);
18
25
  }
19
26
 
20
- /// Exit current scope (class or method)
27
+ /// Exit current scope (class, module, or method)
21
28
  pub fn exit_scope(genv: &mut GlobalEnv) {
22
29
  genv.exit_scope();
23
30
  }
24
31
 
25
32
  /// Extract class name from ClassNode
33
+ /// Supports both simple names (User) and qualified names (Api::V1::User)
26
34
  pub fn extract_class_name(class_node: &ruby_prism::ClassNode) -> String {
27
- if let Some(constant_read) = class_node.constant_path().as_constant_read_node() {
28
- String::from_utf8_lossy(constant_read.name().as_slice()).to_string()
29
- } else {
30
- "UnknownClass".to_string()
35
+ extract_constant_path(&class_node.constant_path()).unwrap_or_else(|| "UnknownClass".to_string())
36
+ }
37
+
38
+ /// Extract module name from ModuleNode
39
+ /// Supports both simple names (Utils) and qualified names (Api::V1::Utils)
40
+ pub fn extract_module_name(module_node: &ruby_prism::ModuleNode) -> String {
41
+ extract_constant_path(&module_node.constant_path())
42
+ .unwrap_or_else(|| "UnknownModule".to_string())
43
+ }
44
+
45
+ /// Extract constant path from a Node (handles both ConstantReadNode and ConstantPathNode)
46
+ ///
47
+ /// Examples:
48
+ /// - `User` (ConstantReadNode) → "User"
49
+ /// - `Api::User` (ConstantPathNode) → "Api::User"
50
+ /// - `Api::V1::User` (nested ConstantPathNode) → "Api::V1::User"
51
+ /// - `::Api::User` (absolute path with COLON3) → "Api::User"
52
+ fn extract_constant_path(node: &Node) -> Option<String> {
53
+ // Simple constant read: `User`
54
+ if let Some(constant_read) = node.as_constant_read_node() {
55
+ return Some(String::from_utf8_lossy(constant_read.name().as_slice()).to_string());
31
56
  }
57
+
58
+ // Constant path: `Api::User` or `Api::V1::User`
59
+ if let Some(constant_path) = node.as_constant_path_node() {
60
+ // name() returns Option<ConstantId>, use as_slice() to get &[u8]
61
+ let name = constant_path
62
+ .name()
63
+ .map(|id| String::from_utf8_lossy(id.as_slice()).to_string())?;
64
+
65
+ // Get parent path if exists
66
+ if let Some(parent_node) = constant_path.parent() {
67
+ if let Some(parent_path) = extract_constant_path(&parent_node) {
68
+ return Some(format!("{}::{}", parent_path, name));
69
+ }
70
+ }
71
+
72
+ // No parent (absolute path like `::User`)
73
+ return Some(name);
74
+ }
75
+
76
+ None
32
77
  }
33
78
 
34
79
  #[cfg(test)]
35
80
  mod tests {
36
81
  use super::*;
82
+ use crate::parser::ParseSession;
37
83
 
38
84
  #[test]
39
85
  fn test_enter_exit_class_scope() {
@@ -49,6 +95,20 @@ mod tests {
49
95
  assert_eq!(genv.scope_manager.current_class_name(), None);
50
96
  }
51
97
 
98
+ #[test]
99
+ fn test_enter_exit_module_scope() {
100
+ let mut genv = GlobalEnv::new();
101
+
102
+ install_module(&mut genv, "Utils".to_string());
103
+ assert_eq!(
104
+ genv.scope_manager.current_module_name(),
105
+ Some("Utils".to_string())
106
+ );
107
+
108
+ exit_scope(&mut genv);
109
+ assert_eq!(genv.scope_manager.current_module_name(), None);
110
+ }
111
+
52
112
  #[test]
53
113
  fn test_nested_method_scope() {
54
114
  let mut genv = GlobalEnv::new();
@@ -67,4 +127,93 @@ mod tests {
67
127
 
68
128
  assert_eq!(genv.scope_manager.current_class_name(), None);
69
129
  }
130
+
131
+ #[test]
132
+ fn test_method_in_module() {
133
+ let mut genv = GlobalEnv::new();
134
+
135
+ install_module(&mut genv, "Helpers".to_string());
136
+ install_method(&mut genv, "format".to_string());
137
+
138
+ // Should find module context from within method
139
+ assert_eq!(
140
+ genv.scope_manager.current_module_name(),
141
+ Some("Helpers".to_string())
142
+ );
143
+
144
+ exit_scope(&mut genv); // exit method
145
+ exit_scope(&mut genv); // exit module
146
+
147
+ assert_eq!(genv.scope_manager.current_module_name(), None);
148
+ }
149
+
150
+ #[test]
151
+ fn test_extract_simple_class_name() {
152
+ let source = "class User; end";
153
+ let session = ParseSession::new();
154
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
155
+ let root = parse_result.node();
156
+ let program = root.as_program_node().unwrap();
157
+ let stmt = program.statements().body().first().unwrap();
158
+ let class_node = stmt.as_class_node().unwrap();
159
+
160
+ let name = extract_class_name(&class_node);
161
+ assert_eq!(name, "User");
162
+ }
163
+
164
+ #[test]
165
+ fn test_extract_qualified_class_name() {
166
+ let source = "class Api::User; end";
167
+ let session = ParseSession::new();
168
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
169
+ let root = parse_result.node();
170
+ let program = root.as_program_node().unwrap();
171
+ let stmt = program.statements().body().first().unwrap();
172
+ let class_node = stmt.as_class_node().unwrap();
173
+
174
+ let name = extract_class_name(&class_node);
175
+ assert_eq!(name, "Api::User");
176
+ }
177
+
178
+ #[test]
179
+ fn test_extract_deeply_qualified_class_name() {
180
+ let source = "class Api::V1::Admin::User; end";
181
+ let session = ParseSession::new();
182
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
183
+ let root = parse_result.node();
184
+ let program = root.as_program_node().unwrap();
185
+ let stmt = program.statements().body().first().unwrap();
186
+ let class_node = stmt.as_class_node().unwrap();
187
+
188
+ let name = extract_class_name(&class_node);
189
+ assert_eq!(name, "Api::V1::Admin::User");
190
+ }
191
+
192
+ #[test]
193
+ fn test_extract_simple_module_name() {
194
+ let source = "module Utils; end";
195
+ let session = ParseSession::new();
196
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
197
+ let root = parse_result.node();
198
+ let program = root.as_program_node().unwrap();
199
+ let stmt = program.statements().body().first().unwrap();
200
+ let module_node = stmt.as_module_node().unwrap();
201
+
202
+ let name = extract_module_name(&module_node);
203
+ assert_eq!(name, "Utils");
204
+ }
205
+
206
+ #[test]
207
+ fn test_extract_qualified_module_name() {
208
+ let source = "module Api::V1; end";
209
+ let session = ParseSession::new();
210
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
211
+ let root = parse_result.node();
212
+ let program = root.as_program_node().unwrap();
213
+ let stmt = program.statements().body().first().unwrap();
214
+ let module_node = stmt.as_module_node().unwrap();
215
+
216
+ let name = extract_module_name(&module_node);
217
+ assert_eq!(name, "Api::V1");
218
+ }
70
219
  }
@@ -9,7 +9,6 @@ use crate::source_map::SourceLocation;
9
9
  use ruby_prism::Node;
10
10
 
11
11
  use super::calls::install_method_call;
12
- use super::literals::install_literal;
13
12
  use super::variables::{
14
13
  install_ivar_read, install_ivar_write, install_local_var_read, install_local_var_write,
15
14
  install_self,
@@ -34,10 +33,15 @@ pub enum NeedsChildKind<'a> {
34
33
  receiver: Node<'a>,
35
34
  method_name: String,
36
35
  location: SourceLocation,
36
+ /// Optional block attached to the method call
37
+ block: Option<Node<'a>>,
37
38
  },
38
39
  }
39
40
 
40
41
  /// First pass: check if node can be handled immediately without child processing
42
+ ///
43
+ /// Note: Literals (including Array) are handled in install.rs via install_literal
44
+ /// because Array literals need child processing for element type inference.
41
45
  pub fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &Node) -> DispatchResult {
42
46
  // Instance variable read: @name
43
47
  if let Some(ivar_read) = node.as_instance_variable_read_node() {
@@ -62,11 +66,6 @@ pub fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &Node) -
62
66
  };
63
67
  }
64
68
 
65
- // Literals (String, Integer, Array, Hash, nil, true, false, Symbol)
66
- if let Some(vtx) = install_literal(genv, node) {
67
- return DispatchResult::Vertex(vtx);
68
- }
69
-
70
69
  DispatchResult::NotHandled
71
70
  }
72
71
 
@@ -90,16 +89,25 @@ pub fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsCh
90
89
  });
91
90
  }
92
91
 
93
- // Method call: x.upcase
92
+ // Method call: x.upcase or x.each { |i| ... }
94
93
  if let Some(call_node) = node.as_call_node() {
95
94
  if let Some(receiver) = call_node.receiver() {
96
95
  let method_name = String::from_utf8_lossy(call_node.name().as_slice()).to_string();
96
+ // Use call_operator_loc (.) for error position, fallback to node location
97
+ let prism_location = call_node
98
+ .call_operator_loc()
99
+ .unwrap_or_else(|| node.location());
97
100
  let location =
98
- SourceLocation::from_prism_location_with_source(&node.location(), source);
101
+ SourceLocation::from_prism_location_with_source(&prism_location, source);
102
+
103
+ // Get block if present (e.g., `x.each { |i| ... }`)
104
+ let block = call_node.block();
105
+
99
106
  return Some(NeedsChildKind::MethodCall {
100
107
  receiver,
101
108
  method_name,
102
109
  location,
110
+ block,
103
111
  });
104
112
  }
105
113
  }