Skip to content

Commit aa293da

Browse files
unhappychoiceclaude
andcommitted
feat: add Ruby language support
Add comprehensive Ruby language support to gittype including: ## Features Added - Language detection for .rb files - Tree-sitter Ruby parser integration - Code extraction for Ruby constructs: - Methods (def) - Classes (class) - Modules (module) - Ruby comment processing - Module chunk type support ## Implementation Details - Add tree-sitter-ruby dependency - Extend Language enum with Ruby variant - Update parser logic for Ruby syntax patterns - Add constant node type support for Ruby class/module names - Include Ruby files in default extraction patterns ## Testing - Add comprehensive Ruby extraction tests - Test method, class, and module extraction - Validate name extraction for Ruby constants - All 95 existing tests continue to pass Closes #54 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b79bb20 commit aa293da

File tree

7 files changed

+146
-0
lines changed

7 files changed

+146
-0
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ tree-sitter = "0.20"
2424
tree-sitter-rust = "0.20"
2525
tree-sitter-typescript = "0.20"
2626
tree-sitter-python = "0.20"
27+
tree-sitter-ruby = "0.20"
2728
crossterm = "0.27"
2829
ratatui = "0.26"
2930
rusqlite = { version = "0.29", features = ["bundled"] }

src/extractor/challenge_converter.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ impl ChallengeConverter {
345345
super::Language::Rust => "rust".to_string(),
346346
super::Language::TypeScript => "typescript".to_string(),
347347
super::Language::Python => "python".to_string(),
348+
super::Language::Ruby => "ruby".to_string(),
348349
}
349350
}
350351
}

src/extractor/chunk.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub enum ChunkType {
77
Class,
88
Method,
99
Struct,
10+
Module,
1011
}
1112

1213
#[derive(Debug, Clone)]

src/extractor/language.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub enum Language {
33
Rust,
44
TypeScript,
55
Python,
6+
Ruby,
67
}
78

89
impl Language {
@@ -11,6 +12,7 @@ impl Language {
1112
"rs" => Some(Language::Rust),
1213
"ts" | "tsx" => Some(Language::TypeScript),
1314
"py" => Some(Language::Python),
15+
"rb" => Some(Language::Ruby),
1416
_ => None,
1517
}
1618
}
@@ -20,6 +22,7 @@ impl Language {
2022
Language::Rust => "rs",
2123
Language::TypeScript => "ts",
2224
Language::Python => "py",
25+
Language::Ruby => "rb",
2326
}
2427
}
2528
}

src/extractor/parser.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ impl Default for ExtractionOptions {
2121
"**/*.ts".to_string(),
2222
"**/*.tsx".to_string(),
2323
"**/*.py".to_string(),
24+
"**/*.rb".to_string(),
2425
],
2526
exclude_patterns: vec![
2627
"**/target/**".to_string(),
@@ -72,6 +73,16 @@ impl CodeExtractor {
7273
))
7374
})?;
7475
}
76+
Language::Ruby => {
77+
parser
78+
.set_language(tree_sitter_ruby::language())
79+
.map_err(|e| {
80+
GitTypeError::ExtractionFailed(format!(
81+
"Failed to set Ruby language: {}",
82+
e
83+
))
84+
})?;
85+
}
7586
}
7687
Ok(parser)
7788
}
@@ -253,6 +264,11 @@ impl CodeExtractor {
253264
(function_definition name: (identifier) @name) @function
254265
(class_definition name: (identifier) @name) @class
255266
",
267+
Language::Ruby => "
268+
(method name: (identifier) @name) @method
269+
(class name: (constant) @name) @class
270+
(module name: (constant) @name) @module
271+
",
256272
};
257273

