llmcc_core/
query.rs

1use crate::block::{BlockKind, BlockRelation};
2use crate::graph_builder::{GraphNode, ProjectGraph};
3
4/// Query API for semantic code questions built on top of ProjectGraph:
5/// - given a function name, find all related code
6/// - given a struct name, find all related code
7/// - given a module/folder, find related modules
8/// - given a file name, extract important structures (functions, types, etc.)
9///
10/// Output format: plain text suitable for LLM ingestion
11/// Represents a semantic code block from the project graph
12#[derive(Debug, Clone)]
13pub struct GraphBlockInfo {
14    pub name: String,
15    pub qualified_name: Option<String>,
16    pub kind: String,
17    pub file_path: Option<String>,
18    pub source_code: Option<String>,
19    pub node: GraphNode,
20    pub unit_index: usize,
21    pub start_line: usize,
22    pub end_line: usize,
23}
24
25impl GraphBlockInfo {
26    fn resolved_location(&self) -> String {
27        use std::env;
28        use std::path::Path;
29
30        if let Some(path) = &self.file_path {
31            let candidate = Path::new(path);
32            if candidate.is_absolute() {
33                candidate.display().to_string()
34            } else if let Ok(cwd) = env::current_dir() {
35                cwd.join(candidate).display().to_string()
36            } else {
37                path.clone()
38            }
39        } else {
40            format!("<file_unit_{}>", self.unit_index)
41        }
42    }
43
44    pub fn format_for_llm(&self) -> String {
45        let mut output = String::new();
46
47        // Header line with name, kind, and location
48        let location = self.resolved_location();
49
50        output.push_str(&format!(
51            "┌─ {} [{}] at {}\n",
52            self.name, self.kind, location
53        ));
54
55        if let Some(fqn) = &self.qualified_name {
56            if fqn != &self.name {
57                output.push_str(&format!("│    aka {}\n", fqn));
58            }
59        }
60
61        // Source code with line numbers
62        if let Some(source) = &self.source_code {
63            let lines: Vec<&str> = source.lines().collect();
64            let max_line_num = self.end_line;
65            let line_num_width = max_line_num.to_string().len();
66
67            for (idx, line) in lines.iter().enumerate() {
68                let line_num = self.start_line + idx;
69                output.push_str(&format!(
70                    "│ [{:width$}] {}\n",
71                    line_num,
72                    line,
73                    width = line_num_width
74                ));
75            }
76        }
77
78        output.push_str("└─\n");
79        output
80    }
81
82    pub fn format_summary(&self) -> String {
83        let display_name = self
84            .qualified_name
85            .as_ref()
86            .filter(|name| !name.is_empty())
87            .cloned()
88            .unwrap_or_else(|| self.name.clone());
89
90        let location = self.resolved_location();
91
92        format!(
93            "{} @ {}:{}-{}",
94            display_name, location, self.start_line, self.end_line
95        )
96    }
97}
98
99/// Query results grouped by relevance and type
100#[derive(Debug, Default)]
101pub struct QueryResult {
102    pub primary: Vec<GraphBlockInfo>,
103    pub depends: Vec<GraphBlockInfo>,
104    pub depended: Vec<GraphBlockInfo>,
105}
106
107impl QueryResult {
108    pub fn format_for_llm(&self) -> String {
109        let mut output = String::new();
110
111        if !self.primary.is_empty() {
112            output.push_str(" ------------- ASK SYMBOL ------------------- \n");
113            for block in &self.primary {
114                output.push_str(&block.format_for_llm());
115                output.push('\n');
116            }
117        }
118
119        if !self.depends.is_empty() {
120            output.push_str(" -------------- DEPENDS ON (Dependencies) ----------------- \n");
121            for block in &self.depends {
122                output.push_str(&block.format_for_llm());
123                output.push('\n');
124            }
125        }
126
127        if !self.depended.is_empty() {
128            output.push_str(" -------------- DEPENDED BY (Dependents) ----------------- \n");
129            for block in &self.depended {
130                output.push_str(&block.format_for_llm());
131                output.push('\n');
132            }
133        }
134
135        output
136    }
137
138    pub fn format_summary(&self) -> String {
139        fn push_section(output: &mut String, title: &str, blocks: &[GraphBlockInfo]) {
140            if blocks.is_empty() {
141                return;
142            }
143            if !output.is_empty() {
144                output.push('\n');
145            }
146            output.push_str(title);
147            output.push('\n');
148            for block in blocks {
149                output.push_str("  - ");
150                output.push_str(&block.format_summary());
151                output.push('\n');
152            }
153        }
154
155        let mut output = String::new();
156        push_section(&mut output, "SYMBOL:", &self.primary);
157        push_section(&mut output, "DEPENDS:", &self.depends);
158        push_section(&mut output, "DEPENDENTS:", &self.depended);
159        while output.ends_with('\n') {
160            output.pop();
161        }
162        output
163    }
164}
165
166/// Main query interface built on ProjectGraph
167pub struct ProjectQuery<'tcx> {
168    graph: &'tcx ProjectGraph<'tcx>,
169}
170
171impl<'tcx> ProjectQuery<'tcx> {
172    pub fn new(graph: &'tcx ProjectGraph<'tcx>) -> Self {
173        Self { graph }
174    }
175
176    /// Find all blocks with a given name
177    pub fn find_by_name(&self, name: &str) -> QueryResult {
178        let mut result = QueryResult::default();
179
180        let blocks = self.graph.blocks_by_name(name);
181        for node in blocks {
182            if let Some(block_info) = self.node_to_block_info(node) {
183                result.primary.push(block_info);
184            }
185        }
186
187        result
188    }
189
190    /// Find all functions in the project
191    pub fn find_all_functions(&self) -> QueryResult {
192        self.find_by_kind(BlockKind::Func)
193    }
194
195    /// Find all structs in the project
196    pub fn find_all_structs(&self) -> QueryResult {
197        self.find_by_kind(BlockKind::Class)
198    }
199
200    /// Find all items of a specific kind
201    pub fn find_by_kind(&self, kind: BlockKind) -> QueryResult {
202        let mut result = QueryResult::default();
203
204        let blocks = self.graph.blocks_by_kind(kind);
205        for node in blocks {
206            if let Some(block_info) = self.node_to_block_info(node) {
207                result.primary.push(block_info.clone());
208            }
209        }
210
211        result
212    }
213
214    /// Get all blocks defined in a specific file/unit
215    pub fn file_structure(&self, unit_index: usize) -> QueryResult {
216        let mut result = QueryResult::default();
217
218        let blocks = self.graph.blocks_in(unit_index);
219        for node in blocks {
220            if let Some(block_info) = self.node_to_block_info(node) {
221                result.primary.push(block_info.clone());
222            }
223        }
224
225        result
226    }
227
228    /// Find all blocks that this block depends on
229    pub fn find_depends(&self, name: &str) -> QueryResult {
230        let mut result = QueryResult::default();
231
232        // Find the primary block
233        if let Some(primary_node) = self.graph.block_by_name(name) {
234            if let Some(block_info) = self.node_to_block_info(primary_node) {
235                result.primary.push(block_info);
236
237                // Find all blocks this one depends on
238                let depends_blocks = self
239                    .graph
240                    .find_related_blocks(primary_node, vec![BlockRelation::DependsOn]);
241                for depends_node in depends_blocks {
242                    if let Some(depends_info) = self.node_to_block_info(depends_node) {
243                        result.depends.push(depends_info);
244                    }
245                }
246            }
247        }
248
249        result
250    }
251
252    /// Find all blocks that depend on this block (dependents)
253    pub fn find_depended(&self, name: &str) -> QueryResult {
254        let mut result = QueryResult::default();
255
256        // Find the primary block
257        if let Some(primary_node) = self.graph.block_by_name(name) {
258            if let Some(block_info) = self.node_to_block_info(primary_node) {
259                result.primary.push(block_info);
260
261                // Find all blocks that depend on this one
262                let depended_blocks = self
263                    .graph
264                    .find_related_blocks(primary_node, vec![BlockRelation::DependedBy]);
265                for depended_node in depended_blocks {
266                    if let Some(depended_info) = self.node_to_block_info(depended_node) {
267                        result.depended.push(depended_info);
268                    }
269                }
270            }
271        }
272
273        result
274    }
275
276    /// Find all blocks that are related to a given block recursively
277    pub fn find_depends_recursive(&self, name: &str) -> QueryResult {
278        let mut result = QueryResult::default();
279
280        // Find the primary block
281        if let Some(primary_node) = self.graph.block_by_name(name) {
282            if let Some(block_info) = self.node_to_block_info(primary_node) {
283                result.primary.push(block_info);
284
285                // Find all related blocks recursively
286                let all_related = self.graph.find_dpends_blocks_recursive(primary_node);
287                for related_node in all_related {
288                    if let Some(related_info) = self.node_to_block_info(related_node) {
289                        result.depends.push(related_info);
290                    }
291                }
292            }
293        }
294
295        result
296    }
297
298    /// Find all blocks that depend on a given block recursively
299    pub fn find_depended_recursive(&self, name: &str) -> QueryResult {
300        let mut result = QueryResult::default();
301
302        if let Some(primary_node) = self.graph.block_by_name(name) {
303            if let Some(block_info) = self.node_to_block_info(primary_node) {
304                result.primary.push(block_info);
305
306                let all_related = self.graph.find_depended_blocks_recursive(primary_node);
307                for related_node in all_related {
308                    if let Some(related_info) = self.node_to_block_info(related_node) {
309                        result.depended.push(related_info);
310                    }
311                }
312            }
313        }
314
315        result
316    }
317
318    /// Traverse graph with BFS from a starting block
319    pub fn traverse_bfs(&self, start_name: &str) -> Vec<GraphBlockInfo> {
320        let mut results = Vec::new();
321
322        if let Some(start_node) = self.graph.block_by_name(start_name) {
323            self.graph.traverse_bfs(start_node, |node| {
324                if let Some(block_info) = self.node_to_block_info(node) {
325                    results.push(block_info);
326                }
327            });
328        }
329
330        results
331    }
332
333    /// Traverse graph with DFS from a starting block
334    pub fn traverse_dfs(&self, start_name: &str) -> Vec<GraphBlockInfo> {
335        let mut results = Vec::new();
336
337        if let Some(start_node) = self.graph.block_by_name(start_name) {
338            self.graph.traverse_dfs(start_node, |node| {
339                if let Some(block_info) = self.node_to_block_info(node) {
340                    results.push(block_info);
341                }
342            });
343        }
344
345        results
346    }
347
348    /// Find all blocks by kind in a specific unit
349    pub fn find_by_kind_in_unit(&self, kind: BlockKind, unit_index: usize) -> QueryResult {
350        let mut result = QueryResult::default();
351
352        let blocks = self.graph.blocks_by_kind_in(kind, unit_index);
353        for node in blocks {
354            if let Some(block_info) = self.node_to_block_info(node) {
355                result.primary.push(block_info.clone());
356            }
357        }
358
359        result
360    }
361
362    /// Helper: convert a GraphNode to block info
363    fn node_to_block_info(&self, node: GraphNode) -> Option<GraphBlockInfo> {
364        let (unit_index, name, kind) = self.graph.block_info(node.block_id)?;
365
366        // Try to get the fully qualified name from the symbol if available
367        let (display_name, qualified_name) =
368            if let Some(symbol) = self.graph.cc.find_symbol_by_block_id(node.block_id) {
369                let fallback = name
370                    .clone()
371                    .unwrap_or_else(|| format!("_unnamed_{}", node.block_id.0));
372                let base_name = if symbol.name.is_empty() {
373                    fallback
374                } else {
375                    symbol.name.clone()
376                };
377
378                let fqn = symbol.fqn_name.borrow().clone();
379                let qualified = if !fqn.is_empty() && fqn != base_name {
380                    Some(fqn)
381                } else {
382                    None
383                };
384
385                (base_name, qualified)
386            } else {
387                (
388                    name.unwrap_or_else(|| format!("_unnamed_{}", node.block_id.0)),
389                    None,
390                )
391            };
392
393        // Get file path from compile context
394        let file_path = self
395            .graph
396            .cc
397            .files
398            .get(unit_index)
399            .and_then(|file| file.path().map(|s| s.to_string()));
400
401        // Extract source code for this block
402        let source_code = self.get_block_source_code(node, unit_index);
403
404        // Calculate line numbers
405        let (start_line, end_line) = self.get_line_numbers(node, unit_index);
406
407        Some(GraphBlockInfo {
408            name: display_name,
409            qualified_name,
410            kind: format!("{:?}", kind),
411            file_path,
412            source_code,
413            node,
414            unit_index,
415            start_line,
416            end_line,
417        })
418    }
419
420    /// Calculate line numbers from byte offsets
421    fn get_line_numbers(&self, node: GraphNode, unit_index: usize) -> (usize, usize) {
422        let file = match self.graph.cc.files.get(unit_index) {
423            Some(f) => f,
424            None => return (0, 0),
425        };
426
427        let unit = self.graph.cc.compile_unit(unit_index);
428
429        // Get the BasicBlock to access its HIR node
430        let bb = match unit.opt_bb(node.block_id) {
431            Some(b) => b,
432            None => return (0, 0),
433        };
434
435        // Get the base which contains the HirNode
436        let base = match bb.base() {
437            Some(b) => b,
438            None => return (0, 0),
439        };
440
441        let hir_node = base.node;
442        let start_byte = hir_node.start_byte();
443        let end_byte = hir_node.end_byte();
444
445        // Get the file content and count lines
446        if let Some(content) = file.file.content.as_ref() {
447            let start_line = content[..start_byte.min(content.len())]
448                .iter()
449                .filter(|&&b| b == b'\n')
450                .count()
451                + 1;
452            let end_line = content[..end_byte.min(content.len())]
453                .iter()
454                .filter(|&&b| b == b'\n')
455                .count()
456                + 1;
457            (start_line, end_line)
458        } else {
459            (0, 0)
460        }
461    }
462
463    /// Extract the source code for a given block
464    fn get_block_source_code(&self, node: GraphNode, unit_index: usize) -> Option<String> {
465        let file = self.graph.cc.files.get(unit_index)?;
466        let unit = self.graph.cc.compile_unit(unit_index);
467
468        // Get the BasicBlock to access its HIR node
469        let bb = unit.opt_bb(node.block_id)?;
470
471        // Get the base which contains the HirNode
472        let base = bb.base()?;
473        let hir_node = base.node;
474
475        // Get the span information from the HirNode
476        let start_byte = hir_node.start_byte();
477        let end_byte = hir_node.end_byte();
478
479        // Special handling for Class blocks: filter out method implementations
480        if let crate::block::BasicBlock::Class(class_block) = bb {
481            return self.extract_class_definition(class_block, unit, file, start_byte, end_byte);
482        }
483
484        file.opt_get_text(start_byte, end_byte)
485    }
486
487    /// Extract only the class definition without method implementations
488    /// This handles classes where methods are defined inside the class body (Python-style)
489    fn extract_class_definition(
490        &self,
491        class_block: &crate::block::BlockClass,
492        unit: crate::context::CompileUnit,
493        file: &crate::file::File,
494        class_start_byte: usize,
495        class_end_byte: usize,
496    ) -> Option<String> {
497        // Get the full class source
498        let full_text = file.opt_get_text(class_start_byte, class_end_byte)?;
499
500        // Collect byte positions of all methods (BlockKind::Func children)
501        let mut method_start_bytes = Vec::new();
502
503        for child_id in &class_block.base.children {
504            let child_bb = unit.opt_bb(*child_id)?;
505            let child_kind = child_bb.kind();
506
507            // If this child is a method (Func block), record its start byte
508            if child_kind == BlockKind::Func {
509                if let Some(child_base) = child_bb.base() {
510                    let child_node = child_base.node;
511                    let child_start = child_node.start_byte();
512                    if child_start > class_start_byte {
513                        method_start_bytes.push(child_start);
514                    }
515                }
516            }
517        }
518
519        // If there are no methods, return the full text
520        if method_start_bytes.is_empty() {
521            return Some(full_text);
522        }
523
524        // Find the byte position of the first method
525        method_start_bytes.sort();
526        let first_method_start = method_start_bytes[0];
527
528        // Calculate the offset relative to class start
529        let offset = first_method_start - class_start_byte;
530        if offset >= full_text.len() {
531            return Some(full_text);
532        }
533
534        // Extract text up to the first method
535        let class_def = full_text[..offset].to_string();
536
537        // Clean up trailing whitespace and incomplete lines
538        let trimmed = class_def.trim_end();
539
540        // Try to find the last complete line (before the first method starts)
541        if let Some(last_newline) = trimmed.rfind('\n') {
542            Some(trimmed[..=last_newline].to_string())
543        } else {
544            Some(trimmed.to_string())
545        }
546    }
547}