diff --git a/core/src/analyzer/definitions.rs b/core/src/analyzer/definitions.rs index 4a464b7..9b48b56 100644 --- a/core/src/analyzer/definitions.rs +++ b/core/src/analyzer/definitions.rs @@ -26,7 +26,8 @@ pub(crate) fn process_class_node( class_node: &ruby_prism::ClassNode, ) -> Option { let class_name = extract_class_name(class_node); - install_class(genv, class_name); + let superclass = class_node.superclass().and_then(|sup| extract_constant_path(&sup)); + install_class(genv, class_name, superclass.as_deref()); if let Some(body) = class_node.body() { if let Some(statements) = body.as_statements_node() { @@ -126,8 +127,8 @@ pub(crate) fn process_def_node( } /// Install class definition -fn install_class(genv: &mut GlobalEnv, class_name: String) { - genv.enter_class(class_name); +fn install_class(genv: &mut GlobalEnv, class_name: String, superclass: Option<&str>) { + genv.enter_class(class_name, superclass); } /// Install module definition @@ -203,7 +204,7 @@ mod tests { fn test_enter_exit_class_scope() { let mut genv = GlobalEnv::new(); - install_class(&mut genv, "User".to_string()); + install_class(&mut genv, "User".to_string(), None); assert_eq!( genv.scope_manager.current_class_name(), Some("User".to_string()) @@ -231,7 +232,7 @@ mod tests { fn test_nested_method_scope() { let mut genv = GlobalEnv::new(); - install_class(&mut genv, "User".to_string()); + install_class(&mut genv, "User".to_string(), None); install_method(&mut genv, "greet".to_string()); // Still in User class context diff --git a/core/src/analyzer/dispatch.rs b/core/src/analyzer/dispatch.rs index 3163e03..3a56ca8 100644 --- a/core/src/analyzer/dispatch.rs +++ b/core/src/analyzer/dispatch.rs @@ -18,6 +18,45 @@ use super::variables::{ install_self, }; +/// Collect positional and keyword arguments from AST argument nodes. +/// +/// Shared by method calls (`dispatch.rs`) and super calls (`super_calls.rs`). +pub(crate) fn collect_arguments<'a>( + genv: &mut GlobalEnv, + lenv: &mut LocalEnv, + changes: &mut ChangeSet, + source: &str, + args: impl Iterator>, +) -> (Vec, Option>) { + let mut positional: Vec = Vec::new(); + let mut keyword: HashMap = HashMap::new(); + + for arg in args { + if let Some(kw_hash) = arg.as_keyword_hash_node() { + for element in kw_hash.elements().iter() { + let assoc = match element.as_assoc_node() { + Some(a) => a, + None => continue, + }; + let name = match assoc.key().as_symbol_node() { + Some(sym) => bytes_to_name(sym.unescaped()), + None => continue, + }; + if let Some(vtx) = + super::install::install_node(genv, lenv, changes, source, &assoc.value()) + { + keyword.insert(name, vtx); + } + } + } else if let Some(vtx) = super::install::install_node(genv, lenv, changes, source, &arg) { + positional.push(vtx); + } + } + + let kw = (!keyword.is_empty()).then_some(keyword); + (positional, kw) +} + /// Kind of attr_* declaration #[derive(Debug, Clone, Copy)] pub enum AttrKind { @@ -343,31 +382,8 @@ fn process_method_call_common<'a>( return Some(super::operators::process_not_operator(genv)); } - // Separate positional arguments and keyword arguments - let mut positional_arg_vtxs: Vec = Vec::new(); - let mut keyword_arg_vtxs: HashMap = HashMap::new(); - - for arg in &arguments { - if let Some(kw_hash) = arg.as_keyword_hash_node() { - for element in kw_hash.elements().iter() { - let assoc = match element.as_assoc_node() { - Some(a) => a, - None => continue, - }; - let name = match assoc.key().as_symbol_node() { - Some(sym) => bytes_to_name(sym.unescaped()), - None => continue, - }; - if let Some(vtx) = - super::install::install_node(genv, lenv, changes, source, &assoc.value()) - { - keyword_arg_vtxs.insert(name, vtx); - } - } - } else if let Some(vtx) = super::install::install_node(genv, lenv, changes, source, arg) { - positional_arg_vtxs.push(vtx); - } - } + let (positional_arg_vtxs, kwarg_vtxs) = + collect_arguments(genv, lenv, changes, source, arguments.into_iter()); if let Some(block_node) = block { if let Some(block) = block_node.as_block_node() { @@ -388,12 +404,6 @@ fn process_method_call_common<'a>( } } - let kwarg_vtxs = if keyword_arg_vtxs.is_empty() { - None - } else { - Some(keyword_arg_vtxs) - }; - Some(finish_method_call( genv, recv_vtx, diff --git a/core/src/analyzer/install.rs b/core/src/analyzer/install.rs index cb1d42d..11b9cfa 100644 --- a/core/src/analyzer/install.rs +++ b/core/src/analyzer/install.rs @@ -19,6 +19,7 @@ use super::loops::{process_for_node, process_until_node, process_while_node}; use super::operators::{process_and_node, process_or_node}; use super::parentheses::process_parentheses_node; use super::returns::process_return_node; +use super::super_calls; /// Build graph from AST (public API wrapper) pub struct AstInstaller<'a> { @@ -89,6 +90,17 @@ pub(crate) fn install_node( return process_rescue_modifier_node(genv, lenv, changes, source, &rescue_modifier); } + // SuperNode: super(args) — explicit arguments + if let Some(super_node) = node.as_super_node() { + return super_calls::process_super_node(genv, lenv, changes, source, &super_node); + } + // ForwardingSuperNode: super — implicit argument forwarding + if let Some(fwd_super_node) = node.as_forwarding_super_node() { + return super_calls::process_forwarding_super_node( + genv, lenv, changes, source, &fwd_super_node, + ); + } + if let Some(while_node) = node.as_while_node() { return process_while_node(genv, lenv, changes, source, &while_node); } diff --git a/core/src/analyzer/mod.rs b/core/src/analyzer/mod.rs index f1ef76d..f96356a 100644 --- a/core/src/analyzer/mod.rs +++ b/core/src/analyzer/mod.rs @@ -13,6 +13,7 @@ mod operators; mod parameters; mod parentheses; mod returns; +mod super_calls; mod variables; pub use install::AstInstaller; diff --git a/core/src/analyzer/super_calls.rs b/core/src/analyzer/super_calls.rs new file mode 100644 index 0000000..11b1aba --- /dev/null +++ b/core/src/analyzer/super_calls.rs @@ -0,0 +1,285 @@ +//! Super call handling: `super` and `super(args)` +//! +//! Ruby's `super` calls the same-named method on the parent class. +//! - `super(args)` → SuperNode: explicit arguments +//! - `super` (bare) → ForwardingSuperNode: implicit argument forwarding +//! +//! Note: ForwardingSuperNode (bare `super`) is treated as a zero-argument +//! call. In Ruby, bare `super` forwards all arguments from the enclosing +//! method, but replicating this requires parameter-vertex forwarding that +//! is not yet implemented. Return type inference is unaffected. + +use ruby_prism::{ForwardingSuperNode, SuperNode}; + +use crate::env::{GlobalEnv, LocalEnv}; +use crate::graph::{ChangeSet, VertexId}; +use crate::source_map::SourceLocation as SL; +use crate::types::Type; + +/// Process SuperNode: `super(args)` — explicit arguments +pub(crate) fn process_super_node( + genv: &mut GlobalEnv, + lenv: &mut LocalEnv, + changes: &mut ChangeSet, + source: &str, + super_node: &SuperNode, +) -> Option { + let location = SL::from_prism_location_with_source(&super_node.location(), source); + process_super_call(genv, lenv, changes, source, super_node.arguments(), location) +} + +/// Process ForwardingSuperNode: `super` — implicit argument forwarding +pub(crate) fn process_forwarding_super_node( + genv: &mut GlobalEnv, + lenv: &mut LocalEnv, + changes: &mut ChangeSet, + source: &str, + node: &ForwardingSuperNode, +) -> Option { + let location = SL::from_prism_location_with_source(&node.location(), source); + process_super_call(genv, lenv, changes, source, None, location) +} + +/// Resolve a super call by looking up the same-named method on the superclass. +/// +/// Returns `None` if there is no enclosing method scope (super outside a method) +/// or no explicit superclass declared on the enclosing class. +fn process_super_call( + genv: &mut GlobalEnv, + lenv: &mut LocalEnv, + changes: &mut ChangeSet, + source: &str, + arguments: Option, + location: SL, +) -> Option { + let method_name = genv.scope_manager.current_method_name()?; + let superclass_name = genv.scope_manager.current_superclass()?; + let recv_vtx = genv.new_source(Type::instance(&superclass_name)); + + let (arg_vtxs, kw) = if let Some(args) = arguments { + super::dispatch::collect_arguments(genv, lenv, changes, source, args.arguments().iter()) + } else { + (vec![], None) + }; + + Some(super::calls::install_method_call( + genv, + recv_vtx, + method_name, + arg_vtxs, + kw, + Some(location), + )) +} + +#[cfg(test)] +mod tests { + use crate::analyzer::install::AstInstaller; + use crate::env::{GlobalEnv, LocalEnv}; + use crate::graph::VertexId; + use crate::parser::ParseSession; + use crate::types::Type; + + /// Helper: parse Ruby source, process with AstInstaller, and return GlobalEnv + fn analyze(source: &str) -> GlobalEnv { + let session = ParseSession::new(); + let parse_result = session.parse_source(source, "test.rb").unwrap(); + let root = parse_result.node(); + let program = root.as_program_node().unwrap(); + + let mut genv = GlobalEnv::new(); + let mut lenv = LocalEnv::new(); + + let mut installer = AstInstaller::new(&mut genv, &mut lenv, source); + for stmt in &program.statements().body() { + installer.install_node(&stmt); + } + installer.finish(); + + genv + } + + /// Helper: get the type string for a vertex ID + fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String { + if let Some(vertex) = genv.get_vertex(vtx) { + vertex.show() + } else if let Some(source) = genv.get_source(vtx) { + source.ty.show() + } else { + panic!("vertex {:?} not found as either Vertex or Source", vtx); + } + } + + #[test] + fn test_super_basic() { + let source = r#" +class Animal + def speak + "..." + end +end + +class Dog < Animal + def speak + super + end +end +"#; + let genv = analyze(source); + let info = genv + .resolve_method(&Type::instance("Dog"), "speak") + .expect("Dog#speak should be registered"); + let ret_vtx = info.return_vertex.unwrap(); + assert_eq!(get_type_show(&genv, ret_vtx), "String"); + } + + #[test] + fn test_super_with_method_chain() { + let source = r#" +class Animal + def speak + "hello" + end +end + +class Dog < Animal + def speak + super.upcase + end +end +"#; + let genv = analyze(source); + let info = genv + .resolve_method(&Type::instance("Dog"), "speak") + .expect("Dog#speak should be registered"); + assert!(info.return_vertex.is_some()); + } + + #[test] + fn test_super_with_arguments() { + let source = r#" +class Base + def greet(name) + name + end +end + +class Child < Base + def greet(name) + super(name) + end +end + +Child.new.greet("Alice") +"#; + let genv = analyze(source); + let info = genv + .resolve_method(&Type::instance("Child"), "greet") + .expect("Child#greet should be registered"); + let ret_vtx = info.return_vertex.unwrap(); + assert_eq!(get_type_show(&genv, ret_vtx), "String"); + } + + #[test] + fn test_super_outside_method_ignored() { + let source = r#" +class Foo < Object + super +end +"#; + analyze(source); + } + + #[test] + fn test_super_explicit_empty_args() { + let source = r#" +class Animal + def speak + "hello" + end +end + +class Dog < Animal + def speak + super() + end +end +"#; + let genv = analyze(source); + let info = genv + .resolve_method(&Type::instance("Dog"), "speak") + .expect("Dog#speak should be registered"); + let ret_vtx = info.return_vertex.unwrap(); + assert_eq!(get_type_show(&genv, ret_vtx), "String"); + } + + #[test] + fn test_super_without_superclass_ignored() { + let source = r#" +class Foo + def bar + super + end +end +"#; + let genv = analyze(source); + let info = genv + .resolve_method(&Type::instance("Foo"), "bar") + .expect("Foo#bar should be registered"); + assert!(info.return_vertex.is_some()); + } + + #[test] + fn test_super_qualified_superclass() { + let source = r#" +module Animals + class Pet + def name + "pet" + end + end +end + +class Dog < Animals::Pet + def name + super + end +end +"#; + let genv = analyze(source); + let info = genv + .resolve_method(&Type::instance("Dog"), "name") + .expect("Dog#name should be registered"); + let ret_vtx = info.return_vertex.unwrap(); + assert_eq!(get_type_show(&genv, ret_vtx), "String"); + } + + #[test] + fn test_super_multi_level_inheritance() { + let source = r#" +class A + def foo + "hello" + end +end + +class B < A + def foo + super + end +end + +class C < B + def foo + super + end +end +"#; + let genv = analyze(source); + let info = genv + .resolve_method(&Type::instance("C"), "foo") + .expect("C#foo should be registered"); + let ret_vtx = info.return_vertex.unwrap(); + assert_eq!(get_type_show(&genv, ret_vtx), "String"); + } +} diff --git a/core/src/env/global_env.rs b/core/src/env/global_env.rs index 2945c73..a822201 100644 --- a/core/src/env/global_env.rs +++ b/core/src/env/global_env.rs @@ -40,6 +40,7 @@ pub struct GlobalEnv { /// Module inclusions: class_name → Vec (in include order) module_inclusions: HashMap>, + } impl GlobalEnv { @@ -238,11 +239,11 @@ impl GlobalEnv { } } - /// Enter a class scope - pub fn enter_class(&mut self, name: String) -> ScopeId { + /// Enter a class scope with optional superclass + pub fn enter_class(&mut self, name: String, superclass: Option<&str>) -> ScopeId { let scope_id = self.scope_manager.new_scope(ScopeKind::Class { name: name.clone(), - superclass: None, + superclass: superclass.map(|s| s.to_string()), }); self.scope_manager.enter_scope(scope_id); self.register_constant_in_parent(scope_id, &name); diff --git a/core/src/env/scope.rs b/core/src/env/scope.rs index 56c7a05..ee2d0be 100644 --- a/core/src/env/scope.rs +++ b/core/src/env/scope.rs @@ -245,6 +245,28 @@ impl ScopeManager { Some(path_segments.join("::")) } + /// Get current method name from nearest enclosing method scope + pub fn current_method_name(&self) -> Option { + self.walk_scopes().find_map(|scope| { + if let ScopeKind::Method { name, .. } = &scope.kind { + Some(name.clone()) + } else { + None + } + }) + } + + /// Get superclass name from nearest enclosing class scope + pub fn current_superclass(&self) -> Option { + self.walk_scopes().find_map(|scope| { + if let ScopeKind::Class { superclass, .. } = &scope.kind { + superclass.clone() + } else { + None + } + }) + } + /// Get return_vertex from the nearest enclosing method scope pub fn current_method_return_vertex(&self) -> Option { self.walk_scopes().find_map(|scope| { @@ -593,4 +615,60 @@ mod tests { // Inside Admin scope, User should resolve to Admin::User assert_eq!(sm.lookup_constant("User"), Some("Admin::User".to_string())); } + + #[test] + fn test_current_method_name() { + let mut sm = ScopeManager::new(); + let class_id = sm.new_scope(ScopeKind::Class { + name: "User".to_string(), + superclass: None, + }); + sm.enter_scope(class_id); + let method_id = sm.new_scope(ScopeKind::Method { + name: "greet".to_string(), + receiver_type: None, + return_vertex: None, + }); + sm.enter_scope(method_id); + assert_eq!(sm.current_method_name(), Some("greet".to_string())); + + sm.exit_scope(); + assert_eq!(sm.current_method_name(), None); + } + + #[test] + fn test_current_superclass() { + let mut sm = ScopeManager::new(); + let class_id = sm.new_scope(ScopeKind::Class { + name: "Dog".to_string(), + superclass: Some("Animal".to_string()), + }); + sm.enter_scope(class_id); + assert_eq!(sm.current_superclass(), Some("Animal".to_string())); + + sm.exit_scope(); + assert_eq!(sm.current_superclass(), None); + } + + #[test] + fn test_current_method_name_from_nested_block() { + let mut sm = ScopeManager::new(); + let class_id = sm.new_scope(ScopeKind::Class { + name: "User".to_string(), + superclass: Some("Base".to_string()), + }); + sm.enter_scope(class_id); + let method_id = sm.new_scope(ScopeKind::Method { + name: "process".to_string(), + receiver_type: None, + return_vertex: None, + }); + sm.enter_scope(method_id); + let block_id = sm.new_scope(ScopeKind::Block); + sm.enter_scope(block_id); + + // Inside a block, should still find enclosing method/superclass + assert_eq!(sm.current_method_name(), Some("process".to_string())); + assert_eq!(sm.current_superclass(), Some("Base".to_string())); + } } diff --git a/test/super_test.rb b/test/super_test.rb new file mode 100644 index 0000000..59bafed --- /dev/null +++ b/test/super_test.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'test_helper' + +class SuperTest < Minitest::Test + include CLITestHelper + + # ============================================ + # No Error + # ============================================ + + def test_super_basic_no_error + source = <<~RUBY + class Animal + def speak + "..." + end + end + + class Dog < Animal + def speak + super + end + end + + Dog.new.speak + RUBY + + assert_no_check_errors(source) + end + + def test_super_with_method_chain + source = <<~RUBY + class Animal + def speak + "hello" + end + end + + class Dog < Animal + def speak + super.upcase + end + end + + Dog.new.speak + RUBY + + assert_no_check_errors(source) + end + + def test_super_with_arguments + source = <<~RUBY + class Base + def greet(name) + name + end + end + + class Child < Base + def greet(name) + super(name) + end + end + + Child.new.greet("Alice") + RUBY + + assert_no_check_errors(source) + end + + def test_super_multi_level_inheritance + source = <<~RUBY + class A + def foo + "hello" + end + end + + class B < A + def foo + super + end + end + + class C < B + def foo + super + end + end + + C.new.foo + RUBY + + assert_no_check_errors(source) + end + + # ============================================ + # Error Detection + # ============================================ + + def test_super_method_chain_type_error + source = <<~RUBY + class Animal + def speak + "hello" + end + end + + class Dog < Animal + def speak + super.even? + end + end + + Dog.new.speak + RUBY + + assert_check_error(source, method_name: 'even?', receiver_type: 'String') + end +end