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 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        // Header line with name, kind, and location
29        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        // Source code with line numbers
50        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/// Query results grouped by relevance and type
72#[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
111/// Main query interface built on ProjectGraph
112pub 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    /// Find all blocks with a given name
122    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    /// Find all functions in the project
136    pub fn find_all_functions(&self) -> QueryResult {
137        self.find_by_kind(BlockKind::Func)
138    }
139
140    /// Find all structs in the project
141    pub fn find_all_structs(&self) -> QueryResult {
142        self.find_by_kind(BlockKind::Class)
143    }
144
145    /// Find all items of a specific kind
146    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    /// Get all blocks defined in a specific file/unit
160    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    /// Find all blocks that this block depends on
174    pub fn find_depends(&self, name: &str) -> QueryResult {
175        let mut result = QueryResult::default();
176
177        // Find the primary block
178        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                // Find all blocks this one depends on
183                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    /// Find all blocks that depend on this block (dependents)
198    pub fn find_depended(&self, name: &str) -> QueryResult {
199        let mut result = QueryResult::default();
200
201        // Find the primary block
202        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                // Find all blocks that depend on this one
207                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    /// Find all blocks that are related to a given block recursively
222    pub fn find_depends_recursive(&self, name: &str) -> QueryResult {
223        let mut result = QueryResult::default();
224
225        // Find the primary block
226        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                // Find all related blocks recursively
231                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    /// Traverse graph with BFS from a starting block
244    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    /// Traverse graph with DFS from a starting block
259    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    /// Find all blocks by kind in a specific unit
274    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    /// Helper: convert a GraphNode to block info
288    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        // Try to get the fully qualified name from the symbol if available
292        let display_name =
293            if let Some(symbol) = self.graph.cc.find_symbol_by_block_id(node.block_id) {
294                let fqn = symbol.fqn_name.borrow();
295                if !fqn.is_empty() && *fqn != symbol.name {
296                    fqn.clone()
297                } else {
298                    name.unwrap_or_else(|| format!("_unnamed_{}", node.block_id.0))
299                }
300            } else {
301                name.unwrap_or_else(|| format!("_unnamed_{}", node.block_id.0))
302            };
303
304        // Get file path from compile context
305        let file_path = self
306            .graph
307            .cc
308            .files
309            .get(unit_index)
310            .and_then(|file| file.path().map(|s| s.to_string()));
311
312        // Extract source code for this block
313        let source_code = self.get_block_source_code(node, unit_index);
314
315        // Calculate line numbers
316        let (start_line, end_line) = self.get_line_numbers(node, unit_index);
317
318        Some(GraphBlockInfo {
319            name: display_name,
320            kind: format!("{:?}", kind),
321            file_path,
322            source_code,
323            node,
324            unit_index,
325            start_line,
326            end_line,
327        })
328    }
329
330    /// Calculate line numbers from byte offsets
331    fn get_line_numbers(&self, node: GraphNode, unit_index: usize) -> (usize, usize) {
332        let file = match self.graph.cc.files.get(unit_index) {
333            Some(f) => f,
334            None => return (0, 0),
335        };
336
337        let unit = self.graph.cc.compile_unit(unit_index);
338
339        // Get the BasicBlock to access its HIR node
340        let bb = match unit.opt_bb(node.block_id) {
341            Some(b) => b,
342            None => return (0, 0),
343        };
344
345        // Get the base which contains the HirNode
346        let base = match bb.base() {
347            Some(b) => b,
348            None => return (0, 0),
349        };
350
351        let hir_node = base.node;
352        let start_byte = hir_node.start_byte();
353        let end_byte = hir_node.end_byte();
354
355        // Get the file content and count lines
356        if let Some(content) = file.file.content.as_ref() {
357            let start_line = content[..start_byte.min(content.len())]
358                .iter()
359                .filter(|&&b| b == b'\n')
360                .count()
361                + 1;
362            let end_line = content[..end_byte.min(content.len())]
363                .iter()
364                .filter(|&&b| b == b'\n')
365                .count()
366                + 1;
367            (start_line, end_line)
368        } else {
369            (0, 0)
370        }
371    }
372
373    /// Extract the source code for a given block
374    fn get_block_source_code(&self, node: GraphNode, unit_index: usize) -> Option<String> {
375        let file = self.graph.cc.files.get(unit_index)?;
376        let unit = self.graph.cc.compile_unit(unit_index);
377
378        // Get the BasicBlock to access its HIR node
379        let bb = unit.opt_bb(node.block_id)?;
380
381        // Get the base which contains the HirNode
382        let base = bb.base()?;
383        let hir_node = base.node;
384
385        // Get the span information from the HirNode
386        let start_byte = hir_node.start_byte();
387        let end_byte = hir_node.end_byte();
388
389        // Special handling for Class blocks: filter out method implementations
390        if let crate::block::BasicBlock::Class(class_block) = bb {
391            return self.extract_class_definition(class_block, unit, file, start_byte, end_byte);
392        }
393
394        file.opt_get_text(start_byte, end_byte)
395    }
396
397    /// Extract only the class definition without method implementations
398    /// This handles classes where methods are defined inside the class body (Python-style)
399    fn extract_class_definition(
400        &self,
401        class_block: &crate::block::BlockClass,
402        unit: crate::context::CompileUnit,
403        file: &crate::file::File,
404        class_start_byte: usize,
405        class_end_byte: usize,
406    ) -> Option<String> {
407        // Get the full class source
408        let full_text = file.opt_get_text(class_start_byte, class_end_byte)?;
409
410        // Collect byte positions of all methods (BlockKind::Func children)
411        let mut method_start_bytes = Vec::new();
412
413        for child_id in &class_block.base.children {
414            let child_bb = unit.opt_bb(*child_id)?;
415            let child_kind = child_bb.kind();
416
417            // If this child is a method (Func block), record its start byte
418            if child_kind == BlockKind::Func {
419                if let Some(child_base) = child_bb.base() {
420                    let child_node = child_base.node;
421                    let child_start = child_node.start_byte();
422                    if child_start > class_start_byte {
423                        method_start_bytes.push(child_start);
424                    }
425                }
426            }
427        }
428
429        // If there are no methods, return the full text
430        if method_start_bytes.is_empty() {
431            return Some(full_text);
432        }
433
434        // Find the byte position of the first method
435        method_start_bytes.sort();
436        let first_method_start = method_start_bytes[0];
437
438        // Calculate the offset relative to class start
439        let offset = first_method_start - class_start_byte;
440        if offset >= full_text.len() {
441            return Some(full_text);
442        }
443
444        // Extract text up to the first method
445        let class_def = full_text[..offset].to_string();
446
447        // Clean up trailing whitespace and incomplete lines
448        let trimmed = class_def.trim_end();
449
450        // Try to find the last complete line (before the first method starts)
451        if let Some(last_newline) = trimmed.rfind('\n') {
452            Some(trimmed[..=last_newline].to_string())
453        } else {
454            Some(trimmed.to_string())
455        }
456    }
457}