Skip to main content

the_code_graph_cli/adapters/
parse.rs

1use 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        // Phase 1: parallel parse
47        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        // Phase 2: build ResolveContext
62        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        // Phase 3: resolve imports (parallel)
76        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                // Merge structural edges + resolved edges
89                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        // Check that at least some edges exist
175        let total_edges: usize = result.iter().map(|fd| fd.edges.len()).sum();
176        assert!(total_edges > 0, "should have edges");
177    }
178}