Skip to content

Commit 7138eb5

Browse files
dak2claude
andcommitted
Add inheritance chain method resolution for user-defined classes
Previously, calling a method defined in a parent class on a child class instance (e.g., Dog.new.speak where speak is defined in Animal) produced a false positive "undefined method" error. This is because the method resolution order (MRO) did not walk the superclass chain for user-defined classes. This commit introduces a superclass_map in GlobalEnv that records parent-child relationships at class definition time, and extends the MRO fallback chain to walk the superclass hierarchy including each ancestor's included modules. Circular inheritance is safely handled via a visited set. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 53b39e9 commit 7138eb5

5 files changed

Lines changed: 443 additions & 35 deletions

File tree

core/src/analyzer/definitions.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ pub(crate) fn process_class_node(
2727
) -> Option<VertexId> {
2828
let class_name = extract_class_name(class_node);
2929
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+
3041
install_class(genv, class_name, superclass.as_deref());
3142

3243
if let Some(body) = class_node.body() {

core/src/analyzer/dispatch.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,82 @@ User.new.foo
14101410

14111411
// === Safe navigation operator (`&.`) tests ===
14121412

1413+
// === Inheritance chain tests ===
1414+
1415+
#[test]
1416+
fn test_inheritance_basic() {
1417+
let source = r#"
1418+
class Animal
1419+
def speak
1420+
"..."
1421+
end
1422+
end
1423+
1424+
class Dog < Animal
1425+
end
1426+
1427+
Dog.new.speak
1428+
"#;
1429+
let genv = analyze(source);
1430+
assert!(
1431+
genv.type_errors.is_empty(),
1432+
"Dog.new.speak should resolve via Animal#speak: {:?}",
1433+
genv.type_errors
1434+
);
1435+
}
1436+
1437+
#[test]
1438+
fn test_inheritance_multi_level() {
1439+
let source = r#"
1440+
class Animal
1441+
def speak
1442+
"..."
1443+
end
1444+
end
1445+
1446+
class Dog < Animal
1447+
end
1448+
1449+
class Puppy < Dog
1450+
end
1451+
1452+
Puppy.new.speak
1453+
"#;
1454+
let genv = analyze(source);
1455+
assert!(
1456+
genv.type_errors.is_empty(),
1457+
"Puppy.new.speak should resolve via Animal#speak: {:?}",
1458+
genv.type_errors
1459+
);
1460+
}
1461+
1462+
#[test]
1463+
fn test_inheritance_override() {
1464+
let source = r#"
1465+
class Animal
1466+
def speak
1467+
"generic"
1468+
end
1469+
end
1470+
1471+
class Dog < Animal
1472+
def speak
1473+
42
1474+
end
1475+
end
1476+
1477+
Dog.new.speak
1478+
"#;
1479+
let genv = analyze(source);
1480+
assert!(genv.type_errors.is_empty());
1481+
1482+
let info = genv
1483+
.resolve_method(&Type::instance("Dog"), "speak")
1484+
.expect("Dog#speak should be resolved");
1485+
let ret_vtx = info.return_vertex.unwrap();
1486+
assert_eq!(get_type_show(&genv, ret_vtx), "Integer");
1487+
}
1488+
14131489
#[test]
14141490
fn test_safe_navigation_basic() {
14151491
let source = r#"

core/src/env/global_env.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use std::collections::HashMap;
77

88
use crate::env::box_manager::BoxManager;
9-
use crate::env::method_registry::{MethodInfo, MethodRegistry};
9+
use crate::env::method_registry::{MethodInfo, MethodRegistry, ResolutionContext};
1010
use crate::env::scope::{Scope, ScopeId, ScopeKind, ScopeManager};
1111
use crate::env::type_error::TypeError;
1212
use crate::env::vertex_manager::VertexManager;
@@ -41,6 +41,8 @@ pub struct GlobalEnv {
4141
/// Module inclusions: class_name → Vec<module_name> (in include order)
4242
module_inclusions: HashMap<String, Vec<String>>,
4343

44+
/// Superclass map: child_class → parent_class
45+
superclass_map: HashMap<String, String>,
4446
}
4547

4648
impl GlobalEnv {
@@ -52,6 +54,7 @@ impl GlobalEnv {
5254
type_errors: Vec::new(),
5355
scope_manager: ScopeManager::new(),
5456
module_inclusions: HashMap::new(),
57+
superclass_map: HashMap::new(),
5558
}
5659
}
5760

@@ -163,8 +166,12 @@ impl GlobalEnv {
163166

164167
/// Resolve method
165168
pub fn resolve_method(&self, recv_ty: &Type, method_name: &str) -> Option<&MethodInfo> {
169+
let ctx = ResolutionContext {
170+
inclusions: &self.module_inclusions,
171+
superclass_map: &self.superclass_map,
172+
};
166173
self.method_registry
167-
.resolve(recv_ty, method_name, &self.module_inclusions)
174+
.resolve(recv_ty, method_name, &ctx)
168175
}
169176

170177
/// Record that a class includes a module
@@ -247,6 +254,30 @@ impl GlobalEnv {
247254
});
248255
self.scope_manager.enter_scope(scope_id);
249256
self.register_constant_in_parent(scope_id, &name);
257+
258+
// Record superclass relationship in superclass_map
259+
if let Some(parent) = superclass {
260+
let child_name = self.scope_manager.current_qualified_name()
261+
.unwrap_or_else(|| name.clone());
262+
// NOTE: lookup_constant may fail for cross-namespace inheritance
263+
// (e.g., `class Dog < Animal` inside `module Service` where Animal is `Api::Animal`).
264+
// In that case, the raw name is used. This is a known limitation (see design doc Q2).
265+
let parent_name = self.scope_manager.lookup_constant(parent)
266+
.unwrap_or_else(|| parent.to_string());
267+
268+
// Detect superclass mismatch (Ruby raises TypeError for this at runtime)
269+
if let Some(existing) = self.superclass_map.get(&child_name) {
270+
if *existing != parent_name {
271+
eprintln!(
272+
"[methodray] warning: superclass mismatch for {}: previously {}, now {}",
273+
child_name, existing, parent_name
274+
);
275+
}
276+
}
277+
278+
self.superclass_map.insert(child_name, parent_name);
279+
}
280+
250281
scope_id
251282
}
252283

0 commit comments

Comments
 (0)