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
11 changes: 11 additions & 0 deletions core/src/analyzer/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ pub(crate) fn process_class_node(
) -> Option<VertexId> {
let class_name = extract_class_name(class_node);
let superclass = class_node.superclass().and_then(|sup| extract_constant_path(&sup));

// Warn if superclass is a dynamic expression (not a constant path)
// TODO: Replace eprintln! with structured diagnostic (record_type_error or warning)
// so this is visible in LSP mode and includes source location.
if class_node.superclass().is_some() && superclass.is_none() {
eprintln!(
"[methodray] warning: dynamic superclass expression in class {}; inheritance will be ignored",
class_name
);
}

install_class(genv, class_name, superclass.as_deref());

if let Some(body) = class_node.body() {
Expand Down
76 changes: 76 additions & 0 deletions core/src/analyzer/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1410,6 +1410,82 @@ User.new.foo

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

// === Inheritance chain tests ===

#[test]
fn test_inheritance_basic() {
let source = r#"
class Animal
def speak
"..."
end
end

class Dog < Animal
end

Dog.new.speak
"#;
let genv = analyze(source);
assert!(
genv.type_errors.is_empty(),
"Dog.new.speak should resolve via Animal#speak: {:?}",
genv.type_errors
);
}

#[test]
fn test_inheritance_multi_level() {
let source = r#"
class Animal
def speak
"..."
end
end

class Dog < Animal
end

class Puppy < Dog
end

Puppy.new.speak
"#;
let genv = analyze(source);
assert!(
genv.type_errors.is_empty(),
"Puppy.new.speak should resolve via Animal#speak: {:?}",
genv.type_errors
);
}

#[test]
fn test_inheritance_override() {
let source = r#"
class Animal
def speak
"generic"
end
end

class Dog < Animal
def speak
42
end
end

Dog.new.speak
"#;
let genv = analyze(source);
assert!(genv.type_errors.is_empty());

let info = genv
.resolve_method(&Type::instance("Dog"), "speak")
.expect("Dog#speak should be resolved");
let ret_vtx = info.return_vertex.unwrap();
assert_eq!(get_type_show(&genv, ret_vtx), "Integer");
}

#[test]
fn test_safe_navigation_basic() {
let source = r#"
Expand Down
35 changes: 33 additions & 2 deletions core/src/env/global_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use std::collections::HashMap;

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

/// Superclass map: child_class → parent_class
superclass_map: HashMap<String, String>,
}

impl GlobalEnv {
Expand All @@ -52,6 +54,7 @@ impl GlobalEnv {
type_errors: Vec::new(),
scope_manager: ScopeManager::new(),
module_inclusions: HashMap::new(),
superclass_map: HashMap::new(),
}
}

Expand Down Expand Up @@ -163,8 +166,12 @@ impl GlobalEnv {

/// Resolve method
pub fn resolve_method(&self, recv_ty: &Type, method_name: &str) -> Option<&MethodInfo> {
let ctx = ResolutionContext {
inclusions: &self.module_inclusions,
superclass_map: &self.superclass_map,
};
self.method_registry
.resolve(recv_ty, method_name, &self.module_inclusions)
.resolve(recv_ty, method_name, &ctx)
}

/// Record that a class includes a module
Expand Down Expand Up @@ -247,6 +254,30 @@ impl GlobalEnv {
});
self.scope_manager.enter_scope(scope_id);
self.register_constant_in_parent(scope_id, &name);

// Record superclass relationship in superclass_map
if let Some(parent) = superclass {
let child_name = self.scope_manager.current_qualified_name()
.unwrap_or_else(|| name.clone());
// NOTE: lookup_constant may fail for cross-namespace inheritance
// (e.g., `class Dog < Animal` inside `module Service` where Animal is `Api::Animal`).
// In that case, the raw name is used. This is a known limitation (see design doc Q2).
let parent_name = self.scope_manager.lookup_constant(parent)
.unwrap_or_else(|| parent.to_string());

// Detect superclass mismatch (Ruby raises TypeError for this at runtime)
if let Some(existing) = self.superclass_map.get(&child_name) {
if *existing != parent_name {
eprintln!(
"[methodray] warning: superclass mismatch for {}: previously {}, now {}",
child_name, existing, parent_name
);
}
}

self.superclass_map.insert(child_name, parent_name);
}

scope_id
}

Expand Down
Loading