diff --git a/lib/methodray/methodray-cli b/lib/methodray/methodray-cli index e301d4b..5d36455 100755 Binary files a/lib/methodray/methodray-cli and b/lib/methodray/methodray-cli differ diff --git a/rust/src/analyzer/attr_methods.rs b/rust/src/analyzer/attr_methods.rs new file mode 100644 index 0000000..9a8318a --- /dev/null +++ b/rust/src/analyzer/attr_methods.rs @@ -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 { + 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, + } +} diff --git a/rust/src/analyzer/install.rs b/rust/src/analyzer/install.rs index 72ac188..b4edb14 100644 --- a/rust/src/analyzer/install.rs +++ b/rust/src/analyzer/install.rs @@ -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, @@ -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), diff --git a/rust/src/analyzer/mod.rs b/rust/src/analyzer/mod.rs index b65e1ff..dd6a45e 100644 --- a/rust/src/analyzer/mod.rs +++ b/rust/src/analyzer/mod.rs @@ -1,3 +1,4 @@ +mod attr_methods; mod calls; mod definitions; mod dispatch; diff --git a/rust/src/analyzer/tests/integration_test.rs b/rust/src/analyzer/tests/integration_test.rs index d4874a5..c6a9140 100644 --- a/rust/src/analyzer/tests/integration_test.rs +++ b/rust/src/analyzer/tests/integration_test.rs @@ -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"); +}