the_code_graph_cli/adapters/
parse.rs1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use domain::error::Result;
5use domain::model::{Edge, FileNode};
6use domain::ports::FileData;
7use parser::resolver::{ResolveContext, ResolverRegistry};
8use parser::{ParseResult, ParserRegistry};
9use rayon::prelude::*;
10use sha2::{Digest, Sha256};
11
12pub struct RayonParseProvider {
13 registry: ParserRegistry,
14}
15
16impl Default for RayonParseProvider {
17 fn default() -> Self {
18 Self::new()
19 }
20}
21
22impl RayonParseProvider {
23 pub fn new() -> Self {
24 Self {
25 registry: ParserRegistry::new(),
26 }
27 }
28
29 fn compute_hash(content: &[u8]) -> String {
30 let mut hasher = Sha256::new();
31 hasher.update(content);
32 format!("{:x}", hasher.finalize())
33 }
34}
35
36impl domain::ports::ParseProvider for RayonParseProvider {
37 fn parse_and_resolve(
38 &self,
39 files: &[(PathBuf, Vec<u8>)],
40 project_root: &Path,
41 ) -> Result<Vec<FileData>> {
42 if files.is_empty() {
43 return Ok(vec![]);
44 }
45
46 let parse_results: Vec<(PathBuf, Vec<u8>, ParseResult, domain::model::Language)> = files
48 .par_iter()
49 .filter_map(|(path, source)| {
50 let parser = self.registry.parser_for_file(path)?;
51 match parser.parse(source, path) {
52 Ok(result) => Some((path.clone(), source.clone(), result, parser.language())),
53 Err(e) => {
54 tracing::warn!("parse failed for {}: {e}", path.display());
55 None
56 }
57 }
58 })
59 .collect();
60
61 let parsed_files: HashMap<PathBuf, ParseResult> = parse_results
63 .iter()
64 .map(|(path, _, result, _)| (path.clone(), result.clone()))
65 .collect();
66
67 let file_tree: Vec<PathBuf> = files.iter().map(|(p, _)| p.clone()).collect();
68
69 let context = ResolveContext {
70 project_root: project_root.to_path_buf(),
71 parsed_files,
72 file_tree,
73 };
74
75 let resolver_registry = ResolverRegistry::new(project_root);
77
78 let file_data: Vec<FileData> = parse_results
79 .par_iter()
80 .map(|(path, source, parse_result, lang)| {
81 let resolved_edges = resolver_registry
82 .resolve_file(path, *lang, parse_result, &context)
83 .unwrap_or_else(|e| {
84 tracing::warn!("resolve failed for {}: {e}", path.display());
85 vec![]
86 });
87
88 let mut all_edges: Vec<Edge> = parse_result.edges.clone();
90 all_edges.extend(resolved_edges);
91
92 let file = FileNode {
93 path: path.clone(),
94 language: *lang,
95 hash: Self::compute_hash(source),
96 };
97
98 FileData {
99 file,
100 symbols: parse_result.symbols.clone(),
101 edges: all_edges,
102 }
103 })
104 .collect();
105
106 Ok(file_data)
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use domain::ports::ParseProvider;
114
115 #[test]
116 fn empty_files_returns_empty() {
117 let provider = RayonParseProvider::new();
118 let result = provider.parse_and_resolve(&[], Path::new("/tmp")).unwrap();
119 assert!(result.is_empty());
120 }
121
122 #[test]
123 fn single_ts_file_returns_file_data() {
124 let provider = RayonParseProvider::new();
125 let source = b"export function hello(): void {}\nexport class Foo {}".to_vec();
126 let files = vec![(PathBuf::from("src/main.ts"), source)];
127 let result = provider
128 .parse_and_resolve(&files, Path::new("/tmp"))
129 .unwrap();
130 assert_eq!(result.len(), 1);
131 let fd = &result[0];
132 assert_eq!(fd.file.path, PathBuf::from("src/main.ts"));
133 assert!(!fd.symbols.is_empty(), "should have symbols");
134 assert!(!fd.edges.is_empty(), "should have structural edges");
135 }
136
137 #[test]
138 fn unsupported_extension_is_skipped() {
139 let provider = RayonParseProvider::new();
140 let files = vec![(PathBuf::from("readme.md"), b"# Hello".to_vec())];
141 let result = provider
142 .parse_and_resolve(&files, Path::new("/tmp"))
143 .unwrap();
144 assert!(result.is_empty());
145 }
146
147 #[test]
148 fn parse_error_skips_file_others_succeed() {
149 let provider = RayonParseProvider::new();
150 let good = b"export function hello(): void {}".to_vec();
151 let files = vec![
152 (PathBuf::from("src/good.ts"), good),
153 (PathBuf::from("src/also_good.rs"), b"fn main() {}".to_vec()),
154 ];
155 let result = provider
156 .parse_and_resolve(&files, Path::new("/tmp"))
157 .unwrap();
158 assert_eq!(result.len(), 2);
159 }
160
161 #[test]
162 fn multiple_files_with_imports_have_resolved_edges() {
163 let provider = RayonParseProvider::new();
164 let index_ts = br#"export { helper } from "./helper";"#.to_vec();
165 let helper_ts = br#"export function helper(): void {}"#.to_vec();
166 let files = vec![
167 (PathBuf::from("src/index.ts"), index_ts),
168 (PathBuf::from("src/helper.ts"), helper_ts),
169 ];
170 let result = provider
171 .parse_and_resolve(&files, Path::new("/tmp"))
172 .unwrap();
173 assert_eq!(result.len(), 2);
174 let total_edges: usize = result.iter().map(|fd| fd.edges.len()).sum();
176 assert!(total_edges > 0, "should have edges");
177 }
178}