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 /// Per-language [`ShapeMapping`](crate::graph::unified::build::shape::ShapeMapping)
64 /// for the identifier-blind body-shape descriptor feature, or `None` if this
65 /// builder does not (yet) supply one.
66 ///
67 /// Resolved once per file in the build seam
68 /// ([`StagingGraph::attach_body_hashes`](crate::graph::unified::build::staging::StagingGraph::attach_body_hashes))
69 /// so the single shared `compute_shape_descriptor` walker can fingerprint each
70 /// Function/Method body alongside its body hash. The default returns `None`: a
71 /// language with no mapping simply contributes no shape descriptors, exactly as
72 /// before this feature. Each language plugin overrides this in its own crate, so
73 /// the fan-out across the 37 plugins stays parallel-safe.
74 fn shape_mapping(&self) -> Option<&dyn crate::graph::unified::build::shape::ShapeMapping> {
75 None
76 }
77
78 /// Incrementally update the graph after an edit.
79 ///
80 /// The default implementation simply rebuilds the file from scratch. Builders
81 /// that can take advantage of the `edit` can override this method to provide
82 /// faster updates.
83 ///
84 /// # Errors
85 ///
86 /// Implementations should return an error when incremental updates cannot be
87 /// applied safely (e.g., inconsistent edit ranges or graph mutation failures).
88 fn update_graph(
89 &self,
90 tree: &Tree,
91 content: &[u8],
92 file: &Path,
93 edit: &InputEdit,
94 staging: &mut StagingGraph,
95 ) -> GraphResult<()> {
96 let _ = edit;
97 self.build_graph(tree, content, file, staging)
98 }
99
100 /// Perform any cross-language edge detection that requires whole-graph context.
101 ///
102 /// Implementations can return additional edges to be merged into the graph
103 /// after all files are processed. The default implementation returns an empty
104 /// list, which is suitable for languages that do not emit cross-language edges.
105 ///
106 /// This method receives an immutable `GraphSnapshot` for read-only access,
107 /// enabling cross-file analysis that requires seeing all nodes.
108 ///
109 /// # Errors
110 ///
111 /// Return an error when inspecting the graph fails (for example, if required
112 /// nodes are missing or metadata cannot be deserialized).
113 fn detect_cross_language_edges(&self, _snapshot: &GraphSnapshot) -> GraphResult<Vec<CodeEdge>> {
114 Ok(Vec::new())
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use std::path::PathBuf;
122 use std::sync::{
123 Arc,
124 atomic::{AtomicUsize, Ordering},
125 };
126 use tree_sitter::{InputEdit, Parser, Point};
127
128 use crate::graph::node::Language;
129
130 struct TestBuilder {
131 language: Language,
132 build_calls: Arc<AtomicUsize>,
133 }
134
135 impl TestBuilder {
136 fn new(language: Language, build_calls: Arc<AtomicUsize>) -> Self {
137 Self {
138 language,
139 build_calls,
140 }
141 }
142 }
143
144 impl GraphBuilder for TestBuilder {
145 fn build_graph(
146 &self,
147 _tree: &Tree,
148 _content: &[u8],
149 _file: &Path,
150 _staging: &mut StagingGraph,
151 ) -> GraphResult<()> {
152 self.build_calls.fetch_add(1, Ordering::SeqCst);
153 Ok(())
154 }
155
156 fn language(&self) -> Language {
157 self.language
158 }
159 }
160
161 fn parse_rust_tree(source: &str) -> Tree {
162 let mut parser = Parser::new();
163 let language = tree_sitter_rust::LANGUAGE;
164 parser
165 .set_language(&language.into())
166 .expect("set Rust language");
167 parser.parse(source, None).expect("parse rust source")
168 }
169
170 fn dummy_edit() -> InputEdit {
171 InputEdit {
172 start_byte: 0,
173 old_end_byte: 0,
174 new_end_byte: 0,
175 start_position: Point { row: 0, column: 0 },
176 old_end_position: Point { row: 0, column: 0 },
177 new_end_position: Point { row: 0, column: 0 },
178 }
179 }
180
181 #[test]
182 fn test_update_graph_defaults_to_build_graph() {
183 let build_calls = Arc::new(AtomicUsize::new(0));
184 let builder = TestBuilder::new(Language::Rust, Arc::clone(&build_calls));
185 let tree = parse_rust_tree("fn main() {}");
186 let mut staging = StagingGraph::new();
187 let file = PathBuf::from("src/main.rs");
188
189 builder
190 .update_graph(
191 &tree,
192 "fn main() {}".as_bytes(),
193 &file,
194 &dummy_edit(),
195 &mut staging,
196 )
197 .expect("update_graph");
198
199 assert_eq!(build_calls.load(Ordering::SeqCst), 1);
200 }
201
202 #[test]
203 fn test_default_cross_language_edges_is_empty() {
204 use crate::graph::unified::concurrent::CodeGraph;
205
206 let build_calls = Arc::new(AtomicUsize::new(0));
207 let builder = TestBuilder::new(Language::Rust, build_calls);
208 let graph = CodeGraph::new();
209 let snapshot = graph.snapshot();
210 let edges = builder
211 .detect_cross_language_edges(&snapshot)
212 .expect("default detect");
213 assert!(edges.is_empty());
214 }
215}