Skip to content
Closed
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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/goose-mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions crates/goose-mcp/src/developer/analyze/languages/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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,
_ => "",
}
}
Expand All @@ -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,
_ => "",
}
}
42 changes: 42 additions & 0 deletions crates/goose-mcp/src/developer/analyze/languages/ruby.rs
Original file line number Diff line number Diff line change
@@ -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)
"#;
2 changes: 1 addition & 1 deletion crates/goose-mcp/src/developer/analyze/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions crates/goose-mcp/src/developer/analyze/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
_ => "",
}
}
Expand Down Expand Up @@ -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,
_ => "",
}
}
Expand Down Expand Up @@ -359,6 +362,7 @@ impl ElementExtractor {
|| kind == "deinit_declaration"
|| kind == "subscript_declaration"
}
"ruby" => kind == "method" || kind == "singleton_method",
_ => false,
};

Expand Down
1 change: 1 addition & 0 deletions crates/goose-mcp/src/developer/analyze/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
92 changes: 92 additions & 0 deletions crates/goose-mcp/src/developer/analyze/tests/ruby_test.rs
Original file line number Diff line number Diff line change
@@ -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"));
}
}
Loading