258274
let query = Query::new(tree.language(), query_str).map_err(|e| {
@@ -335,6 +351,7 @@ impl CodeExtractor {
335351
"method" => ChunkType::Method,
336352
"class" | "impl" => ChunkType::Class,
337353
"struct" => ChunkType::Struct,
354+
"module" => ChunkType::Module,
338355
"arrow_function" => ChunkType::Function,
339356
"function_expression" => ChunkType::Function,
340357
_ => return None,
@@ -403,6 +420,7 @@ impl CodeExtractor {
403420
if child.kind() == "identifier"
404421
|| child.kind() == "type_identifier"
405422
|| child.kind() == "property_identifier"
423+
|| child.kind() == "constant"
406424
{
407425
let start = child.start_byte();
408426
let end = child.end_byte();
@@ -547,6 +565,7 @@ impl CodeExtractor {
547565
Language::Rust => "[(line_comment) (block_comment)] @comment",
548566
Language::TypeScript => "(comment) @comment",
549567
Language::Python => "(comment) @comment",
568+
Language::Ruby => "(comment) @comment",
550569
};
551570

552571
let query = match Query::new(tree.language(), comment_query) {
@@ -569,6 +588,7 @@ impl CodeExtractor {
569588
Language::Rust => node_kind == "line_comment" || node_kind == "block_comment",
570589
Language::TypeScript => node_kind == "comment",
571590
Language::Python => node_kind == "comment",
591+
Language::Ruby => node_kind == "comment",
572592
};
573593

574594
if !is_valid_comment {

tests/extractor_unit_tests.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ fn test_language_from_extension() {
2424
assert_eq!(Language::from_extension("ts"), Some(Language::TypeScript));
2525
assert_eq!(Language::from_extension("tsx"), Some(Language::TypeScript));
2626
assert_eq!(Language::from_extension("py"), Some(Language::Python));
27+
assert_eq!(Language::from_extension("rb"), Some(Language::Ruby));
2728
assert_eq!(Language::from_extension("unknown"), None);
2829
}
2930

@@ -334,3 +335,111 @@ class TsClass{} {{
334335
"Parallel parsing should complete within 5 seconds"
335336
);
336337
}
338+
339+
#[test]
340+
fn test_ruby_function_extraction() {
341+
let temp_dir = TempDir::new().unwrap();
342+
let file_path = temp_dir.path().join("test.rb");
343+
344+
let ruby_code = r#"
345+
def hello_world
346+
puts "Hello, world!"
347+
end
348+
349+
def calculate_sum(a, b)
350+
a + b
351+
end
352+
"#;
353+
fs::write(&file_path, ruby_code).unwrap();
354+
355+
let mut extractor = CodeExtractor::new().unwrap();
356+
let chunks = extractor
357+
.extract_chunks(temp_dir.path(), ExtractionOptions::default())
358+
.unwrap();
359+
360+
assert_eq!(chunks.len(), 2);
361+
assert_eq!(chunks[0].name, "hello_world");
362+
assert_eq!(chunks[1].name, "calculate_sum");
363+
assert!(matches!(chunks[0].chunk_type, ChunkType::Method));
364+
assert!(matches!(chunks[1].chunk_type, ChunkType::Method));
365+
}
366+
367+
#[test]
368+
fn test_ruby_class_extraction() {
369+
let temp_dir = TempDir::new().unwrap();
370+
let file_path = temp_dir.path().join("test.rb");
371+
372+
let ruby_code = r#"
373+
class Person
374+
attr_accessor :name, :age
375+
376+
def initialize(name, age)
377+
@name = name
378+
@age = age
379+
end
380+
381+
def greet
382+
puts "Hello, I'm #{@name}!"
383+
end
384+
end
385+
"#;
386+
fs::write(&file_path, ruby_code).unwrap();
387+
388+
let mut extractor = CodeExtractor::new().unwrap();
389+
let chunks = extractor
390+
.extract_chunks(temp_dir.path(), ExtractionOptions::default())
391+
.unwrap();
392+
393+
assert_eq!(chunks.len(), 3); // class + 2 methods
394+
395+
// Find class chunk
396+
let class_chunk = chunks.iter().find(|c| matches!(c.chunk_type, ChunkType::Class)).unwrap();
397+
assert_eq!(class_chunk.name, "Person");
398+
399+
// Find method chunks
400+
let method_chunks: Vec<_> = chunks.iter().filter(|c| matches!(c.chunk_type, ChunkType::Method)).collect();
401+
assert_eq!(method_chunks.len(), 2);
402+
403+
let method_names: Vec<&String> = method_chunks.iter().map(|c| &c.name).collect();
404+
assert!(method_names.contains(&&"initialize".to_string()));
405+
assert!(method_names.contains(&&"greet".to_string()));
406+
}
407+
408+
#[test]
409+
fn test_ruby_module_extraction() {
410+
let temp_dir = TempDir::new().unwrap();
411+
let file_path = temp_dir.path().join("test.rb");
412+
413+
let ruby_code = r#"
414+
module Authentication
415+
def login(username, password)
416+
puts "Logging in #{username}"
417+
true
418+
end
419+
420+
def logout
421+
puts "Logged out"
422+
end
423+
end
424+
"#;
425+
fs::write(&file_path, ruby_code).unwrap();
426+
427+
let mut extractor = CodeExtractor::new().unwrap();
428+
let chunks = extractor
429+
.extract_chunks(temp_dir.path(), ExtractionOptions::default())
430+
.unwrap();
431+
432+
assert_eq!(chunks.len(), 3); // module + 2 methods
433+
434+
// Find module chunk
435+
let module_chunk = chunks.iter().find(|c| matches!(c.chunk_type, ChunkType::Module)).unwrap();
436+
assert_eq!(module_chunk.name, "Authentication");
437+
438+
// Find method chunks
439+
let method_chunks: Vec<_> = chunks.iter().filter(|c| matches!(c.chunk_type, ChunkType::Method)).collect();
440+
assert_eq!(method_chunks.len(), 2);
441+
442+
let method_names: Vec<&String> = method_chunks.iter().map(|c| &c.name).collect();
443+
assert!(method_names.contains(&&"login".to_string()));
444+
assert!(method_names.contains(&&"logout".to_string()));
445+
}

0 commit comments

Comments
 (0)