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 JavaScriptParser; impl Default for JavaScriptParser { fn default() -> Self { Self::new() } } impl JavaScriptParser { 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_declaration" => { 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 = self.is_exported_function(&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: "javascript".to_string(), community_id: None, is_entry_point: is_entry, graph_index: None, }); if let Some(body) = node.child_by_field_name("body") { self.extract_calls( body, source, file_path, repo_id, graph_build_id, &qualified, output, ); } } } "class_declaration" => { 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::Class, 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: "javascript".to_string(), community_id: None, is_entry_point: false, graph_index: None, }); // Extract superclass if let Some(heritage) = node.child_by_field_name("superclass") { let base_name = &source[heritage.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: base_name.to_string(), kind: CodeEdgeKind::Inherits, file_path: file_path.to_string(), line_number: Some(node.start_position().row as u32 + 1), }); } 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; } } "method_definition" => { 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::Method, 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: "javascript".to_string(), community_id: None, is_entry_point: false, graph_index: None, }); if let Some(body) = node.child_by_field_name("body") { self.extract_calls( body, source, file_path, repo_id, graph_build_id, &qualified, output, ); } } } // Arrow functions assigned to variables: const foo = () => {} "lexical_declaration" | "variable_declaration" => { self.extract_arrow_functions( node, source, file_path, repo_id, graph_build_id, parent_qualified, output, ); } "import_statement" => { let text = &source[node.byte_range()]; if let Some(module) = self.extract_import_source(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: module, kind: CodeEdgeKind::Imports, file_path: file_path.to_string(), line_number: Some(node.start_position().row as u32 + 1), }); } } _ => {} } 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 extract_arrow_functions( &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) { if child.kind() == "variable_declarator" { let name_node = child.child_by_field_name("name"); let value_node = child.child_by_field_name("value"); if let (Some(name_n), Some(value_n)) = (name_node, value_node) { if value_n.kind() == "arrow_function" || value_n.kind() == "function" { let name = &source[name_n.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::Function, file_path: file_path.to_string(), start_line: child.start_position().row as u32 + 1, end_line: child.end_position().row as u32 + 1, language: "javascript".to_string(), community_id: None, is_entry_point: false, graph_index: None, }); if let Some(body) = value_n.child_by_field_name("body") { self.extract_calls( body, source, file_path, repo_id, graph_build_id, &qualified, output, ); } } } } } } fn is_exported_function(&self, node: &Node<'_>, source: &str) -> bool { if let Some(parent) = node.parent() { if parent.kind() == "export_statement" { return true; } } // Check for module.exports patterns if let Some(prev) = node.prev_sibling() { let text = &source[prev.byte_range()]; if text.contains("module.exports") || text.contains("exports.") { return true; } } false } fn extract_import_source(&self, import_text: &str) -> Option { // import ... from 'module' or import 'module' let from_idx = import_text.find("from "); let start = if let Some(idx) = from_idx { idx + 5 } else { import_text.find("import ")? + 7 }; let rest = &import_text[start..]; let module = rest .trim() .trim_matches(|c| c == '\'' || c == '"' || c == ';' || c == ' '); if module.is_empty() { None } else { Some(module.to_string()) } } } impl LanguageParser for JavaScriptParser { fn language(&self) -> &str { "javascript" } fn extensions(&self) -> &[&str] { &["js", "jsx", "mjs", "cjs"] } 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_javascript::LANGUAGE; parser .set_language(&language.into()) .map_err(|e| CoreError::Graph(format!("Failed to set JavaScript language: {e}")))?; let tree = parser .parse(source, None) .ok_or_else(|| CoreError::Graph("Failed to parse JavaScript file".to_string()))?; let file_path_str = file_path.to_string_lossy().to_string(); let mut output = ParseOutput::default(); 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: "javascript".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) } }