Skip to main content

brink_driver/
discover.rs

1//! BFS discovery of files reachable via INCLUDEs.
2
3use std::collections::HashSet;
4use std::io;
5
6use brink_db::{ProjectDb, resolve_include_path};
7use tracing::{debug, info};
8
9/// Errors from file discovery.
10#[derive(Debug, thiserror::Error)]
11pub enum DiscoverError {
12    /// File I/O error during discovery.
13    #[error("I/O error: {0}")]
14    Io(#[from] io::Error),
15    /// Circular INCLUDE dependency detected.
16    #[error("circular INCLUDE: {0}")]
17    CircularInclude(String),
18}
19
20/// Discover all files reachable via INCLUDEs from the entry point.
21///
22/// Performs BFS: reads each file, parses it via `db.set_file()`, then follows
23/// its INCLUDEs. After all files are loaded, rebuilds the include graph and
24/// checks for cycles.
25pub 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        // Discover INCLUDEs
41        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    // Rebuild include graph now that all files are loaded
53    db.rebuild_include_graph();
54
55    // Detect circular includes
56    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_not_normalized() {
92        // No normalization — matches ink behavior
93        assert_eq!(
94            resolve_include_path("a/b/c.ink", "../d.ink"),
95            "a/b/../d.ink"
96        );
97    }
98
99    #[test]
100    fn resolve_deep_nesting() {
101        assert_eq!(resolve_include_path("a/b/c.ink", "d/e.ink"), "a/b/d/e.ink");
102    }
103}