Skip to main content

sqry_core/graph/
builder.rs

1//! Graph builder trait.
2//!
3//! Language integrations implement this trait to populate the unified
4//! `CodeGraph` using tree-sitter ASTs.  Builders operate on mutable references
5//! to `StagingGraph` buffers, which are per-file and can be built in parallel
6//! without synchronisation. After building, staging buffers are merged into
7//! the main graph.
8//!
9//! # Architecture
10//!
11//! The build pipeline uses a transactional staging pattern:
12//! 1. Each file gets its own `StagingGraph` (thread-local)
13//! 2. Builders populate the staging buffer via mutable reference
14//! 3. After all files are processed, staging buffers are committed atomically
15//!
16//! This enables parallel builds while maintaining correctness guarantees.
17
18use std::path::Path;
19
20use tree_sitter::{InputEdit, Tree};
21
22use super::edge::CodeEdge;
23use super::error::GraphResult;
24use super::node::Language;
25use super::unified::build::staging::StagingGraph;
26use super::unified::concurrent::GraphSnapshot;
27
28/// Trait implemented by all language-specific graph builders.
29///
30/// #
31///
32/// This trait now uses the unified graph architecture with `StagingGraph` buffers
33/// for transactional builds. The staging pattern enables:
34/// - Parallel per-file building (each file gets its own staging buffer)
35/// - Atomic commits (all-or-nothing semantics)
36/// - Rollback on error (discards partial work)
37pub trait GraphBuilder: Send + Sync {
38    /// Build graph artifacts for the given file into a staging buffer.
39    ///
40    /// Builders are expected to walk the supplied `tree` (parsed from `content`)
41    /// and insert nodes/edges into `staging`.  Implementations should be idempotent
42    /// so repeated calls with the same inputs produce identical results.
43    ///
44    /// The staging buffer will be committed to the main graph after all files
45    /// in a batch are processed successfully.
46    ///
47    /// # Errors
48    ///
49    /// Implementations return an error when the AST cannot be traversed or when
50    /// extracted metadata violates graph invariants (for example, invalid spans
51    /// or malformed identifiers).
52    fn build_graph(
53        &self,
54        tree: &Tree,
55        content: &[u8],
56        file: &Path,
57        staging: &mut StagingGraph,
58    ) -> GraphResult<()>;
59
60    /// The language handled by this builder.
61    fn language(&self) -> Language;
62
63    /// Incrementally update the graph after an edit.
64    ///
65    /// The default implementation simply rebuilds the file from scratch.  Builders
66    /// that can take advantage of the `edit` can override this method to provide
67    /// faster updates.
68    ///
69    /// # Errors
70    ///
71    /// Implementations should return an error when incremental updates cannot be
72    /// applied safely (e.g., inconsistent edit ranges or graph mutation failures).
73    fn update_graph(
74        &self,
75        tree: &Tree,
76        content: &[u8],
77        file: &Path,
78        edit: &InputEdit,
79        staging: &mut StagingGraph,
80    ) -> GraphResult<()> {
81        let _ = edit;
82        self.build_graph(tree, content, file, staging)
83    }
84
85    /// Perform any cross-language edge detection that requires whole-graph context.
86    ///
87    /// Implementations can return additional edges to be merged into the graph
88    /// after all files are processed.  The default implementation returns an empty
89    /// list, which is suitable for languages that do not emit cross-language edges.
90    ///
91    /// This method receives an immutable `GraphSnapshot` for read-only access,
92    /// enabling cross-file analysis that requires seeing all nodes.
93    ///
94    /// # Errors
95    ///
96    /// Return an error when inspecting the graph fails (for example, if required
97    /// nodes are missing or metadata cannot be deserialized).
98    fn detect_cross_language_edges(&self, _snapshot: &GraphSnapshot) -> GraphResult<Vec<CodeEdge>> {
99        Ok(Vec::new())
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use std::path::PathBuf;
107    use std::sync::{
108        Arc,
109        atomic::{AtomicUsize, Ordering},
110    };
111    use tree_sitter::{InputEdit, Parser, Point};
112
113    use crate::graph::node::Language;
114
115    struct TestBuilder {
116        language: Language,
117        build_calls: Arc<AtomicUsize>,
118    }
119
120    impl TestBuilder {
121        fn new(language: Language, build_calls: Arc<AtomicUsize>) -> Self {
122            Self {
123                language,
124                build_calls,
125            }
126        }
127    }
128
129    impl GraphBuilder for TestBuilder {
130        fn build_graph(
131            &self,
132            _tree: &Tree,
133            _content: &[u8],
134            _file: &Path,
135            _staging: &mut StagingGraph,
136        ) -> GraphResult<()> {
137            self.build_calls.fetch_add(1, Ordering::SeqCst);
138            Ok(())
139        }
140
141        fn language(&self) -> Language {
142            self.language
143        }
144    }
145
146    fn parse_rust_tree(source: &str) -> Tree {
147        let mut parser = Parser::new();
148        let language = tree_sitter_rust::LANGUAGE;
149        parser
150            .set_language(&language.into())
151            .expect("set Rust language");
152        parser.parse(source, None).expect("parse rust source")
153    }
154
155    fn dummy_edit() -> InputEdit {
156        InputEdit {
157            start_byte: 0,
158            old_end_byte: 0,
159            new_end_byte: 0,
160            start_position: Point { row: 0, column: 0 },
161            old_end_position: Point { row: 0, column: 0 },
162            new_end_position: Point { row: 0, column: 0 },
163        }
164    }
165
166    #[test]
167    fn test_update_graph_defaults_to_build_graph() {
168        let build_calls = Arc::new(AtomicUsize::new(0));
169        let builder = TestBuilder::new(Language::Rust, Arc::clone(&build_calls));
170        let tree = parse_rust_tree("fn main() {}");
171        let mut staging = StagingGraph::new();
172        let file = PathBuf::from("src/main.rs");
173
174        builder
175            .update_graph(
176                &tree,
177                "fn main() {}".as_bytes(),
178                &file,
179                &dummy_edit(),
180                &mut staging,
181            )
182            .expect("update_graph");
183
184        assert_eq!(build_calls.load(Ordering::SeqCst), 1);
185    }
186
187    #[test]
188    fn test_default_cross_language_edges_is_empty() {
189        use crate::graph::unified::concurrent::CodeGraph;
190
191        let build_calls = Arc::new(AtomicUsize::new(0));
192        let builder = TestBuilder::new(Language::Rust, build_calls);
193        let graph = CodeGraph::new();
194        let snapshot = graph.snapshot();
195        let edges = builder
196            .detect_cross_language_edges(&snapshot)
197            .expect("default detect");
198        assert!(edges.is_empty());
199    }
200}