1use super::concurrent::GraphSnapshot;
8use super::node::id::NodeId;
9use super::resolution::{
10 FileScope, ResolutionMode, SymbolCandidateOutcome, SymbolQuery, display_graph_qualified_name,
11};
12use super::storage::StringInterner;
13use super::storage::arena::NodeEntry;
14use super::storage::registry::FileRegistry;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct MaterializedNode {
22 pub node_id: NodeId,
24 pub name: String,
26 pub qualified_name: String,
28 pub kind: String,
30 pub language: String,
32 pub file_path: String,
34 pub start_line: u32,
36 pub end_line: u32,
38}
39
40#[must_use]
49pub fn display_entry_qualified_name(
50 entry: &NodeEntry,
51 strings: &StringInterner,
52 files: &FileRegistry,
53 fallback_name: &str,
54) -> String {
55 entry
56 .qualified_name
57 .and_then(|qn_id| strings.resolve(qn_id))
58 .map_or_else(
59 || fallback_name.to_string(),
60 |qualified| {
61 files.language_for_file(entry.file).map_or_else(
62 || qualified.to_string(),
63 |language| {
64 display_graph_qualified_name(
65 language,
66 qualified.as_ref(),
67 entry.kind,
68 entry.is_static,
69 )
70 },
71 )
72 },
73 )
74}
75
76#[must_use]
81pub fn find_nodes_by_name(snapshot: &GraphSnapshot, name: &str) -> Vec<NodeId> {
82 match snapshot.find_symbol_candidates(&SymbolQuery {
83 symbol: name,
84 file_scope: FileScope::Any,
85 mode: ResolutionMode::AllowSuffixCandidates,
86 }) {
87 SymbolCandidateOutcome::Candidates(matches) => matches,
88 SymbolCandidateOutcome::NotFound | SymbolCandidateOutcome::FileNotIndexed => Vec::new(),
89 }
90}
91
92#[must_use]
97pub fn collect_symbol_seeds(snapshot: &GraphSnapshot, symbols: &[String]) -> Vec<NodeId> {
98 let mut seeds: Vec<NodeId> = Vec::new();
99 for symbol in symbols {
100 seeds.extend(find_nodes_by_name(snapshot, symbol));
101 }
102 seeds.sort_unstable();
103 seeds.dedup();
104 seeds
105}
106
107#[must_use]
112pub fn qualified_node_name(snapshot: &GraphSnapshot, node_id: NodeId) -> Option<String> {
113 let strings = snapshot.strings();
114 let files = snapshot.files();
115 let entry = snapshot.get_node(node_id)?;
116
117 let name = strings
118 .resolve(entry.name)
119 .map(|value| value.to_string())
120 .unwrap_or_default();
121 let qualified_name = display_entry_qualified_name(entry, strings, files, &name);
122
123 (!qualified_name.is_empty()).then_some(qualified_name)
124}
125
126#[must_use]
132pub fn materialize_node(snapshot: &GraphSnapshot, node_id: NodeId) -> Option<MaterializedNode> {
133 let strings = snapshot.strings();
134 let files = snapshot.files();
135 let entry = snapshot.get_node(node_id)?;
136
137 let name = strings
138 .resolve(entry.name)
139 .map(|value| value.to_string())
140 .unwrap_or_default();
141
142 let qualified_name = display_entry_qualified_name(entry, strings, files, &name);
143 if qualified_name.is_empty() {
144 return None;
145 }
146
147 let kind = format!("{:?}", entry.kind).to_lowercase();
148 let language = files
149 .language_for_file(entry.file)
150 .map_or("unknown".to_string(), |lang| {
151 lang.to_string().to_ascii_lowercase()
152 });
153 let file_path = files
154 .resolve(entry.file)
155 .map(|path| path.display().to_string())
156 .unwrap_or_default();
157
158 Some(MaterializedNode {
159 node_id,
160 name,
161 qualified_name,
162 kind,
163 language,
164 file_path,
165 start_line: entry.start_line,
166 end_line: entry.end_line,
167 })
168}
169
170#[cfg(test)]
171mod tests {
172 use std::path::{Path, PathBuf};
173
174 use crate::graph::node::Language;
175 use crate::graph::unified::concurrent::CodeGraph;
176 use crate::graph::unified::node::id::NodeId;
177 use crate::graph::unified::node::kind::NodeKind;
178 use crate::graph::unified::storage::arena::NodeEntry;
179
180 use super::{
181 MaterializedNode, collect_symbol_seeds, find_nodes_by_name, materialize_node,
182 qualified_node_name,
183 };
184
185 struct TestNode {
186 node_id: NodeId,
187 }
188
189 fn abs_path(relative: &str) -> PathBuf {
190 PathBuf::from("/materialize-tests").join(relative)
191 }
192
193 trait NodeEntryExt {
194 fn with_qualified_name_opt(
195 self,
196 qualified_name: Option<crate::graph::unified::string::id::StringId>,
197 ) -> Self;
198 }
199
200 impl NodeEntryExt for NodeEntry {
201 fn with_qualified_name_opt(
202 mut self,
203 qualified_name: Option<crate::graph::unified::string::id::StringId>,
204 ) -> Self {
205 self.qualified_name = qualified_name;
206 self
207 }
208 }
209
210 fn add_node(
211 graph: &mut CodeGraph,
212 kind: NodeKind,
213 name: &str,
214 qualified_name: Option<&str>,
215 file_path: &Path,
216 language: Option<Language>,
217 start_line: u32,
218 end_line: u32,
219 ) -> TestNode {
220 let name_id = graph.strings_mut().intern(name).unwrap();
221 let qualified_name_id =
222 qualified_name.map(|value| graph.strings_mut().intern(value).unwrap());
223 let file_id = graph
224 .files_mut()
225 .register_with_language(file_path, language)
226 .unwrap();
227
228 let entry = NodeEntry::new(kind, name_id, file_id)
229 .with_qualified_name_opt(qualified_name_id)
230 .with_location(start_line, 0, end_line, 0);
231
232 let node_id = graph.nodes_mut().alloc(entry).unwrap();
233 graph
234 .indices_mut()
235 .add(node_id, kind, name_id, qualified_name_id, file_id);
236
237 TestNode { node_id }
238 }
239
240 #[test]
241 fn find_nodes_by_name_returns_matching_candidates() {
242 let mut graph = CodeGraph::new();
243 let path = abs_path("src/lib.rs");
244 let node = add_node(
245 &mut graph,
246 NodeKind::Function,
247 "process",
248 Some("crate::process"),
249 &path,
250 Some(Language::Rust),
251 1,
252 10,
253 );
254
255 let snapshot = graph.snapshot();
256 let results = find_nodes_by_name(&snapshot, "process");
257 assert!(
258 results.contains(&node.node_id),
259 "expected node_id {:?} in results {:?}",
260 node.node_id,
261 results
262 );
263 }
264
265 #[test]
266 fn find_nodes_by_name_returns_empty_for_nonexistent() {
267 let mut graph = CodeGraph::new();
268 let path = abs_path("src/lib.rs");
269 let _node = add_node(
270 &mut graph,
271 NodeKind::Function,
272 "existing",
273 Some("crate::existing"),
274 &path,
275 Some(Language::Rust),
276 1,
277 5,
278 );
279
280 let snapshot = graph.snapshot();
281 let results = find_nodes_by_name(&snapshot, "nonexistent_symbol_xyz");
282 assert!(
283 results.is_empty(),
284 "expected empty results, got {results:?}"
285 );
286 }
287
288 #[test]
289 fn collect_symbol_seeds_deduplicates() {
290 let mut graph = CodeGraph::new();
291 let path = abs_path("src/lib.rs");
292 let node = add_node(
293 &mut graph,
294 NodeKind::Function,
295 "dedup_target",
296 Some("crate::dedup_target"),
297 &path,
298 Some(Language::Rust),
299 1,
300 10,
301 );
302
303 let snapshot = graph.snapshot();
304
305 let symbols = vec!["dedup_target".to_string(), "dedup_target".to_string()];
307 let seeds = collect_symbol_seeds(&snapshot, &symbols);
308
309 assert_eq!(
310 seeds.iter().filter(|id| **id == node.node_id).count(),
311 1,
312 "expected exactly one occurrence of node_id {:?}, got seeds {:?}",
313 node.node_id,
314 seeds
315 );
316 }
317
318 #[test]
319 fn qualified_node_name_returns_language_aware_name() {
320 let mut graph = CodeGraph::new();
321 let path = abs_path("src/Program.cs");
322 let node = add_node(
323 &mut graph,
324 NodeKind::Method,
325 "GetName",
326 Some("MyApp::User::GetName"),
327 &path,
328 Some(Language::CSharp),
329 5,
330 15,
331 );
332
333 let snapshot = graph.snapshot();
334 let name = qualified_node_name(&snapshot, node.node_id);
335
336 assert_eq!(name, Some("MyApp.User.GetName".to_string()));
338 }
339
340 #[test]
341 fn materialize_node_produces_complete_node() {
342 let mut graph = CodeGraph::new();
343 let path = abs_path("src/main.rs");
344 let node = add_node(
345 &mut graph,
346 NodeKind::Function,
347 "main",
348 Some("crate::main"),
349 &path,
350 Some(Language::Rust),
351 1,
352 20,
353 );
354
355 let snapshot = graph.snapshot();
356 let materialized = materialize_node(&snapshot, node.node_id);
357
358 let expected = MaterializedNode {
359 node_id: node.node_id,
360 name: "main".to_string(),
361 qualified_name: "crate::main".to_string(),
362 kind: "function".to_string(),
363 language: "rust".to_string(),
364 file_path: path.display().to_string(),
365 start_line: 1,
366 end_line: 20,
367 };
368
369 assert_eq!(materialized, Some(expected));
370 }
371
372 #[test]
373 fn materialize_node_returns_none_for_empty_qualified_name() {
374 let mut graph = CodeGraph::new();
375 let path = abs_path("src/lib.rs");
376 let node = add_node(
379 &mut graph,
380 NodeKind::Function,
381 "",
382 None,
383 &path,
384 Some(Language::Rust),
385 1,
386 1,
387 );
388
389 let snapshot = graph.snapshot();
390 let materialized = materialize_node(&snapshot, node.node_id);
391 assert!(
392 materialized.is_none(),
393 "expected None for node with empty qualified name, got {materialized:?}"
394 );
395 }
396}