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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +27 -1
- data/ext/Cargo.toml +1 -1
- data/ext/src/lib.rs +7 -6
- data/lib/methodray/binary_locator.rb +29 -0
- data/lib/methodray/commands.rb +3 -20
- data/lib/methodray/version.rb +1 -1
- data/lib/methodray.rb +1 -1
- data/rust/Cargo.toml +3 -1
- data/rust/src/analyzer/blocks.rs +94 -0
- data/rust/src/analyzer/definitions.rs +157 -8
- data/rust/src/analyzer/dispatch.rs +16 -8
- data/rust/src/analyzer/install.rs +706 -6
- data/rust/src/analyzer/literals.rs +33 -18
- data/rust/src/analyzer/mod.rs +2 -3
- data/rust/src/analyzer/parameters.rs +154 -0
- data/rust/src/analyzer/variables.rs +4 -5
- data/rust/src/cache/rbs_cache.rs +11 -4
- data/rust/src/checker.rs +20 -8
- data/rust/src/env/global_env.rs +30 -2
- data/rust/src/env/method_registry.rs +31 -4
- data/rust/src/env/mod.rs +1 -0
- data/rust/src/env/scope.rs +291 -25
- data/rust/src/graph/box.rs +334 -0
- data/rust/src/graph/change_set.rs +14 -0
- data/rust/src/graph/mod.rs +1 -1
- data/rust/src/lib.rs +2 -1
- data/rust/src/parser.rs +99 -39
- data/rust/src/rbs/converter.rs +16 -11
- data/rust/src/rbs/loader.rs +35 -5
- data/rust/src/rbs/mod.rs +4 -3
- data/rust/src/types.rs +344 -9
- metadata +4 -3
- data/rust/src/analyzer/tests/integration_test.rs +0 -136
- data/rust/src/analyzer/tests/mod.rs +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c950d346d7e01dfcf74b912ee1677450b9ea3d95c3baac937062cc8bf4363163
|
|
4
|
+
data.tar.gz: d9804ea668007c228bb12078ebaa3ec7e4c3b37daa372b2a801765b90978136a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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,
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
data/lib/methodray/commands.rb
CHANGED
|
@@ -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 =
|
|
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
|
data/lib/methodray/version.rb
CHANGED
data/lib/methodray.rb
CHANGED
data/rust/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "methodray-core"
|
|
3
|
-
version = "0.1.
|
|
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
|
-
//! -
|
|
6
|
-
//! -
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(&
|
|
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
|
}
|