diff --git a/Cargo.lock b/Cargo.lock index 6e702a92f5b7..98a2fdb70582 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2858,6 +2858,7 @@ dependencies = [ "tree-sitter-javascript", "tree-sitter-kotlin", "tree-sitter-python", + "tree-sitter-ruby", "tree-sitter-rust", "umya-spreadsheet", "url", @@ -7085,6 +7086,16 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-ruby" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0031f687c0772f2dad7b77104c43428611099a1804c81244ada21560f41f0b1" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-rust" version = "0.21.2" diff --git a/crates/goose-mcp/Cargo.toml b/crates/goose-mcp/Cargo.toml index e1878329016d..52781aaeb23f 100644 --- a/crates/goose-mcp/Cargo.toml +++ b/crates/goose-mcp/Cargo.toml @@ -69,6 +69,7 @@ tree-sitter-go = "0.21" tree-sitter-java = "0.21" tree-sitter-kotlin = "0.3.8" devgen-tree-sitter-swift = "0.21.0" +tree-sitter-ruby = "0.21.0" streaming-iterator = "0.1" rayon = "1.10" libc = "0.2" diff --git a/crates/goose-mcp/src/developer/analyze/languages/mod.rs b/crates/goose-mcp/src/developer/analyze/languages/mod.rs index c5303ea9cdd8..bfa3a7fe8c47 100644 --- a/crates/goose-mcp/src/developer/analyze/languages/mod.rs +++ b/crates/goose-mcp/src/developer/analyze/languages/mod.rs @@ -3,6 +3,7 @@ pub mod java; pub mod javascript; pub mod kotlin; pub mod python; +pub mod ruby; pub mod rust; pub mod swift; @@ -16,6 +17,7 @@ pub fn get_element_query(language: &str) -> &'static str { "java" => java::ELEMENT_QUERY, "kotlin" => kotlin::ELEMENT_QUERY, "swift" => swift::ELEMENT_QUERY, + "ruby" => ruby::ELEMENT_QUERY, _ => "", } } @@ -30,6 +32,7 @@ pub fn get_call_query(language: &str) -> &'static str { "java" => java::CALL_QUERY, "kotlin" => kotlin::CALL_QUERY, "swift" => swift::CALL_QUERY, + "ruby" => ruby::CALL_QUERY, _ => "", } } diff --git a/crates/goose-mcp/src/developer/analyze/languages/ruby.rs b/crates/goose-mcp/src/developer/analyze/languages/ruby.rs new file mode 100644 index 000000000000..2cd1233a4681 --- /dev/null +++ b/crates/goose-mcp/src/developer/analyze/languages/ruby.rs @@ -0,0 +1,42 @@ +/// Tree-sitter query for extracting Ruby code elements. +/// +/// This query captures: +/// - Method definitions (def) +/// - Class and module definitions +/// - Common attr_* declarations (attr_accessor, attr_reader, attr_writer) +/// - Import statements (require, require_relative, load) +pub const ELEMENT_QUERY: &str = r#" + ; Method definitions + (method name: (identifier) @func) + + ; Class and module definitions + (class name: (constant) @class) + (module name: (constant) @class) + + ; Attr declarations as functions + (call method: (identifier) @func (#eq? @func "attr_accessor")) + (call method: (identifier) @func (#eq? @func "attr_reader")) + (call method: (identifier) @func (#eq? @func "attr_writer")) + + ; Require statements + (call method: (identifier) @import (#eq? @import "require")) + (call method: (identifier) @import (#eq? @import "require_relative")) + (call method: (identifier) @import (#eq? @import "load")) +"#; + +/// Tree-sitter query for extracting Ruby function calls. +/// +/// This query captures: +/// - Direct method calls +/// - Method calls with receivers (object.method) +/// - Calls to constants (typically constructors like ClassName.new) +pub const CALL_QUERY: &str = r#" + ; Method calls + (call method: (identifier) @method.call) + + ; Method calls with receiver + (call receiver: (_) method: (identifier) @method.call) + + ; Calls to constants (typically constructors) + (call receiver: (constant) @function.call) +"#; diff --git a/crates/goose-mcp/src/developer/analyze/mod.rs b/crates/goose-mcp/src/developer/analyze/mod.rs index 5c36f0b318da..d94cb2e37b40 100644 --- a/crates/goose-mcp/src/developer/analyze/mod.rs +++ b/crates/goose-mcp/src/developer/analyze/mod.rs @@ -213,7 +213,7 @@ impl CodeAnalyzer { // Check if we support this language for parsing let supported = matches!( language, - "python" | "rust" | "javascript" | "typescript" | "go" | "java" | "kotlin" | "swift" + "python" | "rust" | "javascript" | "typescript" | "go" | "java" | "kotlin" | "swift" | "ruby" ); if !supported { diff --git a/crates/goose-mcp/src/developer/analyze/parser.rs b/crates/goose-mcp/src/developer/analyze/parser.rs index e44de3cbc991..3f9792c2ccac 100644 --- a/crates/goose-mcp/src/developer/analyze/parser.rs +++ b/crates/goose-mcp/src/developer/analyze/parser.rs @@ -42,6 +42,7 @@ impl ParserManager { "java" => tree_sitter_java::language(), "kotlin" => tree_sitter_kotlin::language(), "swift" => devgen_tree_sitter_swift::language(), + "ruby" => tree_sitter_ruby::language(), _ => { tracing::warn!("Unsupported language: {}", language); return Err(ErrorData::new( @@ -177,6 +178,7 @@ impl ElementExtractor { "java" => languages::java::ELEMENT_QUERY, "kotlin" => languages::kotlin::ELEMENT_QUERY, "swift" => languages::swift::ELEMENT_QUERY, + "ruby" => languages::ruby::ELEMENT_QUERY, _ => "", } } @@ -256,6 +258,7 @@ impl ElementExtractor { "java" => languages::java::CALL_QUERY, "kotlin" => languages::kotlin::CALL_QUERY, "swift" => languages::swift::CALL_QUERY, + "ruby" => languages::ruby::CALL_QUERY, _ => "", } } @@ -359,6 +362,7 @@ impl ElementExtractor { || kind == "deinit_declaration" || kind == "subscript_declaration" } + "ruby" => kind == "method" || kind == "singleton_method", _ => false, }; diff --git a/crates/goose-mcp/src/developer/analyze/tests/mod.rs b/crates/goose-mcp/src/developer/analyze/tests/mod.rs index efc42587736c..904828efef55 100644 --- a/crates/goose-mcp/src/developer/analyze/tests/mod.rs +++ b/crates/goose-mcp/src/developer/analyze/tests/mod.rs @@ -8,3 +8,4 @@ pub mod integration_tests; pub mod large_output_tests; pub mod parser_tests; pub mod traversal_tests; +pub mod ruby_test; diff --git a/crates/goose-mcp/src/developer/analyze/tests/ruby_test.rs b/crates/goose-mcp/src/developer/analyze/tests/ruby_test.rs new file mode 100644 index 000000000000..8490dcf2579b --- /dev/null +++ b/crates/goose-mcp/src/developer/analyze/tests/ruby_test.rs @@ -0,0 +1,92 @@ +#[cfg(test)] +mod ruby_tests { + use crate::developer::analyze::parser::{ElementExtractor, ParserManager}; + + #[test] + fn test_ruby_basic_parsing() { + let parser = ParserManager::new(); + let source = r#" +require 'json' + +class MyClass + attr_accessor :name + + def initialize(name) + @name = name + end + + def greet + puts "Hello" + end +end +"#; + + let tree = parser.parse(source, "ruby").unwrap(); + let result = ElementExtractor::extract_elements(&tree, source, "ruby").unwrap(); + + // Should find MyClass + assert_eq!(result.class_count, 1); + assert!(result.classes.iter().any(|c| c.name == "MyClass")); + + // Should find methods + assert!(result.function_count > 0); + assert!(result.functions.iter().any(|f| f.name == "initialize")); + assert!(result.functions.iter().any(|f| f.name == "greet")); + + // Should find require statement + assert!(result.import_count > 0); + } + + #[test] + fn test_ruby_attr_methods() { + let parser = ParserManager::new(); + let source = r#" +class Person + attr_reader :age + attr_writer :status + attr_accessor :name +end +"#; + + let tree = parser.parse(source, "ruby").unwrap(); + let result = ElementExtractor::extract_elements(&tree, source, "ruby").unwrap(); + + // attr_* should be recognized as functions + assert!(result.function_count >= 3, "Expected at least 3 functions from attr_* declarations, got {}", result.function_count); + } + + #[test] + fn test_ruby_require_patterns() { + let parser = ParserManager::new(); + let source = r#" +require 'json' +require_relative 'lib/helper' +"#; + + let tree = parser.parse(source, "ruby").unwrap(); + let result = ElementExtractor::extract_elements(&tree, source, "ruby").unwrap(); + + assert_eq!(result.import_count, 2, "Should find both require and require_relative"); + } + + #[test] + fn test_ruby_method_calls() { + let parser = ParserManager::new(); + let source = r#" +class Example + def test_method + puts "Hello" + JSON.parse("{}") + object.method_call + end +end +"#; + + let tree = parser.parse(source, "ruby").unwrap(); + let result = ElementExtractor::extract_with_depth(&tree, source, "ruby", "semantic").unwrap(); + + // Should find method calls + assert!(result.calls.len() > 0, "Should find method calls"); + assert!(result.calls.iter().any(|c| c.callee_name == "puts")); + } +}