1use crate::block::{BlockKind, BlockRelation};
2use crate::graph_builder::{GraphNode, ProjectGraph};
3
4#[derive(Debug, Clone)]
13pub struct GraphBlockInfo {
14 pub name: String,
15 pub kind: String,
16 pub file_path: Option<String>,
17 pub source_code: Option<String>,
18 pub node: GraphNode,
19 pub unit_index: usize,
20 pub start_line: usize,
21 pub end_line: usize,
22}
23
24impl GraphBlockInfo {
25 pub fn format_for_llm(&self) -> String {
26 let mut output = String::new();
27
28 let location = if let Some(path) = &self.file_path {
30 use std::path::Path;
31 let abs_path = Path::new(path);
32 if abs_path.is_absolute() {
33 path.clone()
34 } else {
35 match std::env::current_dir() {
36 Ok(cwd) => cwd.join(path).display().to_string(),
37 Err(_) => path.clone(),
38 }
39 }
40 } else {
41 format!("<file_unit_{}>", self.unit_index)
42 };
43
44 output.push_str(&format!(
45 "┌─ {} [{}] at {}\n",
46 self.name, self.kind, location
47 ));
48
49 if let Some(source) = &self.source_code {
51 let lines: Vec<&str> = source.lines().collect();
52 let max_line_num = self.end_line;
53 let line_num_width = max_line_num.to_string().len();
54
55 for (idx, line) in lines.iter().enumerate() {
56 let line_num = self.start_line + idx;
57 output.push_str(&format!(
58 "│ [{:width$}] {}\n",
59 line_num,
60 line,
61 width = line_num_width
62 ));
63 }
64 }
65
66 output.push_str("└─\n");
67 output
68 }
69}
70
71#[derive(Debug, Default)]
73pub struct QueryResult {
74 pub primary: Vec<GraphBlockInfo>,
75 pub depends: Vec<GraphBlockInfo>,
76 pub depended: Vec<GraphBlockInfo>,
77}
78
79impl QueryResult {
80 pub fn format_for_llm(&self) -> String {
81 let mut output = String::new();
82
83 if !self.primary.is_empty() {
84 output.push_str(" ------------- PRIMARY RESULTS ------------------- \n");
85 for block in &self.primary {
86 output.push_str(&block.format_for_llm());
87 output.push('\n');
88 }
89 }
90
91 if !self.depends.is_empty() {
92 output.push_str(" -------------- DEPENDS ON (Dependencies) ----------------- \n");
93 for block in &self.depends {
94 output.push_str(&block.format_for_llm());
95 output.push('\n');
96 }
97 }
98
99 if !self.depended.is_empty() {
100 output.push_str(" -------------- DEPENDED BY (Dependents) ----------------- \n");
101 for block in &self.depended {
102 output.push_str(&block.format_for_llm());
103 output.push('\n');
104 }
105 }
106
107 output
108 }
109}
110
111pub struct ProjectQuery<'tcx> {
113 graph: &'tcx ProjectGraph<'tcx>,
114}
115
116impl<'tcx> ProjectQuery<'tcx> {
117 pub fn new(graph: &'tcx ProjectGraph<'tcx>) -> Self {
118 Self { graph }
119 }
120
121 pub fn find_by_name(&self, name: &str) -> QueryResult {
123 let mut result = QueryResult::default();
124
125 let blocks = self.graph.blocks_by_name(name);
126 for node in blocks {
127 if let Some(block_info) = self.node_to_block_info(node) {
128 result.primary.push(block_info);
129 }
130 }
131
132 result
133 }
134
135 pub fn find_all_functions(&self) -> QueryResult {
137 self.find_by_kind(BlockKind::Func)
138 }
139
140 pub fn find_all_structs(&self) -> QueryResult {
142 self.find_by_kind(BlockKind::Class)
143 }
144
145 pub fn find_by_kind(&self, kind: BlockKind) -> QueryResult {
147 let mut result = QueryResult::default();
148
149 let blocks = self.graph.blocks_by_kind(kind);
150 for node in blocks {
151 if let Some(block_info) = self.node_to_block_info(node) {
152 result.primary.push(block_info.clone());
153 }
154 }
155
156 result
157 }
158
159 pub fn file_structure(&self, unit_index: usize) -> QueryResult {
161 let mut result = QueryResult::default();
162
163 let blocks = self.graph.blocks_in(unit_index);
164 for node in blocks {
165 if let Some(block_info) = self.node_to_block_info(node) {
166 result.primary.push(block_info.clone());
167 }
168 }
169
170 result
171 }
172
173 pub fn find_depends(&self, name: &str) -> QueryResult {
175 let mut result = QueryResult::default();
176
177 if let Some(primary_node) = self.graph.block_by_name(name) {
179 if let Some(block_info) = self.node_to_block_info(primary_node) {
180 result.primary.push(block_info);
181
182 let depends_blocks = self
184 .graph
185 .find_related_blocks(primary_node, vec![BlockRelation::DependsOn]);
186 for depends_node in depends_blocks {
187 if let Some(depends_info) = self.node_to_block_info(depends_node) {
188 result.depends.push(depends_info);
189 }
190 }
191 }
192 }
193
194 result
195 }
196
197 pub fn find_depended(&self, name: &str) -> QueryResult {
199 let mut result = QueryResult::default();
200
201 if let Some(primary_node) = self.graph.block_by_name(name) {
203 if let Some(block_info) = self.node_to_block_info(primary_node) {
204 result.primary.push(block_info);
205
206 let depended_blocks = self
208 .graph
209 .find_related_blocks(primary_node, vec![BlockRelation::DependedBy]);
210 for depended_node in depended_blocks {
211 if let Some(depended_info) = self.node_to_block_info(depended_node) {
212 result.depended.push(depended_info);
213 }
214 }
215 }
216 }
217
218 result
219 }
220
221 pub fn find_depends_recursive(&self, name: &str) -> QueryResult {
223 let mut result = QueryResult::default();
224
225 if let Some(primary_node) = self.graph.block_by_name(name) {
227 if let Some(block_info) = self.node_to_block_info(primary_node) {
228 result.primary.push(block_info);
229
230 let all_related = self.graph.find_dpends_blocks_recursive(primary_node);
232 for related_node in all_related {
233 if let Some(related_info) = self.node_to_block_info(related_node) {
234 result.depends.push(related_info);
235 }
236 }
237 }
238 }
239
240 result
241 }
242
243 pub fn traverse_bfs(&self, start_name: &str) -> Vec<GraphBlockInfo> {
245 let mut results = Vec::new();
246
247 if let Some(start_node) = self.graph.block_by_name(start_name) {
248 self.graph.traverse_bfs(start_node, |node| {
249 if let Some(block_info) = self.node_to_block_info(node) {
250 results.push(block_info);
251 }
252 });
253 }
254
255 results
256 }
257
258 pub fn traverse_dfs(&self, start_name: &str) -> Vec<GraphBlockInfo> {
260 let mut results = Vec::new();
261
262 if let Some(start_node) = self.graph.block_by_name(start_name) {
263 self.graph.traverse_dfs(start_node, |node| {
264 if let Some(block_info) = self.node_to_block_info(node) {
265 results.push(block_info);
266 }
267 });
268 }
269
270 results
271 }
272
273 pub fn find_by_kind_in_unit(&self, kind: BlockKind, unit_index: usize) -> QueryResult {
275 let mut result = QueryResult::default();
276
277 let blocks = self.graph.blocks_by_kind_in(kind, unit_index);
278 for node in blocks {
279 if let Some(block_info) = self.node_to_block_info(node) {
280 result.primary.push(block_info.clone());
281 }
282 }
283
284 result
285 }
286
287 fn node_to_block_info(&self, node: GraphNode) -> Option<GraphBlockInfo> {
289 let (unit_index, name, kind) = self.graph.block_info(node.block_id)?;
290
291 let display_name =
293 if let Some(symbol) = self.graph.cc.find_symbol_by_block_id(node.block_id) {
294 if let Some(ref base_name) = name {
295 base_name.clone()
296 } else {
297 let fqn = symbol.fqn_name.borrow();
298 if !fqn.is_empty() {
299 fqn.clone()
300 } else {
301 symbol.name.as_str().to_string()
302 }
303 }
304 } else {
305 name.unwrap_or_else(|| format!("_unnamed_{}", node.block_id.0))
306 };
307
308 let file_path = self
310 .graph
311 .cc
312 .files
313 .get(unit_index)
314 .and_then(|file| file.path().map(|s| s.to_string()));
315
316 let source_code = self.get_block_source_code(node, unit_index);
318
319 let (start_line, end_line) = self.get_line_numbers(node, unit_index);
321
322 Some(GraphBlockInfo {
323 name: display_name,
324 kind: format!("{:?}", kind),
325 file_path,
326 source_code,
327 node,
328 unit_index,
329 start_line,
330 end_line,
331 })
332 }
333
334 fn get_line_numbers(&self, node: GraphNode, unit_index: usize) -> (usize, usize) {
336 let file = match self.graph.cc.files.get(unit_index) {
337 Some(f) => f,
338 None => return (0, 0),
339 };
340
341 let unit = self.graph.cc.compile_unit(unit_index);
342
343 let bb = match unit.opt_bb(node.block_id) {
345 Some(b) => b,
346 None => return (0, 0),
347 };
348
349 let base = match bb.base() {
351 Some(b) => b,
352 None => return (0, 0),
353 };
354
355 let hir_node = base.node;
356 let start_byte = hir_node.start_byte();
357 let end_byte = hir_node.end_byte();
358
359 if let Some(content) = file.file.content.as_ref() {
361 let start_line = content[..start_byte.min(content.len())]
362 .iter()
363 .filter(|&&b| b == b'\n')
364 .count()
365 + 1;
366 let end_line = content[..end_byte.min(content.len())]
367 .iter()
368 .filter(|&&b| b == b'\n')
369 .count()
370 + 1;
371 (start_line, end_line)
372 } else {
373 (0, 0)
374 }
375 }
376
377 fn get_block_source_code(&self, node: GraphNode, unit_index: usize) -> Option<String> {
379 let file = self.graph.cc.files.get(unit_index)?;
380 let unit = self.graph.cc.compile_unit(unit_index);
381
382 let bb = unit.opt_bb(node.block_id)?;
384
385 let base = bb.base()?;
387 let hir_node = base.node;
388
389 let start_byte = hir_node.start_byte();
391 let end_byte = hir_node.end_byte();
392
393 if let crate::block::BasicBlock::Class(class_block) = bb {
395 return self.extract_class_definition(class_block, unit, file, start_byte, end_byte);
396 }
397
398 file.opt_get_text(start_byte, end_byte)
399 }
400
401 fn extract_class_definition(
404 &self,
405 class_block: &crate::block::BlockClass,
406 unit: crate::context::CompileUnit,
407 file: &crate::file::File,
408 class_start_byte: usize,
409 class_end_byte: usize,
410 ) -> Option<String> {
411 let full_text = file.opt_get_text(class_start_byte, class_end_byte)?;
413
414 let mut method_start_bytes = Vec::new();
416
417 for child_id in &class_block.base.children {
418 let child_bb = unit.opt_bb(*child_id)?;
419 let child_kind = child_bb.kind();
420
421 if child_kind == BlockKind::Func {
423 if let Some(child_base) = child_bb.base() {
424 let child_node = child_base.node;
425 let child_start = child_node.start_byte();
426 if child_start > class_start_byte {
427 method_start_bytes.push(child_start);
428 }
429 }
430 }
431 }
432
433 if method_start_bytes.is_empty() {
435 return Some(full_text);
436 }
437
438 method_start_bytes.sort();
440 let first_method_start = method_start_bytes[0];
441
442 let offset = first_method_start - class_start_byte;
444 if offset >= full_text.len() {
445 return Some(full_text);
446 }
447
448 let class_def = full_text[..offset].to_string();
450
451 let trimmed = class_def.trim_end();
453
454 if let Some(last_newline) = trimmed.rfind('\n') {
456 Some(trimmed[..=last_newline].to_string())
457 } else {
458 Some(trimmed.to_string())
459 }
460 }
461}