method-ray 0.1.8 → 0.1.10
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 +38 -0
- data/README.md +9 -11
- data/{rust → core}/Cargo.toml +1 -1
- data/core/src/analyzer/assignments.rs +219 -0
- data/{rust → core}/src/analyzer/blocks.rs +0 -50
- data/{rust → core}/src/analyzer/calls.rs +3 -32
- data/core/src/analyzer/conditionals.rs +190 -0
- data/core/src/analyzer/definitions.rs +205 -0
- data/core/src/analyzer/dispatch.rs +455 -0
- data/core/src/analyzer/exceptions.rs +168 -0
- data/{rust → core}/src/analyzer/install.rs +16 -1
- data/{rust → core}/src/analyzer/literals.rs +3 -71
- data/core/src/analyzer/loops.rs +94 -0
- data/{rust → core}/src/analyzer/mod.rs +1 -15
- data/core/src/analyzer/operators.rs +79 -0
- data/{rust → core}/src/analyzer/parameters.rs +4 -67
- data/core/src/analyzer/parentheses.rs +25 -0
- data/core/src/analyzer/returns.rs +39 -0
- data/core/src/analyzer/super_calls.rs +74 -0
- data/{rust → core}/src/analyzer/variables.rs +5 -25
- data/{rust → core}/src/checker.rs +0 -13
- data/{rust → core}/src/diagnostics/diagnostic.rs +0 -41
- data/{rust → core}/src/diagnostics/formatter.rs +0 -38
- data/{rust → core}/src/env/box_manager.rs +0 -30
- data/{rust → core}/src/env/global_env.rs +67 -80
- data/core/src/env/local_env.rs +42 -0
- data/core/src/env/method_registry.rs +173 -0
- data/core/src/env/scope.rs +299 -0
- data/{rust → core}/src/env/vertex_manager.rs +0 -73
- data/core/src/graph/box.rs +347 -0
- data/{rust → core}/src/graph/change_set.rs +0 -65
- data/{rust → core}/src/graph/vertex.rs +0 -69
- data/{rust → core}/src/parser.rs +0 -77
- data/{rust → core}/src/types.rs +11 -0
- data/ext/Cargo.toml +2 -2
- data/lib/methodray/binary_locator.rb +2 -2
- data/lib/methodray/commands.rb +1 -1
- data/lib/methodray/version.rb +1 -1
- metadata +58 -56
- data/rust/src/analyzer/assignments.rs +0 -152
- data/rust/src/analyzer/conditionals.rs +0 -538
- data/rust/src/analyzer/definitions.rs +0 -719
- data/rust/src/analyzer/dispatch.rs +0 -1137
- data/rust/src/analyzer/exceptions.rs +0 -521
- data/rust/src/analyzer/loops.rs +0 -176
- data/rust/src/analyzer/operators.rs +0 -284
- data/rust/src/analyzer/parentheses.rs +0 -113
- data/rust/src/analyzer/returns.rs +0 -191
- data/rust/src/env/local_env.rs +0 -92
- data/rust/src/env/method_registry.rs +0 -268
- data/rust/src/env/scope.rs +0 -596
- data/rust/src/graph/box.rs +0 -766
- /data/{rust → core}/src/analyzer/attributes.rs +0 -0
- /data/{rust → core}/src/cache/mod.rs +0 -0
- /data/{rust → core}/src/cache/rbs_cache.rs +0 -0
- /data/{rust → core}/src/cli/args.rs +0 -0
- /data/{rust → core}/src/cli/commands.rs +0 -0
- /data/{rust → core}/src/cli/mod.rs +0 -0
- /data/{rust → core}/src/diagnostics/mod.rs +0 -0
- /data/{rust → core}/src/env/mod.rs +0 -0
- /data/{rust → core}/src/env/type_error.rs +0 -0
- /data/{rust → core}/src/graph/mod.rs +0 -0
- /data/{rust → core}/src/lib.rs +0 -0
- /data/{rust → core}/src/lsp/diagnostics.rs +0 -0
- /data/{rust → core}/src/lsp/main.rs +0 -0
- /data/{rust → core}/src/lsp/mod.rs +0 -0
- /data/{rust → core}/src/lsp/server.rs +0 -0
- /data/{rust → core}/src/main.rs +0 -0
- /data/{rust → core}/src/rbs/converter.rs +0 -0
- /data/{rust → core}/src/rbs/error.rs +0 -0
- /data/{rust → core}/src/rbs/loader.rs +0 -0
- /data/{rust → core}/src/rbs/mod.rs +0 -0
- /data/{rust → core}/src/source_map.rs +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
//! Definition Handlers - Processing Ruby class/method/module definitions
|
|
2
|
+
//!
|
|
3
|
+
//! This module is responsible for:
|
|
4
|
+
//! - Class definition scope management (class Foo ... end)
|
|
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)
|
|
8
|
+
|
|
9
|
+
use std::collections::HashMap;
|
|
10
|
+
|
|
11
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
12
|
+
use crate::graph::{ChangeSet, VertexId};
|
|
13
|
+
use crate::types::Type;
|
|
14
|
+
use ruby_prism::Node;
|
|
15
|
+
|
|
16
|
+
use super::bytes_to_name;
|
|
17
|
+
use super::install::install_statements;
|
|
18
|
+
use super::parameters::install_parameters;
|
|
19
|
+
|
|
20
|
+
/// Process class definition node
|
|
21
|
+
pub(crate) fn process_class_node(
|
|
22
|
+
genv: &mut GlobalEnv,
|
|
23
|
+
lenv: &mut LocalEnv,
|
|
24
|
+
changes: &mut ChangeSet,
|
|
25
|
+
source: &str,
|
|
26
|
+
class_node: &ruby_prism::ClassNode,
|
|
27
|
+
) -> Option<VertexId> {
|
|
28
|
+
let class_name = extract_class_name(class_node);
|
|
29
|
+
let superclass = class_node.superclass().and_then(|sup| extract_constant_path(&sup));
|
|
30
|
+
|
|
31
|
+
// Warn if superclass is a dynamic expression (not a constant path)
|
|
32
|
+
// TODO: Replace eprintln! with structured diagnostic (record_type_error or warning)
|
|
33
|
+
// so this is visible in LSP mode and includes source location.
|
|
34
|
+
if class_node.superclass().is_some() && superclass.is_none() {
|
|
35
|
+
eprintln!(
|
|
36
|
+
"[methodray] warning: dynamic superclass expression in class {}; inheritance will be ignored",
|
|
37
|
+
class_name
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
install_class(genv, class_name, superclass.as_deref());
|
|
42
|
+
|
|
43
|
+
if let Some(body) = class_node.body() {
|
|
44
|
+
if let Some(statements) = body.as_statements_node() {
|
|
45
|
+
install_statements(genv, lenv, changes, source, &statements);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
exit_scope(genv);
|
|
50
|
+
None
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Process module definition node
|
|
54
|
+
pub(crate) fn process_module_node(
|
|
55
|
+
genv: &mut GlobalEnv,
|
|
56
|
+
lenv: &mut LocalEnv,
|
|
57
|
+
changes: &mut ChangeSet,
|
|
58
|
+
source: &str,
|
|
59
|
+
module_node: &ruby_prism::ModuleNode,
|
|
60
|
+
) -> Option<VertexId> {
|
|
61
|
+
let module_name = extract_module_name(module_node);
|
|
62
|
+
install_module(genv, module_name);
|
|
63
|
+
|
|
64
|
+
if let Some(body) = module_node.body() {
|
|
65
|
+
if let Some(statements) = body.as_statements_node() {
|
|
66
|
+
install_statements(genv, lenv, changes, source, &statements);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
exit_scope(genv);
|
|
71
|
+
None
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Process method definition node
|
|
75
|
+
pub(crate) fn process_def_node(
|
|
76
|
+
genv: &mut GlobalEnv,
|
|
77
|
+
lenv: &mut LocalEnv,
|
|
78
|
+
changes: &mut ChangeSet,
|
|
79
|
+
source: &str,
|
|
80
|
+
def_node: &ruby_prism::DefNode,
|
|
81
|
+
) -> Option<VertexId> {
|
|
82
|
+
let method_name = bytes_to_name(def_node.name().as_slice());
|
|
83
|
+
|
|
84
|
+
// Check if this is a class method (def self.foo)
|
|
85
|
+
let is_class_method = def_node
|
|
86
|
+
.receiver()
|
|
87
|
+
.map(|r| r.as_self_node().is_some())
|
|
88
|
+
.unwrap_or(false);
|
|
89
|
+
|
|
90
|
+
install_method(genv, method_name.clone());
|
|
91
|
+
|
|
92
|
+
let merge_vtx = genv.scope_manager.current_method_return_vertex();
|
|
93
|
+
|
|
94
|
+
// Process parameters BEFORE processing body
|
|
95
|
+
let (param_vtxs, keyword_param_vtxs) = if let Some(params_node) = def_node.parameters() {
|
|
96
|
+
install_parameters(genv, lenv, changes, source, ¶ms_node)
|
|
97
|
+
} else {
|
|
98
|
+
(vec![], HashMap::new())
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
let mut last_vtx = None;
|
|
102
|
+
if let Some(body) = def_node.body() {
|
|
103
|
+
if let Some(statements) = body.as_statements_node() {
|
|
104
|
+
last_vtx = install_statements(genv, lenv, changes, source, &statements);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Connect last expression to merge vertex so that implicit return
|
|
109
|
+
// (Ruby's last-expression-is-return-value) is included in the union type
|
|
110
|
+
if let (Some(last), Some(merge)) = (last_vtx, merge_vtx) {
|
|
111
|
+
genv.add_edge(last, merge);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Register user-defined method with merge vertex as return vertex
|
|
115
|
+
let return_vtx = merge_vtx.or(last_vtx);
|
|
116
|
+
if let Some(ret_vtx) = return_vtx {
|
|
117
|
+
let recv_type_name = genv.scope_manager.current_qualified_name();
|
|
118
|
+
|
|
119
|
+
if let Some(name) = recv_type_name {
|
|
120
|
+
let recv_type = if is_class_method {
|
|
121
|
+
Type::singleton(&name)
|
|
122
|
+
} else {
|
|
123
|
+
Type::instance(&name)
|
|
124
|
+
};
|
|
125
|
+
let kw_params = (!keyword_param_vtxs.is_empty()).then_some(keyword_param_vtxs);
|
|
126
|
+
genv.register_user_method(
|
|
127
|
+
recv_type,
|
|
128
|
+
&method_name,
|
|
129
|
+
ret_vtx,
|
|
130
|
+
param_vtxs,
|
|
131
|
+
kw_params,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
exit_scope(genv);
|
|
137
|
+
None
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Install class definition
|
|
141
|
+
fn install_class(genv: &mut GlobalEnv, class_name: String, superclass: Option<&str>) {
|
|
142
|
+
genv.enter_class(class_name, superclass);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Install module definition
|
|
146
|
+
fn install_module(genv: &mut GlobalEnv, module_name: String) {
|
|
147
|
+
genv.enter_module(module_name);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Install method definition
|
|
151
|
+
fn install_method(genv: &mut GlobalEnv, method_name: String) {
|
|
152
|
+
genv.enter_method(method_name);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Exit current scope (class, module, or method)
|
|
156
|
+
fn exit_scope(genv: &mut GlobalEnv) {
|
|
157
|
+
genv.exit_scope();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Extract class name from ClassNode
|
|
161
|
+
/// Supports both simple names (User) and qualified names (Api::V1::User)
|
|
162
|
+
fn extract_class_name(class_node: &ruby_prism::ClassNode) -> String {
|
|
163
|
+
extract_constant_path(&class_node.constant_path()).unwrap_or_else(|| "UnknownClass".to_string())
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Extract module name from ModuleNode
|
|
167
|
+
/// Supports both simple names (Utils) and qualified names (Api::V1::Utils)
|
|
168
|
+
fn extract_module_name(module_node: &ruby_prism::ModuleNode) -> String {
|
|
169
|
+
extract_constant_path(&module_node.constant_path())
|
|
170
|
+
.unwrap_or_else(|| "UnknownModule".to_string())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Extract constant path from a Node (handles both ConstantReadNode and ConstantPathNode)
|
|
174
|
+
///
|
|
175
|
+
/// Examples:
|
|
176
|
+
/// - `User` (ConstantReadNode) → "User"
|
|
177
|
+
/// - `Api::User` (ConstantPathNode) → "Api::User"
|
|
178
|
+
/// - `Api::V1::User` (nested ConstantPathNode) → "Api::V1::User"
|
|
179
|
+
/// - `::Api::User` (absolute path with COLON3) → "Api::User"
|
|
180
|
+
pub(crate) fn extract_constant_path(node: &Node) -> Option<String> {
|
|
181
|
+
// Simple constant read: `User`
|
|
182
|
+
if let Some(constant_read) = node.as_constant_read_node() {
|
|
183
|
+
return Some(bytes_to_name(constant_read.name().as_slice()));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Constant path: `Api::User` or `Api::V1::User`
|
|
187
|
+
if let Some(constant_path) = node.as_constant_path_node() {
|
|
188
|
+
// name() returns Option<ConstantId>, use as_slice() to get &[u8]
|
|
189
|
+
let name = constant_path
|
|
190
|
+
.name()
|
|
191
|
+
.map(|id| bytes_to_name(id.as_slice()))?;
|
|
192
|
+
|
|
193
|
+
// Get parent path if exists
|
|
194
|
+
if let Some(parent_node) = constant_path.parent() {
|
|
195
|
+
if let Some(parent_path) = extract_constant_path(&parent_node) {
|
|
196
|
+
return Some(format!("{}::{}", parent_path, name));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// No parent (absolute path like `::User`)
|
|
201
|
+
return Some(name);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
None
|
|
205
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
//! Node Dispatch - Dispatch AST nodes to appropriate handlers
|
|
2
|
+
//!
|
|
3
|
+
//! This module handles the pattern matching of Ruby AST nodes
|
|
4
|
+
//! and dispatches them to specialized handlers.
|
|
5
|
+
|
|
6
|
+
use std::collections::HashMap;
|
|
7
|
+
|
|
8
|
+
use crate::env::{GlobalEnv, LocalEnv};
|
|
9
|
+
use crate::graph::{BlockParameterTypeBox, ChangeSet, VertexId};
|
|
10
|
+
use crate::source_map::SourceLocation;
|
|
11
|
+
use crate::types::Type;
|
|
12
|
+
use ruby_prism::Node;
|
|
13
|
+
|
|
14
|
+
use super::bytes_to_name;
|
|
15
|
+
use super::calls::install_method_call;
|
|
16
|
+
use super::variables::{
|
|
17
|
+
install_ivar_read, install_ivar_write, install_local_var_read, install_local_var_write,
|
|
18
|
+
install_self,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/// Collect positional and keyword arguments from AST argument nodes.
|
|
22
|
+
///
|
|
23
|
+
/// Shared by method calls (`dispatch.rs`) and super calls (`super_calls.rs`).
|
|
24
|
+
pub(crate) fn collect_arguments<'a>(
|
|
25
|
+
genv: &mut GlobalEnv,
|
|
26
|
+
lenv: &mut LocalEnv,
|
|
27
|
+
changes: &mut ChangeSet,
|
|
28
|
+
source: &str,
|
|
29
|
+
args: impl Iterator<Item = ruby_prism::Node<'a>>,
|
|
30
|
+
) -> (Vec<VertexId>, Option<HashMap<String, VertexId>>) {
|
|
31
|
+
let mut positional: Vec<VertexId> = Vec::new();
|
|
32
|
+
let mut keyword: HashMap<String, VertexId> = HashMap::new();
|
|
33
|
+
|
|
34
|
+
for arg in args {
|
|
35
|
+
if let Some(kw_hash) = arg.as_keyword_hash_node() {
|
|
36
|
+
for element in kw_hash.elements().iter() {
|
|
37
|
+
let assoc = match element.as_assoc_node() {
|
|
38
|
+
Some(a) => a,
|
|
39
|
+
None => continue,
|
|
40
|
+
};
|
|
41
|
+
let name = match assoc.key().as_symbol_node() {
|
|
42
|
+
Some(sym) => bytes_to_name(sym.unescaped()),
|
|
43
|
+
None => continue,
|
|
44
|
+
};
|
|
45
|
+
if let Some(vtx) =
|
|
46
|
+
super::install::install_node(genv, lenv, changes, source, &assoc.value())
|
|
47
|
+
{
|
|
48
|
+
keyword.insert(name, vtx);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} else if let Some(vtx) = super::install::install_node(genv, lenv, changes, source, &arg) {
|
|
52
|
+
positional.push(vtx);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let kw = (!keyword.is_empty()).then_some(keyword);
|
|
57
|
+
(positional, kw)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Kind of attr_* declaration
|
|
61
|
+
#[derive(Debug, Clone, Copy)]
|
|
62
|
+
pub(crate) enum AttrKind {
|
|
63
|
+
Reader,
|
|
64
|
+
Writer,
|
|
65
|
+
Accessor,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Result of dispatching a simple node (no child processing needed)
|
|
69
|
+
pub(crate) enum DispatchResult {
|
|
70
|
+
/// Node produced a vertex
|
|
71
|
+
Vertex(VertexId),
|
|
72
|
+
/// Node was not handled
|
|
73
|
+
NotHandled,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Kind of child processing needed
|
|
77
|
+
pub(crate) enum NeedsChildKind<'a> {
|
|
78
|
+
/// Instance variable write: need to process value, then call finish_ivar_write
|
|
79
|
+
IvarWrite { ivar_name: String, value: Node<'a> },
|
|
80
|
+
/// Local variable write: need to process value, then call finish_local_var_write
|
|
81
|
+
LocalVarWrite { var_name: String, value: Node<'a> },
|
|
82
|
+
/// Method call: need to process receiver, then call finish_method_call
|
|
83
|
+
MethodCall {
|
|
84
|
+
receiver: Node<'a>,
|
|
85
|
+
method_name: String,
|
|
86
|
+
location: SourceLocation,
|
|
87
|
+
/// Optional block attached to the method call
|
|
88
|
+
block: Option<Node<'a>>,
|
|
89
|
+
/// Arguments to the method call
|
|
90
|
+
arguments: Vec<Node<'a>>,
|
|
91
|
+
/// Whether this is a safe navigation call (`&.`)
|
|
92
|
+
safe_navigation: bool,
|
|
93
|
+
},
|
|
94
|
+
/// Implicit self method call: method call without explicit receiver (implicit self)
|
|
95
|
+
ImplicitSelfCall {
|
|
96
|
+
method_name: String,
|
|
97
|
+
location: SourceLocation,
|
|
98
|
+
block: Option<Node<'a>>,
|
|
99
|
+
arguments: Vec<Node<'a>>,
|
|
100
|
+
},
|
|
101
|
+
/// attr_reader / attr_writer / attr_accessor declaration
|
|
102
|
+
AttrDeclaration {
|
|
103
|
+
kind: AttrKind,
|
|
104
|
+
attr_names: Vec<String>,
|
|
105
|
+
},
|
|
106
|
+
/// include / extend declaration: `include Greetable`, `extend ClassMethods`
|
|
107
|
+
ModuleMixinDeclaration {
|
|
108
|
+
module_names: Vec<String>,
|
|
109
|
+
kind: MixinKind,
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Kind of module mixin (include or extend)
|
|
114
|
+
#[derive(Debug, Clone, Copy)]
|
|
115
|
+
pub(crate) enum MixinKind {
|
|
116
|
+
Include,
|
|
117
|
+
Extend,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// First pass: check if node can be handled immediately without child processing
|
|
121
|
+
///
|
|
122
|
+
/// Note: Literals (including Array) are handled in install.rs via install_literal
|
|
123
|
+
/// because Array literals need child processing for element type inference.
|
|
124
|
+
pub(crate) fn dispatch_simple(genv: &mut GlobalEnv, lenv: &mut LocalEnv, node: &Node) -> DispatchResult {
|
|
125
|
+
// Instance variable read: @name
|
|
126
|
+
if let Some(ivar_read) = node.as_instance_variable_read_node() {
|
|
127
|
+
let ivar_name = bytes_to_name(ivar_read.name().as_slice());
|
|
128
|
+
return match install_ivar_read(genv, &ivar_name) {
|
|
129
|
+
Some(vtx) => DispatchResult::Vertex(vtx),
|
|
130
|
+
None => DispatchResult::NotHandled,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// self
|
|
135
|
+
if node.as_self_node().is_some() {
|
|
136
|
+
return DispatchResult::Vertex(install_self(genv));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Local variable read: x
|
|
140
|
+
if let Some(read_node) = node.as_local_variable_read_node() {
|
|
141
|
+
let var_name = bytes_to_name(read_node.name().as_slice());
|
|
142
|
+
return match install_local_var_read(lenv, &var_name) {
|
|
143
|
+
Some(vtx) => DispatchResult::Vertex(vtx),
|
|
144
|
+
None => DispatchResult::NotHandled,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ConstantReadNode: User → Type::Singleton("User") or Type::Singleton("Api::User")
|
|
149
|
+
if let Some(const_read) = node.as_constant_read_node() {
|
|
150
|
+
let name = bytes_to_name(const_read.name().as_slice());
|
|
151
|
+
let resolved_name = genv.scope_manager.lookup_constant(&name)
|
|
152
|
+
.unwrap_or(name);
|
|
153
|
+
let vtx = genv.new_source(Type::singleton(&resolved_name));
|
|
154
|
+
return DispatchResult::Vertex(vtx);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ConstantPathNode: Api::User → Type::Singleton("Api::User")
|
|
158
|
+
if node.as_constant_path_node().is_some() {
|
|
159
|
+
if let Some(name) = super::definitions::extract_constant_path(node) {
|
|
160
|
+
let vtx = genv.new_source(Type::singleton(&name));
|
|
161
|
+
return DispatchResult::Vertex(vtx);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
DispatchResult::NotHandled
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Extract symbol names from attr_* arguments (e.g., `attr_reader :name, :email`)
|
|
169
|
+
fn extract_symbol_names(call_node: &ruby_prism::CallNode) -> Vec<String> {
|
|
170
|
+
call_node
|
|
171
|
+
.arguments()
|
|
172
|
+
.map(|args| {
|
|
173
|
+
args.arguments()
|
|
174
|
+
.iter()
|
|
175
|
+
.filter_map(|arg| {
|
|
176
|
+
arg.as_symbol_node().map(|sym| {
|
|
177
|
+
bytes_to_name(sym.unescaped())
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
.collect()
|
|
181
|
+
})
|
|
182
|
+
.unwrap_or_default()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// Extract module names from include/extend arguments
|
|
186
|
+
fn extract_mixin_module_names(call_node: &ruby_prism::CallNode) -> Vec<String> {
|
|
187
|
+
call_node
|
|
188
|
+
.arguments()
|
|
189
|
+
.map(|args| {
|
|
190
|
+
args.arguments()
|
|
191
|
+
.iter()
|
|
192
|
+
.filter_map(|arg| super::definitions::extract_constant_path(&arg))
|
|
193
|
+
.collect()
|
|
194
|
+
})
|
|
195
|
+
.unwrap_or_default()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// Check if node needs child processing
|
|
199
|
+
pub(crate) fn dispatch_needs_child<'a>(node: &Node<'a>, source: &str) -> Option<NeedsChildKind<'a>> {
|
|
200
|
+
// Instance variable write: @name = value
|
|
201
|
+
if let Some(ivar_write) = node.as_instance_variable_write_node() {
|
|
202
|
+
let ivar_name = bytes_to_name(ivar_write.name().as_slice());
|
|
203
|
+
return Some(NeedsChildKind::IvarWrite {
|
|
204
|
+
ivar_name,
|
|
205
|
+
value: ivar_write.value(),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Local variable write: x = value
|
|
210
|
+
if let Some(write_node) = node.as_local_variable_write_node() {
|
|
211
|
+
let var_name = bytes_to_name(write_node.name().as_slice());
|
|
212
|
+
return Some(NeedsChildKind::LocalVarWrite {
|
|
213
|
+
var_name,
|
|
214
|
+
value: write_node.value(),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Method call: x.upcase, x.each { |i| ... }, or name (implicit self)
|
|
219
|
+
if let Some(call_node) = node.as_call_node() {
|
|
220
|
+
let method_name = bytes_to_name(call_node.name().as_slice());
|
|
221
|
+
let block = call_node.block();
|
|
222
|
+
let arguments: Vec<Node<'a>> = call_node
|
|
223
|
+
.arguments()
|
|
224
|
+
.map(|args| args.arguments().iter().collect())
|
|
225
|
+
.unwrap_or_default();
|
|
226
|
+
|
|
227
|
+
if let Some(receiver) = call_node.receiver() {
|
|
228
|
+
// Explicit receiver: x.upcase, x.each { |i| ... }
|
|
229
|
+
let prism_location = call_node
|
|
230
|
+
.call_operator_loc()
|
|
231
|
+
.unwrap_or_else(|| node.location());
|
|
232
|
+
let location =
|
|
233
|
+
SourceLocation::from_prism_location_with_source(&prism_location, source);
|
|
234
|
+
|
|
235
|
+
return Some(NeedsChildKind::MethodCall {
|
|
236
|
+
receiver,
|
|
237
|
+
method_name,
|
|
238
|
+
location,
|
|
239
|
+
block,
|
|
240
|
+
arguments,
|
|
241
|
+
safe_navigation: call_node.is_safe_navigation(),
|
|
242
|
+
});
|
|
243
|
+
} else {
|
|
244
|
+
// No receiver: implicit self method call (e.g., `name`, `puts "hello"`)
|
|
245
|
+
|
|
246
|
+
if let Some(kind) = match method_name.as_str() {
|
|
247
|
+
"attr_reader" => Some(AttrKind::Reader),
|
|
248
|
+
"attr_writer" => Some(AttrKind::Writer),
|
|
249
|
+
"attr_accessor" => Some(AttrKind::Accessor),
|
|
250
|
+
_ => None,
|
|
251
|
+
} {
|
|
252
|
+
let attr_names = extract_symbol_names(&call_node);
|
|
253
|
+
if !attr_names.is_empty() {
|
|
254
|
+
return Some(NeedsChildKind::AttrDeclaration { kind, attr_names });
|
|
255
|
+
}
|
|
256
|
+
return None;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let mixin_kind = match method_name.as_str() {
|
|
260
|
+
"include" => Some(MixinKind::Include),
|
|
261
|
+
"extend" => Some(MixinKind::Extend),
|
|
262
|
+
_ => None,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if let Some(kind) = mixin_kind {
|
|
266
|
+
let module_names = extract_mixin_module_names(&call_node);
|
|
267
|
+
if !module_names.is_empty() {
|
|
268
|
+
return Some(NeedsChildKind::ModuleMixinDeclaration { module_names, kind });
|
|
269
|
+
}
|
|
270
|
+
return None;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let prism_location = call_node
|
|
274
|
+
.message_loc()
|
|
275
|
+
.unwrap_or_else(|| node.location());
|
|
276
|
+
let location =
|
|
277
|
+
SourceLocation::from_prism_location_with_source(&prism_location, source);
|
|
278
|
+
|
|
279
|
+
return Some(NeedsChildKind::ImplicitSelfCall {
|
|
280
|
+
method_name,
|
|
281
|
+
location,
|
|
282
|
+
block,
|
|
283
|
+
arguments,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
None
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// Process a node that needs child processing
|
|
292
|
+
///
|
|
293
|
+
/// This function handles the second phase of two-phase dispatch:
|
|
294
|
+
/// 1. `dispatch_needs_child` identifies the node kind and extracts data
|
|
295
|
+
/// 2. `process_needs_child` processes child nodes and completes the operation
|
|
296
|
+
pub(crate) fn process_needs_child(
|
|
297
|
+
genv: &mut GlobalEnv,
|
|
298
|
+
lenv: &mut LocalEnv,
|
|
299
|
+
changes: &mut ChangeSet,
|
|
300
|
+
source: &str,
|
|
301
|
+
kind: NeedsChildKind,
|
|
302
|
+
) -> Option<VertexId> {
|
|
303
|
+
match kind {
|
|
304
|
+
NeedsChildKind::IvarWrite { ivar_name, value } => {
|
|
305
|
+
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
|
|
306
|
+
Some(finish_ivar_write(genv, ivar_name, value_vtx))
|
|
307
|
+
}
|
|
308
|
+
NeedsChildKind::LocalVarWrite { var_name, value } => {
|
|
309
|
+
let value_vtx = super::install::install_node(genv, lenv, changes, source, &value)?;
|
|
310
|
+
Some(finish_local_var_write(genv, lenv, changes, var_name, value_vtx))
|
|
311
|
+
}
|
|
312
|
+
NeedsChildKind::MethodCall {
|
|
313
|
+
receiver,
|
|
314
|
+
method_name,
|
|
315
|
+
location,
|
|
316
|
+
block,
|
|
317
|
+
arguments,
|
|
318
|
+
safe_navigation,
|
|
319
|
+
} => {
|
|
320
|
+
let recv_vtx = super::install::install_node(genv, lenv, changes, source, &receiver)?;
|
|
321
|
+
process_method_call_common(
|
|
322
|
+
genv, lenv, changes, source,
|
|
323
|
+
MethodCallContext { recv_vtx, method_name, location, block, arguments, safe_navigation },
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
NeedsChildKind::ImplicitSelfCall {
|
|
327
|
+
method_name,
|
|
328
|
+
location,
|
|
329
|
+
block,
|
|
330
|
+
arguments,
|
|
331
|
+
} => {
|
|
332
|
+
// Use qualified name to match method registration in definitions.rs
|
|
333
|
+
let recv_vtx = if let Some(name) = genv.scope_manager.current_qualified_name() {
|
|
334
|
+
genv.new_source(Type::instance(&name))
|
|
335
|
+
} else {
|
|
336
|
+
genv.new_source(Type::instance("Object"))
|
|
337
|
+
};
|
|
338
|
+
process_method_call_common(
|
|
339
|
+
genv, lenv, changes, source,
|
|
340
|
+
// Implicit self calls cannot use safe navigation (`&.` requires explicit receiver)
|
|
341
|
+
MethodCallContext { recv_vtx, method_name, location, block, arguments, safe_navigation: false },
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
NeedsChildKind::AttrDeclaration { kind, attr_names } => {
|
|
345
|
+
super::attributes::process_attr_declaration(genv, kind, attr_names);
|
|
346
|
+
None
|
|
347
|
+
}
|
|
348
|
+
NeedsChildKind::ModuleMixinDeclaration { module_names, kind } => {
|
|
349
|
+
if let Some(class_name) = genv.scope_manager.current_qualified_name() {
|
|
350
|
+
// Ruby processes `include/extend A, B` right-to-left (B first, then A on top),
|
|
351
|
+
// so A ends up with higher MRO priority. Reverse to match this behavior.
|
|
352
|
+
for module_name in module_names.iter().rev() {
|
|
353
|
+
match kind {
|
|
354
|
+
MixinKind::Include => genv.record_include(&class_name, module_name),
|
|
355
|
+
MixinKind::Extend => genv.record_extend(&class_name, module_name),
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
None
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/// Finish instance variable write after child is processed
|
|
365
|
+
fn finish_ivar_write(genv: &mut GlobalEnv, ivar_name: String, value_vtx: VertexId) -> VertexId {
|
|
366
|
+
install_ivar_write(genv, ivar_name, value_vtx)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/// Finish local variable write after child is processed
|
|
370
|
+
fn finish_local_var_write(
|
|
371
|
+
genv: &mut GlobalEnv,
|
|
372
|
+
lenv: &mut LocalEnv,
|
|
373
|
+
changes: &mut ChangeSet,
|
|
374
|
+
var_name: String,
|
|
375
|
+
value_vtx: VertexId,
|
|
376
|
+
) -> VertexId {
|
|
377
|
+
install_local_var_write(genv, lenv, changes, var_name, value_vtx)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/// Bundled parameters for method call processing
|
|
381
|
+
struct MethodCallContext<'a> {
|
|
382
|
+
recv_vtx: VertexId,
|
|
383
|
+
method_name: String,
|
|
384
|
+
location: SourceLocation,
|
|
385
|
+
block: Option<Node<'a>>,
|
|
386
|
+
arguments: Vec<Node<'a>>,
|
|
387
|
+
safe_navigation: bool,
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/// MethodCall / ImplicitSelfCall common processing:
|
|
391
|
+
/// Handles argument processing, block processing, and MethodCallBox creation after recv_vtx is obtained
|
|
392
|
+
fn process_method_call_common<'a>(
|
|
393
|
+
genv: &mut GlobalEnv,
|
|
394
|
+
lenv: &mut LocalEnv,
|
|
395
|
+
changes: &mut ChangeSet,
|
|
396
|
+
source: &str,
|
|
397
|
+
ctx: MethodCallContext<'a>,
|
|
398
|
+
) -> Option<VertexId> {
|
|
399
|
+
let MethodCallContext {
|
|
400
|
+
recv_vtx,
|
|
401
|
+
method_name,
|
|
402
|
+
location,
|
|
403
|
+
block,
|
|
404
|
+
arguments,
|
|
405
|
+
safe_navigation,
|
|
406
|
+
} = ctx;
|
|
407
|
+
if method_name == "!" {
|
|
408
|
+
return Some(super::operators::process_not_operator(genv));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let (positional_arg_vtxs, kwarg_vtxs) =
|
|
412
|
+
collect_arguments(genv, lenv, changes, source, arguments.into_iter());
|
|
413
|
+
|
|
414
|
+
if let Some(block_node) = block {
|
|
415
|
+
if let Some(block) = block_node.as_block_node() {
|
|
416
|
+
let param_vtxs = super::blocks::process_block_node_with_params(
|
|
417
|
+
genv, lenv, changes, source, &block,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
if !param_vtxs.is_empty() {
|
|
421
|
+
let box_id = genv.alloc_box_id();
|
|
422
|
+
let block_box = BlockParameterTypeBox::new(
|
|
423
|
+
box_id,
|
|
424
|
+
recv_vtx,
|
|
425
|
+
method_name.clone(),
|
|
426
|
+
param_vtxs,
|
|
427
|
+
);
|
|
428
|
+
genv.register_box(box_id, Box::new(block_box));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
Some(finish_method_call(
|
|
434
|
+
genv,
|
|
435
|
+
recv_vtx,
|
|
436
|
+
method_name,
|
|
437
|
+
positional_arg_vtxs,
|
|
438
|
+
kwarg_vtxs,
|
|
439
|
+
location,
|
|
440
|
+
safe_navigation,
|
|
441
|
+
))
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/// Finish method call after receiver is processed
|
|
445
|
+
fn finish_method_call(
|
|
446
|
+
genv: &mut GlobalEnv,
|
|
447
|
+
recv_vtx: VertexId,
|
|
448
|
+
method_name: String,
|
|
449
|
+
arg_vtxs: Vec<VertexId>,
|
|
450
|
+
kwarg_vtxs: Option<HashMap<String, VertexId>>,
|
|
451
|
+
location: SourceLocation,
|
|
452
|
+
safe_navigation: bool,
|
|
453
|
+
) -> VertexId {
|
|
454
|
+
install_method_call(genv, recv_vtx, method_name, arg_vtxs, kwarg_vtxs, Some(location), safe_navigation)
|
|
455
|
+
}
|