use std::path::Path; use compliance_core::error::CoreError; use compliance_core::models::graph::{CodeEdge, CodeEdgeKind, CodeNode, CodeNodeKind}; use compliance_core::traits::graph_builder::{LanguageParser, ParseOutput}; use tree_sitter::{Node, Parser}; pub struct RustParser; impl Default for RustParser { fn default() -> Self { Self::new() } } impl RustParser { pub fn new() -> Self { Self } fn walk_tree( &self, node: Node<'_>, source: &str, file_path: &str, repo_id: &str, graph_build_id: &str, parent_qualified: Option<&str>, output: &mut ParseOutput, ) { match node.kind() { "function_item" | "function_signature_item" => { if let Some(name_node) = node.child_by_field_name("name") { let name = &source[name_node.byte_range()]; let qualified = match parent_qualified { Some(p) => format!("{p}::{name}"), None => format!("{file_path}::{name}"), }; let is_entry = name == "main" || self.has_attribute(&node, source, "test") || self.has_attribute(&node, source, "tokio::main") || self.has_pub_visibility(&node, source); output.nodes.push(CodeNode { id: None, repo_id: repo_id.to_string(), graph_build_id: graph_build_id.to_string(), qualified_name: qualified.clone(), name: name.to_string(), kind: CodeNodeKind::Function, file_path: file_path.to_string(), start_line: node.start_position().row as u32 + 1, end_line: node.end_position().row as u32 + 1, language: "rust".to_string(), community_id: None, is_entry_point: is_entry, graph_index: None, }); // Extract function calls within the body if let Some(body) = node.child_by_field_name("body") { self.extract_calls( body, source, file_path, repo_id, graph_build_id, &qualified, output, ); } } } "struct_item" => { if let Some(name_node) = node.child_by_field_name("name") { let name = &source[name_node.byte_range()]; let qualified = match parent_qualified { Some(p) => format!("{p}::{name}"), None => format!("{file_path}::{name}"), }; output.nodes.push(CodeNode { id: None, repo_id: repo_id.to_string(), graph_build_id: graph_build_id.to_string(), qualified_name: qualified, name: name.to_string(), kind: CodeNodeKind::Struct, file_path: file_path.to_string(), start_line: node.start_position().row as u32 + 1, end_line: node.end_position().row as u32 + 1, language: "rust".to_string(), community_id: None, is_entry_point: false, graph_index: None, }); } } "enum_item" => { if let Some(name_node) = node.child_by_field_name("name") { let name = &source[name_node.byte_range()]; let qualified = match parent_qualified { Some(p) => format!("{p}::{name}"), None => format!("{file_path}::{name}"), }; output.nodes.push(CodeNode { id: None, repo_id: repo_id.to_string(), graph_build_id: graph_build_id.to_string(), qualified_name: qualified, name: name.to_string(), kind: CodeNodeKind::Enum, file_path: file_path.to_string(), start_line: node.start_position().row as u32 + 1, end_line: node.end_position().row as u32 + 1, language: "rust".to_string(), community_id: None, is_entry_point: false, graph_index: None, }); } } "trait_item" => { if let Some(name_node) = node.child_by_field_name("name") { let name = &source[name_node.byte_range()]; let qualified = match parent_qualified { Some(p) => format!("{p}::{name}"), None => format!("{file_path}::{name}"), }; output.nodes.push(CodeNode { id: None, repo_id: repo_id.to_string(), graph_build_id: graph_build_id.to_string(), qualified_name: qualified.clone(), name: name.to_string(), kind: CodeNodeKind::Trait, file_path: file_path.to_string(), start_line: node.start_position().row as u32 + 1, end_line: node.end_position().row as u32 + 1, language: "rust".to_string(), community_id: None, is_entry_point: false, graph_index: None, }); // Parse methods inside the trait self.walk_children( node, source, file_path, repo_id, graph_build_id, Some(&qualified), output, ); return; // Don't walk children again } } "impl_item" => { // Extract impl target type for qualified naming let impl_name = self.extract_impl_type(&node, source); let qualified = match parent_qualified { Some(p) => format!("{p}::{impl_name}"), None => format!("{file_path}::{impl_name}"), }; // Check for trait impl (impl Trait for Type) if let Some(trait_node) = node.child_by_field_name("trait") { let trait_name = &source[trait_node.byte_range()]; output.edges.push(CodeEdge { id: None, repo_id: repo_id.to_string(), graph_build_id: graph_build_id.to_string(), source: qualified.clone(), target: trait_name.to_string(), kind: CodeEdgeKind::Implements, file_path: file_path.to_string(), line_number: Some(node.start_position().row as u32 + 1), }); } // Walk methods inside impl block self.walk_children( node, source, file_path, repo_id, graph_build_id, Some(&qualified), output, ); return; } "use_declaration" => { let use_text = &source[node.byte_range()]; // Extract the imported path if let Some(path) = self.extract_use_path(use_text) { output.edges.push(CodeEdge { id: None, repo_id: repo_id.to_string(), graph_build_id: graph_build_id.to_string(), source: parent_qualified.unwrap_or(file_path).to_string(), target: path, kind: CodeEdgeKind::Imports, file_path: file_path.to_string(), line_number: Some(node.start_position().row as u32 + 1), }); } } "mod_item" => { if let Some(name_node) = node.child_by_field_name("name") { let name = &source[name_node.byte_range()]; let qualified = match parent_qualified { Some(p) => format!("{p}::{name}"), None => format!("{file_path}::{name}"), }; output.nodes.push(CodeNode { id: None, repo_id: repo_id.to_string(), graph_build_id: graph_build_id.to_string(), qualified_name: qualified.clone(), name: name.to_string(), kind: CodeNodeKind::Module, file_path: file_path.to_string(), start_line: node.start_position().row as u32 + 1, end_line: node.end_position().row as u32 + 1, language: "rust".to_string(), community_id: None, is_entry_point: false, graph_index: None, }); // If it has a body (inline module), walk it if let Some(body) = node.child_by_field_name("body") { self.walk_children( body, source, file_path, repo_id, graph_build_id, Some(&qualified), output, ); return; } } } _ => {} } // Default: walk children self.walk_children( node, source, file_path, repo_id, graph_build_id, parent_qualified, output, ); } fn walk_children( &self, node: Node<'_>, source: &str, file_path: &str, repo_id: &str, graph_build_id: &str, parent_qualified: Option<&str>, output: &mut ParseOutput, ) { let mut cursor = node.walk(); for child in node.children(&mut cursor) { self.walk_tree( child, source, file_path, repo_id, graph_build_id, parent_qualified, output, ); } } fn extract_calls( &self, node: Node<'_>, source: &str, file_path: &str, repo_id: &str, graph_build_id: &str, caller_qualified: &str, output: &mut ParseOutput, ) { if node.kind() == "call_expression" { if let Some(func_node) = node.child_by_field_name("function") { let callee = &source[func_node.byte_range()]; output.edges.push(CodeEdge { id: None, repo_id: repo_id.to_string(), graph_build_id: graph_build_id.to_string(), source: caller_qualified.to_string(), target: callee.to_string(), kind: CodeEdgeKind::Calls, file_path: file_path.to_string(), line_number: Some(node.start_position().row as u32 + 1), }); } } let mut cursor = node.walk(); for child in node.children(&mut cursor) { self.extract_calls( child, source, file_path, repo_id, graph_build_id, caller_qualified, output, ); } } fn has_attribute(&self, node: &Node<'_>, source: &str, attr_name: &str) -> bool { if let Some(prev) = node.prev_sibling() { if prev.kind() == "attribute_item" || prev.kind() == "attribute" { let text = &source[prev.byte_range()]; return text.contains(attr_name); } } false } fn has_pub_visibility(&self, node: &Node<'_>, source: &str) -> bool { let mut cursor = node.walk(); for child in node.children(&mut cursor) { if child.kind() == "visibility_modifier" { let text = &source[child.byte_range()]; return text == "pub"; } } false } fn extract_impl_type(&self, node: &Node<'_>, source: &str) -> String { if let Some(type_node) = node.child_by_field_name("type") { return source[type_node.byte_range()].to_string(); } "unknown".to_string() } fn extract_use_path(&self, use_text: &str) -> Option { // "use foo::bar::baz;" -> "foo::bar::baz" let trimmed = use_text.strip_prefix("use ")?.trim_end_matches(';').trim(); Some(trimmed.to_string()) } } #[cfg(test)] mod tests { use super::*; use compliance_core::traits::graph_builder::LanguageParser; use std::path::PathBuf; fn parse_rust(source: &str) -> ParseOutput { let parser = RustParser::new(); parser .parse_file(&PathBuf::from("test.rs"), source, "repo1", "build1") .unwrap() } #[test] fn test_extract_use_path_simple() { let parser = RustParser::new(); assert_eq!( parser.extract_use_path("use std::collections::HashMap;"), Some("std::collections::HashMap".to_string()) ); } #[test] fn test_extract_use_path_nested() { let parser = RustParser::new(); assert_eq!( parser.extract_use_path("use crate::models::graph::CodeNode;"), Some("crate::models::graph::CodeNode".to_string()) ); } #[test] fn test_extract_use_path_no_prefix() { let parser = RustParser::new(); assert_eq!(parser.extract_use_path("let x = 5;"), None); } #[test] fn test_extract_use_path_empty() { let parser = RustParser::new(); assert_eq!(parser.extract_use_path(""), None); } #[test] fn test_parse_function() { let output = parse_rust("fn hello() {\n let x = 1;\n}\n"); let fn_nodes: Vec<_> = output .nodes .iter() .filter(|n| n.kind == CodeNodeKind::Function) .collect(); assert_eq!(fn_nodes.len(), 1); assert_eq!(fn_nodes[0].name, "hello"); assert!(fn_nodes[0].qualified_name.contains("hello")); } #[test] fn test_parse_struct() { let output = parse_rust("struct Foo {\n x: i32,\n}\n"); let struct_nodes: Vec<_> = output .nodes .iter() .filter(|n| n.kind == CodeNodeKind::Struct) .collect(); assert_eq!(struct_nodes.len(), 1); assert_eq!(struct_nodes[0].name, "Foo"); } #[test] fn test_parse_enum() { let output = parse_rust("enum Color {\n Red,\n Blue,\n}\n"); let enum_nodes: Vec<_> = output .nodes .iter() .filter(|n| n.kind == CodeNodeKind::Enum) .collect(); assert_eq!(enum_nodes.len(), 1); assert_eq!(enum_nodes[0].name, "Color"); } #[test] fn test_parse_trait() { let output = parse_rust("trait Drawable {\n fn draw(&self);\n}\n"); let trait_nodes: Vec<_> = output .nodes .iter() .filter(|n| n.kind == CodeNodeKind::Trait) .collect(); assert_eq!(trait_nodes.len(), 1); assert_eq!(trait_nodes[0].name, "Drawable"); } #[test] fn test_parse_file_node_always_created() { let output = parse_rust(""); let file_nodes: Vec<_> = output .nodes .iter() .filter(|n| n.kind == CodeNodeKind::File) .collect(); assert_eq!(file_nodes.len(), 1); assert_eq!(file_nodes[0].language, "rust"); } #[test] fn test_parse_multiple_functions() { let source = "fn foo() {}\nfn bar() {}\nfn baz() {}\n"; let output = parse_rust(source); let fn_nodes: Vec<_> = output .nodes .iter() .filter(|n| n.kind == CodeNodeKind::Function) .collect(); assert_eq!(fn_nodes.len(), 3); } #[test] fn test_parse_main_is_entry_point() { let output = parse_rust("fn main() {\n println!(\"hi\");\n}\n"); let main_node = output.nodes.iter().find(|n| n.name == "main").unwrap(); assert!(main_node.is_entry_point); } #[test] fn test_parse_pub_fn_is_entry_point() { let output = parse_rust("pub fn handler() {}\n"); let node = output.nodes.iter().find(|n| n.name == "handler").unwrap(); assert!(node.is_entry_point); } #[test] fn test_parse_private_fn_is_not_entry_point() { let output = parse_rust("fn helper() {}\n"); let node = output.nodes.iter().find(|n| n.name == "helper").unwrap(); assert!(!node.is_entry_point); } #[test] fn test_parse_function_calls_create_edges() { let source = "fn caller() {\n callee();\n}\nfn callee() {}\n"; let output = parse_rust(source); let call_edges: Vec<_> = output .edges .iter() .filter(|e| e.kind == CodeEdgeKind::Calls) .collect(); assert!(!call_edges.is_empty()); assert!(call_edges.iter().any(|e| e.target.contains("callee"))); } #[test] fn test_parse_use_declaration_creates_import_edge() { let source = "use std::collections::HashMap;\nfn foo() {}\n"; let output = parse_rust(source); let import_edges: Vec<_> = output .edges .iter() .filter(|e| e.kind == CodeEdgeKind::Imports) .collect(); assert!(!import_edges.is_empty()); } #[test] fn test_parse_impl_methods() { let source = "struct Foo {}\nimpl Foo {\n fn do_thing(&self) {}\n}\n"; let output = parse_rust(source); let fn_nodes: Vec<_> = output .nodes .iter() .filter(|n| n.kind == CodeNodeKind::Function) .collect(); assert_eq!(fn_nodes.len(), 1); assert_eq!(fn_nodes[0].name, "do_thing"); // Method should be qualified under the impl type assert!(fn_nodes[0].qualified_name.contains("Foo")); } #[test] fn test_parse_mod_item() { let source = "mod inner {\n fn nested() {}\n}\n"; let output = parse_rust(source); let mod_nodes: Vec<_> = output .nodes .iter() .filter(|n| n.kind == CodeNodeKind::Module) .collect(); assert_eq!(mod_nodes.len(), 1); assert_eq!(mod_nodes[0].name, "inner"); } #[test] fn test_parse_line_numbers() { let source = "fn first() {}\n\n\nfn second() {}\n"; let output = parse_rust(source); let first = output.nodes.iter().find(|n| n.name == "first").unwrap(); let second = output.nodes.iter().find(|n| n.name == "second").unwrap(); assert_eq!(first.start_line, 1); assert!(second.start_line > first.start_line); } #[test] fn test_language_and_extensions() { let parser = RustParser::new(); assert_eq!(parser.language(), "rust"); assert_eq!(parser.extensions(), &["rs"]); } } impl LanguageParser for RustParser { fn language(&self) -> &str { "rust" } fn extensions(&self) -> &[&str] { &["rs"] } fn parse_file( &self, file_path: &Path, source: &str, repo_id: &str, graph_build_id: &str, ) -> Result { let mut parser = Parser::new(); let language = tree_sitter_rust::LANGUAGE; parser .set_language(&language.into()) .map_err(|e| CoreError::Graph(format!("Failed to set Rust language: {e}")))?; let tree = parser .parse(source, None) .ok_or_else(|| CoreError::Graph("Failed to parse Rust file".to_string()))?; let file_path_str = file_path.to_string_lossy().to_string(); let mut output = ParseOutput::default(); // Add file node output.nodes.push(CodeNode { id: None, repo_id: repo_id.to_string(), graph_build_id: graph_build_id.to_string(), qualified_name: file_path_str.clone(), name: file_path .file_name() .map(|n| n.to_string_lossy().to_string()) .unwrap_or_default(), kind: CodeNodeKind::File, file_path: file_path_str.clone(), start_line: 1, end_line: source.lines().count() as u32, language: "rust".to_string(), community_id: None, is_entry_point: false, graph_index: None, }); self.walk_tree( tree.root_node(), source, &file_path_str, repo_id, graph_build_id, None, &mut output, ); Ok(output) } }