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}