1use std::collections::HashSet;
4use std::io;
5
6use brink_db::{ProjectDb, resolve_include_path};
7use tracing::{debug, info};
8
9#[derive(Debug, thiserror::Error)]
11pub enum DiscoverError {
12 #[error("I/O error: {0}")]
14 Io(#[from] io::Error),
15 #[error("circular INCLUDE: {0}")]
17 CircularInclude(String),
18}
19
20pub fn discover<F>(db: &mut ProjectDb, entry: &str, read_file: &mut F) -> Result<(), DiscoverError>
26where
27 F: FnMut(&str) -> Result<String, io::Error>,
28{
29 let mut queue: Vec<String> = vec![entry.to_string()];
30 let mut seen: HashSet<String> = HashSet::new();
31
32 while let Some(path) = queue.pop() {
33 if !seen.insert(path.clone()) {
34 continue;
35 }
36
37 let source = read_file(&path)?;
38 let file_id = db.set_file(&path, source);
39
40 if let Some(hir) = db.hir(file_id) {
42 for include in &hir.includes {
43 let resolved = resolve_include_path(&path, &include.file_path);
44 if !seen.contains(&resolved) {
45 debug!(from = path, include = resolved, "discovered INCLUDE");
46 queue.push(resolved);
47 }
48 }
49 }
50 }
51
52 db.rebuild_include_graph();
54
55 if let Some(cycle) = db.find_cycle() {
57 let names: Vec<_> = cycle.iter().filter_map(|id| db.file_path(*id)).collect();
58 return Err(DiscoverError::CircularInclude(names.join(" -> ")));
59 }
60
61 info!(files = seen.len(), "discovery complete");
62 Ok(())
63}
64
65#[cfg(test)]
66mod tests {
67 use brink_db::resolve_include_path;
68
69 #[test]
70 fn resolve_relative_include() {
71 assert_eq!(
72 resolve_include_path("src/main.ink", "utils.ink"),
73 "src/utils.ink"
74 );
75 }
76
77 #[test]
78 fn resolve_no_directory() {
79 assert_eq!(resolve_include_path("story.ink", "other.ink"), "other.ink");
80 }
81
82 #[test]
83 fn resolve_nested_directory() {
84 assert_eq!(
85 resolve_include_path("story.ink", "lib/helpers.ink"),
86 "lib/helpers.ink"
87 );
88 }
89
90 #[test]
91 fn resolve_parent_traversal_normalized() {
92 assert_eq!(resolve_include_path("a/b/c.ink", "../d.ink"), "a/d.ink");
95 }
96
97 #[test]
98 fn resolve_deep_nesting() {
99 assert_eq!(resolve_include_path("a/b/c.ink", "d/e.ink"), "a/b/d/e.ink");
100 }
101}