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                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        // Get file path from compile context
309        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        // Extract source code for this block
317        let source_code = self.get_block_source_code(node, unit_index);
318
319        // Calculate line numbers
320        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    /// Calculate line numbers from byte offsets
335    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        // Get the BasicBlock to access its HIR node
344        let bb = match unit.opt_bb(node.block_id) {
345            Some(b) => b,
346            None => return (0, 0),
347        };
348
349        // Get the base which contains the HirNode
350        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        // Get the file content and count lines
360        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    /// Extract the source code for a given block
378    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        // Get the BasicBlock to access its HIR node
383        let bb = unit.opt_bb(node.block_id)?;
384
385        // Get the base which contains the HirNode
386        let base = bb.base()?;
387        let hir_node = base.node;
388
389        // Get the span information from the HirNode
390        let start_byte = hir_node.start_byte();
391        let end_byte = hir_node.end_byte();
392
393        // Special handling for Class blocks: filter out method implementations
394        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    /// Extract only the class definition without method implementations
402    /// This handles classes where methods are defined inside the class body (Python-style)
403    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        // Get the full class source
412        let full_text = file.opt_get_text(class_start_byte, class_end_byte)?;
413
414        // Collect byte positions of all methods (BlockKind::Func children)
415        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 this child is a method (Func block), record its start byte
422            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 there are no methods, return the full text
434        if method_start_bytes.is_empty() {
435            return Some(full_text);
436        }
437
438        // Find the byte position of the first method
439        method_start_bytes.sort();
440        let first_method_start = method_start_bytes[0];
441
442        // Calculate the offset relative to class start
443        let offset = first_method_start - class_start_byte;
444        if offset >= full_text.len() {
445            return Some(full_text);
446        }
447
448        // Extract text up to the first method
449        let class_def = full_text[..offset].to_string();
450
451        // Clean up trailing whitespace and incomplete lines
452        let trimmed = class_def.trim_end();
453
454        // Try to find the last complete line (before the first method starts)
455        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}