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    /// 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}