Skip to main content
Glama

CodeGraph CLI MCP Server

by Jakedismo
ruby.rs11.9 kB
use codegraph_core::{CodeNode, Language, Location, NodeType}; use tree_sitter::{Node, Tree, TreeCursor}; /// Advanced Ruby AST extractor for dynamic programming intelligence. /// /// Extracts: /// - classes, modules, methods, constants /// - metaprogramming patterns (define_method, class_eval, etc.) /// - Rails patterns (controllers, models, migrations) /// - blocks, lambdas, and functional patterns /// - mixins, includes, and module composition /// - attr_accessor and dynamic property definitions /// - gem dependencies and require statements /// /// Notes: /// - Optimized for Ruby on Rails development patterns /// - Captures dynamic and metaprogramming constructs /// - Handles Ruby's flexible syntax and duck typing /// - Understands Rails conventions and patterns pub struct RubyExtractor; #[derive(Default, Clone)] struct RubyContext { module_path: Vec<String>, current_class: Option<String>, current_module: Option<String>, is_rails_file: bool, } impl RubyExtractor { pub fn extract(tree: &Tree, content: &str, file_path: &str) -> Vec<CodeNode> { let mut collector = RubyCollector::new(content, file_path); let mut cursor = tree.walk(); // Detect Rails patterns from file path let is_rails = file_path.contains("/app/") || file_path.contains("/config/") || file_path.contains("/db/migrate"); let mut ctx = RubyContext::default(); ctx.is_rails_file = is_rails; collector.walk(&mut cursor, ctx); collector.into_nodes() } } struct RubyCollector<'a> { content: &'a str, file_path: &'a str, nodes: Vec<CodeNode>, } impl<'a> RubyCollector<'a> { fn new(content: &'a str, file_path: &'a str) -> Self { Self { content, file_path, nodes: Vec::new(), } } fn into_nodes(self) -> Vec<CodeNode> { self.nodes } fn walk(&mut self, cursor: &mut TreeCursor, mut ctx: RubyContext) { let node = cursor.node(); match node.kind() { // Ruby Classes "class" => { if let Some(name) = self.child_text_by_field(node, "name") { let loc = self.location(&node); let content_text = self.node_text(&node); let mut code = CodeNode::new( name.clone(), Some(NodeType::Class), Some(Language::Ruby), loc, ) .with_content(content_text.clone()); // Detect inheritance if let Some(superclass) = self.child_text_by_field(node, "superclass") { code.metadata .attributes .insert("superclass".into(), superclass); } // Detect Rails patterns if ctx.is_rails_file { if name.ends_with("Controller") { code.metadata .attributes .insert("rails_pattern".into(), "controller".into()); } else if content_text.contains("< ApplicationRecord") { code.metadata .attributes .insert("rails_pattern".into(), "model".into()); } else if content_text.contains("< ActiveRecord::Migration") { code.metadata .attributes .insert("rails_pattern".into(), "migration".into()); } } code.metadata .attributes .insert("kind".into(), "class".into()); self.nodes.push(code); ctx.current_class = Some(name); } } // Ruby Modules "module" => { if let Some(name) = self.child_text_by_field(node, "name") { let loc = self.location(&node); let mut code = CodeNode::new( name.clone(), Some(NodeType::Module), Some(Language::Ruby), loc, ) .with_content(self.node_text(&node)); code.metadata .attributes .insert("kind".into(), "module".into()); self.nodes.push(code); ctx.module_path.push(name.clone()); ctx.current_module = Some(name); } } // Ruby Methods "method" => { if let Some(name) = self.child_text_by_field(node, "name") { let loc = self.location(&node); let content_text = self.node_text(&node); let mut code = CodeNode::new( name.clone(), Some(NodeType::Function), Some(Language::Ruby), loc, ) .with_content(content_text.clone()); // Detect class vs instance methods if content_text.starts_with("def self.") { code.metadata .attributes .insert("method_type".into(), "class".into()); } else { code.metadata .attributes .insert("method_type".into(), "instance".into()); } // Detect Rails action methods if ctx.is_rails_file && ctx .current_class .as_ref() .map_or(false, |c| c.ends_with("Controller")) { if matches!( name.as_str(), "index" | "show" | "new" | "create" | "edit" | "update" | "destroy" ) { code.metadata .attributes .insert("rails_action".into(), "true".into()); } } // Detect metaprogramming patterns if content_text.contains("define_method") { code.metadata .attributes .insert("metaprogramming".into(), "define_method".into()); } code.metadata .attributes .insert("kind".into(), "method".into()); if let Some(ref current_class) = ctx.current_class { code.metadata .attributes .insert("parent_class".into(), current_class.clone()); } self.nodes.push(code); } } // Ruby Constants "constant" => { let name = self.node_text(&node); let loc = self.location(&node); let mut code = CodeNode::new( name.clone(), Some(NodeType::Variable), Some(Language::Ruby), loc, ) .with_content(name.clone()); code.metadata .attributes .insert("kind".into(), "constant".into()); self.nodes.push(code); } // Ruby attr_* declarations "call" => { let call_text = self.node_text(&node); if call_text.starts_with("attr_") { let loc = self.location(&node); let mut code = CodeNode::new( call_text.clone(), Some(NodeType::Variable), Some(Language::Ruby), loc, ) .with_content(call_text.clone()); // Detect specific attr types if call_text.starts_with("attr_accessor") { code.metadata .attributes .insert("attr_type".into(), "accessor".into()); } else if call_text.starts_with("attr_reader") { code.metadata .attributes .insert("attr_type".into(), "reader".into()); } else if call_text.starts_with("attr_writer") { code.metadata .attributes .insert("attr_type".into(), "writer".into()); } code.metadata .attributes .insert("kind".into(), "attribute".into()); self.nodes.push(code); } } // Ruby require/load statements "call" if self.node_text(&node).starts_with("require") => { let require_text = self.node_text(&node); let loc = self.location(&node); let mut code = CodeNode::new( require_text.clone(), Some(NodeType::Import), Some(Language::Ruby), loc, ) .with_content(require_text.clone()); code.metadata .attributes .insert("kind".into(), "require".into()); // Extract required gem/file name if let Some(arg) = self.extract_string_argument(&node) { code.metadata .attributes .insert("required_module".into(), arg); } self.nodes.push(code); } _ => {} } // Recursively walk children if cursor.goto_first_child() { loop { self.walk(cursor, ctx.clone()); if !cursor.goto_next_sibling() { break; } } cursor.goto_parent(); } } fn child_text_by_field(&self, node: Node, field_name: &str) -> Option<String> { node.child_by_field_name(field_name) .map(|child| self.node_text(&child)) } fn extract_string_argument(&self, node: &Node) -> Option<String> { // Extract string argument from method calls like require "gem_name" for i in 0..node.child_count() { if let Some(child) = node.child(i) { if child.kind() == "string" { let text = self.node_text(&child); return Some(text.trim_matches('"').trim_matches('\'').to_string()); } } } None } fn node_text(&self, node: &Node) -> String { node.utf8_text(self.content.as_bytes()) .unwrap_or("") .to_string() } fn location(&self, node: &Node) -> Location { Location { file_path: self.file_path.to_string(), line: (node.start_position().row + 1) as u32, column: (node.start_position().column + 1) as u32, end_line: Some((node.end_position().row + 1) as u32), end_column: Some((node.end_position().column + 1) as u32), } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jakedismo/codegraph-rust'

If you have feedback or need assistance with the MCP directory API, please join our Discord server