diff --git a/rust/Cargo.toml b/rust/Cargo.toml index ee722a8..c0bd610 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -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" diff --git a/rust/src/analyzer/definitions.rs b/rust/src/analyzer/definitions.rs index 7398dc8..517b0d7 100644 --- a/rust/src/analyzer/definitions.rs +++ b/rust/src/analyzer/definitions.rs @@ -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) { @@ -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 { + // 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, 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() { @@ -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"); + } } diff --git a/rust/src/analyzer/literals.rs b/rust/src/analyzer/literals.rs index d4bdd72..ab641ce 100644 --- a/rust/src/analyzer/literals.rs +++ b/rust/src/analyzer/literals.rs @@ -39,23 +39,17 @@ pub fn install_literal(genv: &mut GlobalEnv, node: &Node) -> Option { // 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 diff --git a/rust/src/analyzer/variables.rs b/rust/src/analyzer/variables.rs index bbcfd4c..20eafdb 100644 --- a/rust/src/analyzer/variables.rs +++ b/rust/src/analyzer/variables.rs @@ -45,13 +45,12 @@ pub fn install_ivar_read(genv: &GlobalEnv, ivar_name: &str) -> Option } /// 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")) } } diff --git a/rust/src/cache/rbs_cache.rs b/rust/src/cache/rbs_cache.rs index b95a0b8..20998b8 100644 --- a/rust/src/cache/rbs_cache.rs +++ b/rust/src/cache/rbs_cache.rs @@ -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) } } diff --git a/rust/src/checker.rs b/rust/src/checker.rs index df54967..e5150f9 100644 --- a/rust/src/checker.rs +++ b/rust/src/checker.rs @@ -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| { diff --git a/rust/src/env/method_registry.rs b/rust/src/env/method_registry.rs index cd2cacd..65cf9f6 100644 --- a/rust/src/env/method_registry.rs +++ b/rust/src/env/method_registry.rs @@ -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())); } @@ -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] diff --git a/rust/src/env/scope.rs b/rust/src/env/scope.rs index 94aad46..72397fb 100644 --- a/rust/src/env/scope.rs +++ b/rust/src/env/scope.rs @@ -209,7 +209,7 @@ impl ScopeManager { } } - /// Get current class name + /// Get current class name (simple name, not qualified) pub fn current_class_name(&self) -> Option { let mut current = Some(self.current_scope); @@ -227,7 +227,7 @@ impl ScopeManager { None } - /// Get current module name + /// Get current module name (simple name, not qualified) pub fn current_module_name(&self) -> Option { let mut current = Some(self.current_scope); @@ -245,6 +245,70 @@ impl ScopeManager { None } + /// Get current fully qualified name by traversing all parent class/module scopes + /// + /// For example, in: + /// ```ruby + /// module Api + /// module V1 + /// class User + /// def greet; end + /// end + /// end + /// end + /// ``` + /// When inside `greet`, this returns `Some("Api::V1::User")` + pub fn current_qualified_name(&self) -> Option { + let mut path_segments: Vec = Vec::new(); + let mut current = Some(self.current_scope); + + // Traverse from current scope up to top-level, collecting class/module names + while let Some(scope_id) = current { + if let Some(scope) = self.scopes.get(&scope_id) { + match &scope.kind { + ScopeKind::Class { name, .. } => { + // If the name already contains ::, it's a qualified name from AST + // (e.g., `class Api::User` defined at top level) + if name.contains("::") { + path_segments.push(name.clone()); + } else { + path_segments.push(name.clone()); + } + } + ScopeKind::Module { name } => { + if name.contains("::") { + path_segments.push(name.clone()); + } else { + path_segments.push(name.clone()); + } + } + _ => {} + } + current = scope.parent; + } else { + break; + } + } + + if path_segments.is_empty() { + return None; + } + + // Reverse to get from outermost to innermost + path_segments.reverse(); + + // Join all segments, handling cases where segments may already contain :: + let mut result = String::new(); + for segment in path_segments { + if !result.is_empty() { + result.push_str("::"); + } + result.push_str(&segment); + } + + Some(result) + } + /// Lookup instance variable in enclosing module scope pub fn lookup_instance_var_in_module(&self, name: &str) -> Option { let mut current = Some(self.current_scope); @@ -445,4 +509,88 @@ mod tests { Some(VertexId(100)) ); } + + #[test] + fn test_current_qualified_name_simple_class() { + let mut sm = ScopeManager::new(); + + // module Api; class User; end; end + let class_id = sm.new_scope(ScopeKind::Class { + name: "User".to_string(), + superclass: None, + }); + sm.enter_scope(class_id); + + assert_eq!( + sm.current_qualified_name(), + Some("User".to_string()) + ); + } + + #[test] + fn test_current_qualified_name_nested_module_class() { + let mut sm = ScopeManager::new(); + + // module Api + let api_id = sm.new_scope(ScopeKind::Module { + name: "Api".to_string(), + }); + sm.enter_scope(api_id); + + // module V1 + let v1_id = sm.new_scope(ScopeKind::Module { + name: "V1".to_string(), + }); + sm.enter_scope(v1_id); + + // class User + let user_id = sm.new_scope(ScopeKind::Class { + name: "User".to_string(), + superclass: None, + }); + sm.enter_scope(user_id); + + assert_eq!( + sm.current_qualified_name(), + Some("Api::V1::User".to_string()) + ); + + // def greet + let method_id = sm.new_scope(ScopeKind::Method { + name: "greet".to_string(), + receiver_type: None, + }); + sm.enter_scope(method_id); + + // Inside method, should still get the qualified class name + assert_eq!( + sm.current_qualified_name(), + Some("Api::V1::User".to_string()) + ); + } + + #[test] + fn test_current_qualified_name_with_inline_qualified_class() { + let mut sm = ScopeManager::new(); + + // class Api::User (defined at top level with qualified name) + let class_id = sm.new_scope(ScopeKind::Class { + name: "Api::User".to_string(), + superclass: None, + }); + sm.enter_scope(class_id); + + assert_eq!( + sm.current_qualified_name(), + Some("Api::User".to_string()) + ); + } + + #[test] + fn test_current_qualified_name_at_top_level() { + let sm = ScopeManager::new(); + + // At top level, no class/module + assert_eq!(sm.current_qualified_name(), None); + } } diff --git a/rust/src/graph/box.rs b/rust/src/graph/box.rs index 8459491..d2d76eb 100644 --- a/rust/src/graph/box.rs +++ b/rust/src/graph/box.rs @@ -152,7 +152,9 @@ impl BlockParameterTypeBox { /// - Hash[K, V]: K → type_args[0], V → type_args[1] fn resolve_type_variable(ty: &Type, recv_ty: &Type) -> Option { let type_var_name = match ty { - Type::Instance { class_name } if Self::is_type_variable_name(class_name) => class_name, + Type::Instance { name } if Self::is_type_variable_name(name.full_name()) => { + name.full_name() + } _ => return None, // Not a type variable }; @@ -161,7 +163,7 @@ impl BlockParameterTypeBox { let class_name = recv_ty.base_class_name()?; // Map type variable to type argument index based on class - let index = match (class_name, type_var_name.as_str()) { + let index = match (class_name, type_var_name) { // Array[Elem] ("Array", "Elem") => 0, ("Array", "T") => 0, @@ -217,11 +219,11 @@ impl BoxTrait for BlockParameterTypeBox { // Try to resolve type variable from receiver's type arguments let resolved_type = - if let Some(resolved) = Self::resolve_type_variable(¶m_type, &recv_ty) { + if let Some(resolved) = Self::resolve_type_variable(param_type, &recv_ty) { // Type variable resolved (e.g., Elem → Integer) resolved - } else if let Type::Instance { class_name } = ¶m_type { - if Self::is_type_variable_name(class_name) { + } else if let Type::Instance { name } = ¶m_type { + if Self::is_type_variable_name(name.full_name()) { // Type variable couldn't be resolved, skip continue; } else { @@ -358,9 +360,7 @@ mod tests { Type::array(), "each", Type::array(), - Some(vec![Type::Instance { - class_name: "Elem".to_string(), - }]), + Some(vec![Type::instance("Elem")]), ); let recv_vtx = genv.new_vertex(); @@ -428,9 +428,7 @@ mod tests { Type::array(), "each", Type::array(), - Some(vec![Type::Instance { - class_name: "Elem".to_string(), - }]), + Some(vec![Type::instance("Elem")]), ); // Create receiver vertex with Array[Integer] type @@ -464,14 +462,7 @@ mod tests { Type::hash(), "each", Type::hash(), - Some(vec![ - Type::Instance { - class_name: "K".to_string(), - }, - Type::Instance { - class_name: "V".to_string(), - }, - ]), + Some(vec![Type::instance("K"), Type::instance("V")]), ); // Create receiver vertex with Hash[String, Integer] type diff --git a/rust/src/rbs/converter.rs b/rust/src/rbs/converter.rs index c30d8df..3df4c7a 100644 --- a/rust/src/rbs/converter.rs +++ b/rust/src/rbs/converter.rs @@ -20,18 +20,12 @@ impl RbsTypeConverter { match type_name { "bool" => Type::Union(vec![ - Type::Instance { - class_name: "TrueClass".to_string(), - }, - Type::Instance { - class_name: "FalseClass".to_string(), - }, + Type::instance("TrueClass"), + Type::instance("FalseClass"), ]), "void" | "nil" => Type::Nil, "untyped" | "top" => Type::Bot, - _ => Type::Instance { - class_name: type_name.to_string(), - }, + _ => Type::instance(type_name), } } } @@ -43,12 +37,23 @@ mod tests { #[test] fn test_parse_simple_types() { match RbsTypeConverter::parse("::String") { - Type::Instance { class_name } => assert_eq!(class_name, "String"), + Type::Instance { name } => assert_eq!(name.full_name(), "String"), _ => panic!("Expected Instance type"), } match RbsTypeConverter::parse("Integer") { - Type::Instance { class_name } => assert_eq!(class_name, "Integer"), + Type::Instance { name } => assert_eq!(name.full_name(), "Integer"), + _ => panic!("Expected Instance type"), + } + } + + #[test] + fn test_parse_qualified_types() { + match RbsTypeConverter::parse("::Api::User") { + Type::Instance { name } => { + assert_eq!(name.full_name(), "Api::User"); + assert_eq!(name.name(), "User"); + } _ => panic!("Expected Instance type"), } } diff --git a/rust/src/rbs/loader.rs b/rust/src/rbs/loader.rs index 2296f0e..018b527 100644 --- a/rust/src/rbs/loader.rs +++ b/rust/src/rbs/loader.rs @@ -166,9 +166,7 @@ pub fn register_rbs_methods(genv: &mut GlobalEnv, ruby: &Ruby) -> Result, +} + +impl QualifiedName { + /// Create a new QualifiedName from a full qualified name string + pub fn new(full: &str) -> Self { + let mut offsets = SmallVec::new(); + offsets.push(0); + for (i, _) in full.match_indices("::") { + offsets.push((i + 2) as u16); + } + Self { + full: full.to_string(), + offsets, + } + } + + /// Create a QualifiedName for a simple (non-namespaced) name + pub fn simple(name: &str) -> Self { + Self { + full: name.to_string(), + offsets: smallvec::smallvec![0], + } + } + + /// Get the last segment (class/module name without namespace) + pub fn name(&self) -> &str { + let start = *self.offsets.last().unwrap() as usize; + &self.full[start..] + } + + /// Get the full qualified name string + pub fn full_name(&self) -> &str { + &self.full + } + + /// Get the number of segments + pub fn depth(&self) -> usize { + self.offsets.len() + } + + /// Check if this is a simple (non-namespaced) name + pub fn is_simple(&self) -> bool { + self.offsets.len() == 1 + } + + /// Get the n-th segment (0-indexed) + pub fn segment(&self, n: usize) -> Option<&str> { + if n >= self.offsets.len() { + return None; + } + let start = self.offsets[n] as usize; + let end = self + .offsets + .get(n + 1) + .map(|&o| o as usize - 2) // subtract "::" + .unwrap_or(self.full.len()); + Some(&self.full[start..end]) + } + + /// Get the parent namespace (e.g., "Api::V1" for "Api::V1::User") + pub fn parent(&self) -> Option { + if self.offsets.len() <= 1 { + return None; + } + let last_offset = self.offsets[self.offsets.len() - 1] as usize; + let parent_full = &self.full[..last_offset - 2]; // exclude "::" + Some(Self { + full: parent_full.to_string(), + offsets: self.offsets[..self.offsets.len() - 1].into(), + }) + } + + /// Create a child by appending a name segment + pub fn child(&self, name: &str) -> Self { + let mut full = self.full.clone(); + full.push_str("::"); + full.push_str(name); + + let mut offsets = self.offsets.clone(); + offsets.push((self.full.len() + 2) as u16); + + Self { full, offsets } + } + + /// Join two qualified names + pub fn join(&self, other: &QualifiedName) -> Self { + let mut result = self.clone(); + for i in 0..other.depth() { + if let Some(seg) = other.segment(i) { + result = result.child(seg); + } + } + result + } +} + +impl std::fmt::Display for QualifiedName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.full) + } +} + +impl From<&str> for QualifiedName { + fn from(s: &str) -> Self { + QualifiedName::new(s) + } +} + +impl From for QualifiedName { + fn from(s: String) -> Self { + QualifiedName::new(&s) + } +} + /// Type system for graph-based type inference #[derive(Clone, Debug, PartialEq, Eq, Hash)] #[allow(dead_code)] pub enum Type { - /// Instance type: String, Integer, etc. - Instance { class_name: String }, + /// Instance type: String, Integer, Api::User, etc. + Instance { name: QualifiedName }, /// Generic instance type: Array[Integer], Hash[String, Integer], etc. Generic { - class_name: String, + name: QualifiedName, type_args: Vec, }, /// Singleton type: for class methods - Singleton { class_name: String }, + Singleton { name: QualifiedName }, /// nil type Nil, /// Union type: sum of multiple types @@ -23,15 +148,12 @@ impl Type { /// Convert type to string representation pub fn show(&self) -> String { match self { - Type::Instance { class_name } => class_name.clone(), - Type::Generic { - class_name, - type_args, - } => { + Type::Instance { name } => name.full_name().to_string(), + Type::Generic { name, type_args } => { let args: Vec<_> = type_args.iter().map(|t| t.show()).collect(); - format!("{}[{}]", class_name, args.join(", ")) + format!("{}[{}]", name.full_name(), args.join(", ")) } - Type::Singleton { class_name } => format!("singleton({})", class_name), + Type::Singleton { name } => format!("singleton({})", name.full_name()), Type::Nil => "nil".to_string(), Type::Union(types) => { let names: Vec<_> = types.iter().map(|t| t.show()).collect(); @@ -41,16 +163,26 @@ impl Type { } } - /// Get the base class name (without type arguments) - pub fn base_class_name(&self) -> Option<&str> { + /// Get the qualified name for this type + pub fn qualified_name(&self) -> Option<&QualifiedName> { match self { - Type::Instance { class_name } => Some(class_name), - Type::Generic { class_name, .. } => Some(class_name), - Type::Singleton { class_name } => Some(class_name), + Type::Instance { name } => Some(name), + Type::Generic { name, .. } => Some(name), + Type::Singleton { name } => Some(name), _ => None, } } + /// Get the base class name (full qualified name, without type arguments) + pub fn base_class_name(&self) -> Option<&str> { + self.qualified_name().map(|n| n.full_name()) + } + + /// Get just the simple name (without namespace) + pub fn simple_name(&self) -> Option<&str> { + self.qualified_name().map(|n| n.name()) + } + /// Get type arguments for generic types pub fn type_args(&self) -> Option<&[Type]> { match self { @@ -59,35 +191,61 @@ impl Type { } } + /// Create an instance type from a qualified name string + pub fn instance(name: &str) -> Self { + Type::Instance { + name: QualifiedName::new(name), + } + } + + /// Create a singleton type from a qualified name string + pub fn singleton(name: &str) -> Self { + Type::Singleton { + name: QualifiedName::new(name), + } + } + /// Convenience constructors pub fn string() -> Self { Type::Instance { - class_name: "String".to_string(), + name: QualifiedName::simple("String"), } } pub fn integer() -> Self { Type::Instance { - class_name: "Integer".to_string(), + name: QualifiedName::simple("Integer"), + } + } + + pub fn float() -> Self { + Type::Instance { + name: QualifiedName::simple("Float"), + } + } + + pub fn symbol() -> Self { + Type::Instance { + name: QualifiedName::simple("Symbol"), } } pub fn array() -> Self { Type::Instance { - class_name: "Array".to_string(), + name: QualifiedName::simple("Array"), } } pub fn hash() -> Self { Type::Instance { - class_name: "Hash".to_string(), + name: QualifiedName::simple("Hash"), } } /// Create a generic Array type: Array[element_type] pub fn array_of(element_type: Type) -> Self { Type::Generic { - class_name: "Array".to_string(), + name: QualifiedName::simple("Array"), type_args: vec![element_type], } } @@ -95,7 +253,7 @@ impl Type { /// Create a generic Hash type: Hash[key_type, value_type] pub fn hash_of(key_type: Type, value_type: Type) -> Self { Type::Generic { - class_name: "Hash".to_string(), + name: QualifiedName::simple("Hash"), type_args: vec![key_type, value_type], } } @@ -105,6 +263,71 @@ impl Type { mod tests { use super::*; + // QualifiedName tests + #[test] + fn test_qualified_name_simple() { + let name = QualifiedName::simple("User"); + assert_eq!(name.name(), "User"); + assert_eq!(name.full_name(), "User"); + assert_eq!(name.depth(), 1); + assert!(name.is_simple()); + assert!(name.parent().is_none()); + } + + #[test] + fn test_qualified_name_nested() { + let name = QualifiedName::new("Api::V1::User"); + assert_eq!(name.name(), "User"); + assert_eq!(name.full_name(), "Api::V1::User"); + assert_eq!(name.depth(), 3); + assert!(!name.is_simple()); + + // Test segments + assert_eq!(name.segment(0), Some("Api")); + assert_eq!(name.segment(1), Some("V1")); + assert_eq!(name.segment(2), Some("User")); + assert_eq!(name.segment(3), None); + } + + #[test] + fn test_qualified_name_parent() { + let name = QualifiedName::new("Api::V1::User"); + let parent = name.parent().unwrap(); + assert_eq!(parent.full_name(), "Api::V1"); + assert_eq!(parent.name(), "V1"); + + let grandparent = parent.parent().unwrap(); + assert_eq!(grandparent.full_name(), "Api"); + assert!(grandparent.parent().is_none()); + } + + #[test] + fn test_qualified_name_child() { + let name = QualifiedName::simple("Api"); + let child = name.child("V1"); + assert_eq!(child.full_name(), "Api::V1"); + + let grandchild = child.child("User"); + assert_eq!(grandchild.full_name(), "Api::V1::User"); + assert_eq!(grandchild.depth(), 3); + } + + #[test] + fn test_qualified_name_display() { + let name = QualifiedName::new("Api::V1::User"); + assert_eq!(format!("{}", name), "Api::V1::User"); + } + + #[test] + fn test_qualified_name_from() { + let name: QualifiedName = "Api::User".into(); + assert_eq!(name.full_name(), "Api::User"); + + let name2: QualifiedName = String::from("Module::Class").into(); + assert_eq!(name2.full_name(), "Module::Class"); + } + + // Type tests #[test] fn test_type_show() { assert_eq!(Type::string().show(), "String"); @@ -113,6 +336,14 @@ mod tests { assert_eq!(Type::Bot.show(), "untyped"); } + #[test] + fn test_type_instance_qualified() { + let user_type = Type::instance("Api::V1::User"); + assert_eq!(user_type.show(), "Api::V1::User"); + assert_eq!(user_type.base_class_name(), Some("Api::V1::User")); + assert_eq!(user_type.simple_name(), Some("User")); + } + #[test] fn test_type_union() { let union = Type::Union(vec![Type::string(), Type::integer()]); @@ -131,7 +362,10 @@ mod tests { #[test] fn test_base_class_name() { assert_eq!(Type::string().base_class_name(), Some("String")); - assert_eq!(Type::array_of(Type::integer()).base_class_name(), Some("Array")); + assert_eq!( + Type::array_of(Type::integer()).base_class_name(), + Some("Array") + ); assert_eq!(Type::Nil.base_class_name(), None); assert_eq!(Type::Bot.base_class_name(), None); } @@ -146,4 +380,11 @@ mod tests { // Non-generic types have no type args assert!(Type::string().type_args().is_none()); } + + #[test] + fn test_singleton_type() { + let singleton = Type::singleton("Api::User"); + assert_eq!(singleton.show(), "singleton(Api::User)"); + assert_eq!(singleton.base_class_name(), Some("Api::User")); + } }