Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ path = "src/lsp/main.rs"
required-features = ["lsp"]

[dependencies]
smallvec = "1.13"
rayon = "1.10"
walkdir = "2.5"
anyhow = "1.0"
Expand Down
116 changes: 106 additions & 10 deletions rust/src/analyzer/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
//! - Class definition scope management (class Foo ... end)
//! - Module definition scope management (module Bar ... end)
//! - Method definition scope management (def baz ... end)
//! - Extracting class/module names from AST nodes
//! - Extracting class/module names from AST nodes (including qualified names like Api::User)

use crate::env::GlobalEnv;
use ruby_prism::Node;

/// Install class definition
pub fn install_class(genv: &mut GlobalEnv, class_name: String) {
Expand All @@ -29,26 +30,56 @@ pub fn exit_scope(genv: &mut GlobalEnv) {
}

/// Extract class name from ClassNode
/// Supports both simple names (User) and qualified names (Api::V1::User)
pub fn extract_class_name(class_node: &ruby_prism::ClassNode) -> String {
if let Some(constant_read) = class_node.constant_path().as_constant_read_node() {
String::from_utf8_lossy(constant_read.name().as_slice()).to_string()
} else {
"UnknownClass".to_string()
}
extract_constant_path(&class_node.constant_path()).unwrap_or_else(|| "UnknownClass".to_string())
}

/// Extract module name from ModuleNode
/// Supports both simple names (Utils) and qualified names (Api::V1::Utils)
pub fn extract_module_name(module_node: &ruby_prism::ModuleNode) -> String {
if let Some(constant_read) = module_node.constant_path().as_constant_read_node() {
String::from_utf8_lossy(constant_read.name().as_slice()).to_string()
} else {
"UnknownModule".to_string()
extract_constant_path(&module_node.constant_path())
.unwrap_or_else(|| "UnknownModule".to_string())
}

/// Extract constant path from a Node (handles both ConstantReadNode and ConstantPathNode)
///
/// Examples:
/// - `User` (ConstantReadNode) → "User"
/// - `Api::User` (ConstantPathNode) → "Api::User"
/// - `Api::V1::User` (nested ConstantPathNode) → "Api::V1::User"
/// - `::Api::User` (absolute path with COLON3) → "Api::User"
fn extract_constant_path(node: &Node) -> Option<String> {
// Simple constant read: `User`
if let Some(constant_read) = node.as_constant_read_node() {
return Some(String::from_utf8_lossy(constant_read.name().as_slice()).to_string());
}

// Constant path: `Api::User` or `Api::V1::User`
if let Some(constant_path) = node.as_constant_path_node() {
// name() returns Option<ConstantId>, use as_slice() to get &[u8]
let name = constant_path
.name()
.map(|id| String::from_utf8_lossy(id.as_slice()).to_string())?;

// Get parent path if exists
if let Some(parent_node) = constant_path.parent() {
if let Some(parent_path) = extract_constant_path(&parent_node) {
return Some(format!("{}::{}", parent_path, name));
}
}

// No parent (absolute path like `::User`)
return Some(name);
}

None
}

#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse_ruby_source;

#[test]
fn test_enter_exit_class_scope() {
Expand Down Expand Up @@ -115,4 +146,69 @@ mod tests {

assert_eq!(genv.scope_manager.current_module_name(), None);
}

#[test]
fn test_extract_simple_class_name() {
let source = "class User; end";
let parse_result = parse_ruby_source(source, "test.rb".to_string()).unwrap();
let root = parse_result.node();
let program = root.as_program_node().unwrap();
let stmt = program.statements().body().first().unwrap();
let class_node = stmt.as_class_node().unwrap();

let name = extract_class_name(&class_node);
assert_eq!(name, "User");
}

#[test]
fn test_extract_qualified_class_name() {
let source = "class Api::User; end";
let parse_result = parse_ruby_source(source, "test.rb".to_string()).unwrap();
let root = parse_result.node();
let program = root.as_program_node().unwrap();
let stmt = program.statements().body().first().unwrap();
let class_node = stmt.as_class_node().unwrap();

let name = extract_class_name(&class_node);
assert_eq!(name, "Api::User");
}

#[test]
fn test_extract_deeply_qualified_class_name() {
let source = "class Api::V1::Admin::User; end";
let parse_result = parse_ruby_source(source, "test.rb".to_string()).unwrap();
let root = parse_result.node();
let program = root.as_program_node().unwrap();
let stmt = program.statements().body().first().unwrap();
let class_node = stmt.as_class_node().unwrap();

let name = extract_class_name(&class_node);
assert_eq!(name, "Api::V1::Admin::User");
}

#[test]
fn test_extract_simple_module_name() {
let source = "module Utils; end";
let parse_result = parse_ruby_source(source, "test.rb".to_string()).unwrap();
let root = parse_result.node();
let program = root.as_program_node().unwrap();
let stmt = program.statements().body().first().unwrap();
let module_node = stmt.as_module_node().unwrap();

let name = extract_module_name(&module_node);
assert_eq!(name, "Utils");
}

#[test]
fn test_extract_qualified_module_name() {
let source = "module Api::V1; end";
let parse_result = parse_ruby_source(source, "test.rb".to_string()).unwrap();
let root = parse_result.node();
let program = root.as_program_node().unwrap();
let stmt = program.statements().body().first().unwrap();
let module_node = stmt.as_module_node().unwrap();

let name = extract_module_name(&module_node);
assert_eq!(name, "Api::V1");
}
}
12 changes: 3 additions & 9 deletions rust/src/analyzer/literals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,17 @@ pub fn install_literal(genv: &mut GlobalEnv, node: &Node) -> Option<VertexId> {

// true
if node.as_true_node().is_some() {
return Some(genv.new_source(Type::Instance {
class_name: "TrueClass".to_string(),
}));
return Some(genv.new_source(Type::instance("TrueClass")));
}

// false
if node.as_false_node().is_some() {
return Some(genv.new_source(Type::Instance {
class_name: "FalseClass".to_string(),
}));
return Some(genv.new_source(Type::instance("FalseClass")));
}

// :symbol
if node.as_symbol_node().is_some() {
return Some(genv.new_source(Type::Instance {
class_name: "Symbol".to_string(),
}));
return Some(genv.new_source(Type::symbol()));
}

None
Expand Down
9 changes: 4 additions & 5 deletions rust/src/analyzer/variables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,12 @@ pub fn install_ivar_read(genv: &GlobalEnv, ivar_name: &str) -> Option<VertexId>
}

/// Install self node
/// Uses the fully qualified name if available (e.g., Api::V1::User instead of just User)
pub fn install_self(genv: &mut GlobalEnv) -> VertexId {
if let Some(class_name) = genv.scope_manager.current_class_name() {
genv.new_source(Type::Instance { class_name })
if let Some(qualified_name) = genv.scope_manager.current_qualified_name() {
genv.new_source(Type::instance(&qualified_name))
} else {
genv.new_source(Type::Instance {
class_name: "Object".to_string(),
})
genv.new_source(Type::instance("Object"))
}
}

Expand Down
4 changes: 1 addition & 3 deletions rust/src/cache/rbs_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ pub struct SerializableMethodInfo {
impl SerializableMethodInfo {
/// Parse return type string into Type (simple parser for cached data)
pub fn return_type(&self) -> crate::types::Type {
crate::types::Type::Instance {
class_name: self.return_type_str.clone(),
}
crate::types::Type::instance(&self.return_type_str)
}
}

Expand Down
4 changes: 1 addition & 3 deletions rust/src/checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,7 @@ fn load_rbs_from_cache(genv: &mut GlobalEnv) -> Result<()> {
let methods = cache.methods();

for method_info in methods {
let receiver_type = Type::Instance {
class_name: method_info.receiver_class.clone(),
};
let receiver_type = Type::instance(&method_info.receiver_class);

// Convert block param type strings to Type enums
let block_param_types = method_info.block_param_types.as_ref().map(|types| {
Expand Down
10 changes: 3 additions & 7 deletions rust/src/env/method_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,8 @@ impl MethodRegistry {
}

// For generic types, fall back to base class
if let Type::Generic { class_name, .. } = recv_ty {
let base_type = Type::Instance {
class_name: class_name.clone(),
};
if let Type::Generic { name, .. } = recv_ty {
let base_type = Type::Instance { name: name.clone() };
return self.methods.get(&(base_type, method_name.to_string()));
}

Expand All @@ -81,9 +79,7 @@ mod tests {
registry.register(Type::string(), "length", Type::integer());

let info = registry.resolve(&Type::string(), "length").unwrap();
assert!(
matches!(info.return_type, Type::Instance { ref class_name, .. } if class_name == "Integer")
);
assert_eq!(info.return_type.base_class_name(), Some("Integer"));
}

#[test]
Expand Down
Loading