Skip to content
Open
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
Binary file modified lib/methodray/methodray-cli
Binary file not shown.
116 changes: 116 additions & 0 deletions rust/src/analyzer/attr_methods.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//! attr_reader, attr_writer, attr_accessor support
//!
//! Handles Ruby attribute methods that generate getter/setter methods.

use crate::env::GlobalEnv;
use crate::types::Type;
use ruby_prism::CallNode;

/// Process attr_reader call
/// attr_reader :name, :age generates:
/// - def name; @name; end
/// - def age; @age; end
pub fn process_attr_reader(genv: &mut GlobalEnv, call_node: &CallNode) {
let class_name: String = match genv.scope_manager.current_class_name() {
Some(name) => name,
None => return, // attr_reader outside of class is ignored
};

let attr_names = extract_symbol_arguments(call_node);

for attr_name in attr_names {
let ivar_name = format!("@{}", attr_name);

// Get instance variable type from class scope, or default to Bot (untyped)
let return_type = genv
.scope_manager
.lookup_instance_var(&ivar_name)
.and_then(|vtx| {
genv.get_vertex(vtx).and_then(|v| {
// Get first type from vertex's types HashMap
v.types.keys().next().cloned()
})
})
.unwrap_or(Type::Bot);

// Register getter method: def name; @name; end
let recv_ty = Type::Instance {
class_name: class_name.clone(),
};
genv.register_builtin_method(recv_ty, &attr_name, return_type);
}
}

/// Process attr_writer call
/// attr_writer :name generates:
/// - def name=(value); @name = value; end
pub fn process_attr_writer(genv: &mut GlobalEnv, call_node: &CallNode) {
let class_name: String = match genv.scope_manager.current_class_name() {
Some(name) => name,
None => return,
};

let attr_names = extract_symbol_arguments(call_node);

for attr_name in attr_names {
let method_name = format!("{}=", attr_name);

// Writer method returns the assigned value (Bot for now)
let recv_ty = Type::Instance {
class_name: class_name.clone(),
};
genv.register_builtin_method(recv_ty, &method_name, Type::Bot);
}
}

/// Process attr_accessor call
/// attr_accessor :name is equivalent to attr_reader :name + attr_writer :name
pub fn process_attr_accessor(genv: &mut GlobalEnv, call_node: &CallNode) {
process_attr_reader(genv, call_node);
process_attr_writer(genv, call_node);
}

/// Extract symbol names from arguments
/// e.g., attr_reader :name, :age -> ["name", "age"]
fn extract_symbol_arguments(call_node: &CallNode) -> Vec<String> {
let mut names = Vec::new();

if let Some(arguments) = call_node.arguments() {
for arg in &arguments.arguments() {
if let Some(symbol_node) = arg.as_symbol_node() {
let unescaped = symbol_node.unescaped();
let name = String::from_utf8_lossy(&unescaped).to_string();
names.push(name);
}
}
}

names
}

/// Check if a CallNode is an attr method and process it
/// Returns true if it was an attr method call
pub fn try_process_attr_method(genv: &mut GlobalEnv, call_node: &CallNode) -> bool {
// Only handle receiver-less calls (attr_reader is called without explicit receiver)
if call_node.receiver().is_some() {
return false;
}

let method_name = String::from_utf8_lossy(call_node.name().as_slice()).to_string();

match method_name.as_str() {
"attr_reader" => {
process_attr_reader(genv, call_node);
true
}
"attr_writer" => {
process_attr_writer(genv, call_node);
true
}
"attr_accessor" => {
process_attr_accessor(genv, call_node);
true
}
_ => false,
}
}
8 changes: 8 additions & 0 deletions rust/src/analyzer/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::env::{GlobalEnv, LocalEnv};
use crate::graph::{ChangeSet, VertexId};
use ruby_prism::Node;

use super::attr_methods::try_process_attr_method;
use super::definitions::{exit_scope, extract_class_name, install_class, install_method};
use super::dispatch::{
dispatch_needs_child, dispatch_simple, finish_ivar_write, finish_local_var_write,
Expand Down Expand Up @@ -44,6 +45,13 @@ impl<'a> AstInstaller<'a> {
return self.install_def_node(&def_node);
}

// attr_reader, attr_writer, attr_accessor
if let Some(call_node) = node.as_call_node() {
if try_process_attr_method(self.genv, &call_node) {
return None;
}
}

// Try simple dispatch first (no child processing needed)
match dispatch_simple(self.genv, self.lenv, node) {
DispatchResult::Vertex(vtx) => return Some(vtx),
Expand Down
1 change: 1 addition & 0 deletions rust/src/analyzer/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod attr_methods;
mod calls;
mod definitions;
mod dispatch;
Expand Down
91 changes: 91 additions & 0 deletions rust/src/analyzer/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,94 @@ y = x.upcase.downcase
let y_vtx = lenv.get_var("y").unwrap();
assert_eq!(genv.get_vertex(y_vtx).unwrap().show(), "String");
}

#[test]
fn test_attr_reader_registers_method() {
let source = r#"
class User
attr_reader :name
end
"#;

let (genv, _lenv) = analyze(source);

// attr_reader should register a method on the User class
let recv_ty = Type::Instance {
class_name: "User".to_string(),
};
let result = genv.resolve_method(&recv_ty, "name");
assert!(result.is_some(), "attr_reader should register a 'name' method");
}

#[test]
fn test_attr_reader_with_ivar_type() {
// Note: Currently attr_reader registers with Bot type because
// type propagation happens after attr_reader processing.
// This is a known limitation - ideally we'd do two-pass processing
// or lazy evaluation to get the correct type.
let source = r#"
class User
def initialize
@name = "John"
end

attr_reader :name
end
"#;

let (genv, _lenv) = analyze(source);

// attr_reader should register a method (type may be untyped due to processing order)
let recv_ty = Type::Instance {
class_name: "User".to_string(),
};
let result = genv.resolve_method(&recv_ty, "name");
assert!(result.is_some(), "attr_reader should register 'name' method");
// Type is Bot (untyped) because type propagation hasn't run yet when attr_reader processes
// This is acceptable for now - the method is registered and callable
}

#[test]
fn test_attr_reader_error_detection() {
let source = r#"
class User
def initialize
@age = 25
end

attr_reader :age

def test
x = @age.upcase
end
end
"#;

let (genv, _lenv) = analyze(source);

// Type error should be detected: @age is Integer, not String
assert_eq!(genv.type_errors.len(), 1);
assert_eq!(genv.type_errors[0].method_name, "upcase");
}

#[test]
fn test_attr_accessor() {
let source = r#"
class User
attr_accessor :email
end
"#;

let (genv, _lenv) = analyze(source);

let recv_ty = Type::Instance {
class_name: "User".to_string(),
};

// attr_accessor should register both getter and setter
let getter = genv.resolve_method(&recv_ty, "email");
assert!(getter.is_some(), "attr_accessor should register getter");

let setter = genv.resolve_method(&recv_ty, "email=");
assert!(setter.is_some(), "attr_accessor should register setter");
}