Skip to main content

nusy_codegraph/
rust_parser.rs

1//! tree-sitter Rust parser — extract CodeNodes from Rust source files.
2//!
3//! Parses Rust source into a tree of CodeNodes representing files, functions,
4//! structs, enums, traits, impl blocks, methods, macros, use declarations,
5//! const/static items, type aliases, and modules. Builds containment hierarchy
6//! via parent_id and extracts import information from use declarations.
7
8use crate::parser::{ImportInfo, ParseError, ParseResult, sha256_hex};
9use crate::schema::{CodeNode, CodeNodeKind};
10use std::path::Path;
11
12/// Parse a Rust source file into CodeNodes.
13///
14/// Returns nodes for the file and all top-level and nested items:
15/// functions, structs, enums, traits, impl blocks (with methods),
16/// macros, use declarations, const/static items, type aliases, and modules.
17pub fn parse_rust_file(path: &Path, source: &str) -> Result<ParseResult, ParseError> {
18    let mut parser = tree_sitter::Parser::new();
19    let language = tree_sitter_rust::LANGUAGE;
20    parser.set_language(&language.into())?;
21
22    let tree = parser
23        .parse(source, None)
24        .ok_or_else(|| ParseError::ParseFailed {
25            path: path.display().to_string(),
26        })?;
27
28    let path_str = path.display().to_string();
29    let file_id = format!("file:{path_str}");
30
31    // File node
32    let file_node = CodeNode {
33        id: file_id.clone(),
34        kind: CodeNodeKind::File,
35        parent_id: None,
36        name: path
37            .file_name()
38            .map(|n| n.to_string_lossy().to_string())
39            .unwrap_or_default(),
40        signature: None,
41        docstring: None,
42        body_hash: Some(sha256_hex(source.as_bytes())),
43        body: None, // File-level body omitted (too large)
44        loc: Some(source.lines().count() as i32),
45        file_path: Some(path_str.clone()),
46        ..Default::default()
47    };
48
49    let mut nodes = vec![file_node];
50    let mut imports = Vec::new();
51
52    let root = tree.root_node();
53    let src = source.as_bytes();
54
55    // Walk top-level children
56    let mut cursor = root.walk();
57    for child in root.children(&mut cursor) {
58        extract_item(&child, src, &path_str, &file_id, &mut nodes, &mut imports);
59    }
60
61    Ok(ParseResult { nodes, imports })
62}
63
64// ---- Extraction dispatcher --------------------------------------------------
65
66fn extract_item(
67    node: &tree_sitter::Node,
68    src: &[u8],
69    path: &str,
70    parent_id: &str,
71    nodes: &mut Vec<CodeNode>,
72    imports: &mut Vec<ImportInfo>,
73) {
74    match node.kind() {
75        "function_item" => {
76            let is_test = has_test_attribute(node, src);
77            let extracted = extract_function(node, src, path, parent_id, is_test);
78            nodes.push(extracted);
79        }
80        "struct_item" => {
81            nodes.push(extract_struct(node, src, path, parent_id));
82        }
83        "enum_item" => {
84            nodes.push(extract_enum(node, src, path, parent_id));
85        }
86        "impl_item" => {
87            let impl_nodes = extract_impl(node, src, path, parent_id);
88            nodes.extend(impl_nodes);
89        }
90        "trait_item" => {
91            let trait_nodes = extract_trait(node, src, path, parent_id);
92            nodes.extend(trait_nodes);
93        }
94        "mod_item" => {
95            let mod_nodes = extract_mod(node, src, path, parent_id, imports);
96            nodes.extend(mod_nodes);
97        }
98        "macro_definition" => {
99            nodes.push(extract_macro(node, src, path, parent_id));
100        }
101        "use_declaration" => {
102            let (use_node, import) = extract_use(node, src, path, parent_id);
103            nodes.push(use_node);
104            if let Some(imp) = import {
105                imports.push(imp);
106            }
107        }
108        "const_item" => {
109            nodes.push(extract_const(node, src, path, parent_id));
110        }
111        "static_item" => {
112            nodes.push(extract_static(node, src, path, parent_id));
113        }
114        "type_item" => {
115            nodes.push(extract_type_alias(node, src, path, parent_id));
116        }
117        // Handle attributed items (e.g., #[test] fn ...)
118        "attribute_item" => {
119            // Attributes are handled by peeking at siblings in the parent context;
120            // nothing to extract standalone.
121        }
122        _ => {}
123    }
124}
125
126// ---- Individual extractors --------------------------------------------------
127
128fn extract_function(
129    node: &tree_sitter::Node,
130    src: &[u8],
131    path: &str,
132    parent_id: &str,
133    is_test: bool,
134) -> CodeNode {
135    let name = node_child_text(node, "name", src).unwrap_or_default();
136    let kind = if is_test {
137        CodeNodeKind::RustTest
138    } else {
139        CodeNodeKind::RustFn
140    };
141
142    let id = if is_test {
143        format!("rust_test:{path}::{name}")
144    } else {
145        format!("rust_fn:{path}::{name}")
146    };
147
148    let signature = extract_rust_fn_signature(node, src);
149    let body = node_text(node, src);
150    let body_hash = sha256_hex(body.as_bytes());
151    let loc = (node.end_position().row - node.start_position().row + 1) as i32;
152
153    CodeNode {
154        id,
155        kind,
156        parent_id: Some(parent_id.to_string()),
157        name,
158        signature: Some(signature),
159        docstring: extract_rust_doc_comment(node, src),
160        body_hash: Some(body_hash),
161        body: Some(body),
162        loc: Some(loc),
163        start_line: Some(node.start_position().row as u32 + 1),
164        end_line: Some(node.end_position().row as u32 + 1),
165        start_col: Some(node.start_position().column as u32),
166        end_col: Some(node.end_position().column as u32),
167        file_path: Some(path.to_string()),
168        byte_offset: Some(node.start_byte() as u64),
169        ..Default::default()
170    }
171}
172
173fn extract_struct(node: &tree_sitter::Node, src: &[u8], path: &str, parent_id: &str) -> CodeNode {
174    let name = node_child_text(node, "name", src).unwrap_or_default();
175    let id = format!("rust_struct:{path}::{name}");
176    let body = node_text(node, src);
177    let body_hash = sha256_hex(body.as_bytes());
178    let loc = (node.end_position().row - node.start_position().row + 1) as i32;
179
180    // Signature: first line up to opening brace or semicolon
181    let signature = extract_first_line_signature(&body);
182
183    CodeNode {
184        id,
185        kind: CodeNodeKind::RustStruct,
186        parent_id: Some(parent_id.to_string()),
187        name,
188        signature: Some(signature),
189        docstring: extract_rust_doc_comment(node, src),
190        body_hash: Some(body_hash),
191        body: Some(body),
192        loc: Some(loc),
193        start_line: Some(node.start_position().row as u32 + 1),
194        end_line: Some(node.end_position().row as u32 + 1),
195        start_col: Some(node.start_position().column as u32),
196        end_col: Some(node.end_position().column as u32),
197        file_path: Some(path.to_string()),
198        byte_offset: Some(node.start_byte() as u64),
199        ..Default::default()
200    }
201}
202
203fn extract_enum(node: &tree_sitter::Node, src: &[u8], path: &str, parent_id: &str) -> CodeNode {
204    let name = node_child_text(node, "name", src).unwrap_or_default();
205    let id = format!("rust_enum:{path}::{name}");
206    let body = node_text(node, src);
207    let body_hash = sha256_hex(body.as_bytes());
208    let loc = (node.end_position().row - node.start_position().row + 1) as i32;
209    let signature = extract_first_line_signature(&body);
210
211    CodeNode {
212        id,
213        kind: CodeNodeKind::RustEnum,
214        parent_id: Some(parent_id.to_string()),
215        name,
216        signature: Some(signature),
217        docstring: extract_rust_doc_comment(node, src),
218        body_hash: Some(body_hash),
219        body: Some(body),
220        loc: Some(loc),
221        start_line: Some(node.start_position().row as u32 + 1),
222        end_line: Some(node.end_position().row as u32 + 1),
223        start_col: Some(node.start_position().column as u32),
224        end_col: Some(node.end_position().column as u32),
225        file_path: Some(path.to_string()),
226        byte_offset: Some(node.start_byte() as u64),
227        ..Default::default()
228    }
229}
230
231fn extract_impl(
232    node: &tree_sitter::Node,
233    src: &[u8],
234    path: &str,
235    parent_id: &str,
236) -> Vec<CodeNode> {
237    let body_text = node_text(node, src);
238
239    // Build impl name: "impl Foo" or "impl Trait for Foo"
240    let impl_name = extract_impl_name(node, src);
241    let id = format!("rust_impl:{path}::{impl_name}");
242
243    let body_hash = sha256_hex(body_text.as_bytes());
244    let loc = (node.end_position().row - node.start_position().row + 1) as i32;
245    let signature = extract_first_line_signature(&body_text);
246
247    let impl_node = CodeNode {
248        id: id.clone(),
249        kind: CodeNodeKind::RustImpl,
250        parent_id: Some(parent_id.to_string()),
251        name: impl_name,
252        signature: Some(signature),
253        docstring: extract_rust_doc_comment(node, src),
254        body_hash: Some(body_hash),
255        body: Some(body_text),
256        loc: Some(loc),
257        start_line: Some(node.start_position().row as u32 + 1),
258        end_line: Some(node.end_position().row as u32 + 1),
259        start_col: Some(node.start_position().column as u32),
260        end_col: Some(node.end_position().column as u32),
261        file_path: Some(path.to_string()),
262        byte_offset: Some(node.start_byte() as u64),
263        ..Default::default()
264    };
265
266    let mut nodes = vec![impl_node];
267
268    // Extract methods from the impl body
269    if let Some(body) = node.child_by_field_name("body") {
270        let mut cursor = body.walk();
271        for child in body.children(&mut cursor) {
272            if child.kind() == "function_item" {
273                let is_test = has_test_attribute(&child, src);
274                let method_name = node_child_text(&child, "name", src).unwrap_or_default();
275                let kind = if is_test {
276                    CodeNodeKind::RustTest
277                } else {
278                    CodeNodeKind::RustMethod
279                };
280                let method_id = if is_test {
281                    format!("rust_test:{path}::{method_name}")
282                } else {
283                    format!("rust_method:{path}::{method_name}")
284                };
285
286                let method_sig = extract_rust_fn_signature(&child, src);
287                let method_body = node_text(&child, src);
288                let method_hash = sha256_hex(method_body.as_bytes());
289                let method_loc = (child.end_position().row - child.start_position().row + 1) as i32;
290
291                nodes.push(CodeNode {
292                    id: method_id,
293                    kind,
294                    parent_id: Some(id.clone()),
295                    name: method_name,
296                    signature: Some(method_sig),
297                    docstring: extract_rust_doc_comment(&child, src),
298                    body_hash: Some(method_hash),
299                    body: Some(method_body),
300                    loc: Some(method_loc),
301                    start_line: Some(child.start_position().row as u32 + 1),
302                    end_line: Some(child.end_position().row as u32 + 1),
303                    start_col: Some(child.start_position().column as u32),
304                    end_col: Some(child.end_position().column as u32),
305                    file_path: Some(path.to_string()),
306                    byte_offset: Some(child.start_byte() as u64),
307                    ..Default::default()
308                });
309            }
310        }
311    }
312
313    nodes
314}
315
316fn extract_trait(
317    node: &tree_sitter::Node,
318    src: &[u8],
319    path: &str,
320    parent_id: &str,
321) -> Vec<CodeNode> {
322    let name = node_child_text(node, "name", src).unwrap_or_default();
323    let id = format!("rust_trait:{path}::{name}");
324    let body_text = node_text(node, src);
325    let body_hash = sha256_hex(body_text.as_bytes());
326    let loc = (node.end_position().row - node.start_position().row + 1) as i32;
327    let signature = extract_first_line_signature(&body_text);
328
329    let trait_node = CodeNode {
330        id: id.clone(),
331        kind: CodeNodeKind::RustTrait,
332        parent_id: Some(parent_id.to_string()),
333        name,
334        signature: Some(signature),
335        docstring: extract_rust_doc_comment(node, src),
336        body_hash: Some(body_hash),
337        body: Some(body_text),
338        loc: Some(loc),
339        start_line: Some(node.start_position().row as u32 + 1),
340        end_line: Some(node.end_position().row as u32 + 1),
341        start_col: Some(node.start_position().column as u32),
342        end_col: Some(node.end_position().column as u32),
343        file_path: Some(path.to_string()),
344        byte_offset: Some(node.start_byte() as u64),
345        ..Default::default()
346    };
347
348    let mut nodes = vec![trait_node];
349
350    // Extract methods from the trait body
351    if let Some(body) = node.child_by_field_name("body") {
352        let mut cursor = body.walk();
353        for child in body.children(&mut cursor) {
354            if child.kind() == "function_item" || child.kind() == "function_signature_item" {
355                let method_name = node_child_text(&child, "name", src).unwrap_or_default();
356                let method_id = format!("rust_method:{path}::{method_name}");
357                let method_sig = extract_rust_fn_signature(&child, src);
358                let method_body = node_text(&child, src);
359                let method_hash = sha256_hex(method_body.as_bytes());
360                let method_loc = (child.end_position().row - child.start_position().row + 1) as i32;
361
362                nodes.push(CodeNode {
363                    id: method_id,
364                    kind: CodeNodeKind::RustMethod,
365                    parent_id: Some(id.clone()),
366                    name: method_name,
367                    signature: Some(method_sig),
368                    docstring: extract_rust_doc_comment(&child, src),
369                    body_hash: Some(method_hash),
370                    body: Some(method_body),
371                    loc: Some(method_loc),
372                    start_line: Some(child.start_position().row as u32 + 1),
373                    end_line: Some(child.end_position().row as u32 + 1),
374                    start_col: Some(child.start_position().column as u32),
375                    end_col: Some(child.end_position().column as u32),
376                    file_path: Some(path.to_string()),
377                    byte_offset: Some(child.start_byte() as u64),
378                    ..Default::default()
379                });
380            }
381        }
382    }
383
384    nodes
385}
386
387fn extract_mod(
388    node: &tree_sitter::Node,
389    src: &[u8],
390    path: &str,
391    parent_id: &str,
392    imports: &mut Vec<ImportInfo>,
393) -> Vec<CodeNode> {
394    let name = node_child_text(node, "name", src).unwrap_or_default();
395    let id = format!("rust_mod:{path}::{name}");
396    let body_text = node_text(node, src);
397    let body_hash = sha256_hex(body_text.as_bytes());
398    let loc = (node.end_position().row - node.start_position().row + 1) as i32;
399
400    let mod_node = CodeNode {
401        id: id.clone(),
402        kind: CodeNodeKind::RustMod,
403        parent_id: Some(parent_id.to_string()),
404        name,
405        signature: Some(extract_first_line_signature(&body_text)),
406        docstring: extract_rust_doc_comment(node, src),
407        body_hash: Some(body_hash),
408        body: Some(body_text),
409        loc: Some(loc),
410        start_line: Some(node.start_position().row as u32 + 1),
411        end_line: Some(node.end_position().row as u32 + 1),
412        start_col: Some(node.start_position().column as u32),
413        end_col: Some(node.end_position().column as u32),
414        file_path: Some(path.to_string()),
415        byte_offset: Some(node.start_byte() as u64),
416        ..Default::default()
417    };
418
419    let mut nodes = vec![mod_node];
420
421    // If the mod has a body (inline module), extract its children
422    if let Some(body) = node.child_by_field_name("body") {
423        let mut cursor = body.walk();
424        for child in body.children(&mut cursor) {
425            extract_item(&child, src, path, &id, &mut nodes, imports);
426        }
427    }
428
429    nodes
430}
431
432fn extract_macro(node: &tree_sitter::Node, src: &[u8], path: &str, parent_id: &str) -> CodeNode {
433    let name = node_child_text(node, "name", src).unwrap_or_default();
434    let id = format!("rust_macro:{path}::{name}");
435    let body = node_text(node, src);
436    let body_hash = sha256_hex(body.as_bytes());
437    let loc = (node.end_position().row - node.start_position().row + 1) as i32;
438
439    CodeNode {
440        id,
441        kind: CodeNodeKind::RustMacro,
442        parent_id: Some(parent_id.to_string()),
443        name,
444        signature: Some(format!(
445            "macro_rules! {}",
446            node_child_text(node, "name", src).unwrap_or_default()
447        )),
448        docstring: extract_rust_doc_comment(node, src),
449        body_hash: Some(body_hash),
450        body: Some(body),
451        loc: Some(loc),
452        start_line: Some(node.start_position().row as u32 + 1),
453        end_line: Some(node.end_position().row as u32 + 1),
454        start_col: Some(node.start_position().column as u32),
455        end_col: Some(node.end_position().column as u32),
456        file_path: Some(path.to_string()),
457        byte_offset: Some(node.start_byte() as u64),
458        ..Default::default()
459    }
460}
461
462fn extract_use(
463    node: &tree_sitter::Node,
464    src: &[u8],
465    path: &str,
466    parent_id: &str,
467) -> (CodeNode, Option<ImportInfo>) {
468    let full_text = node_text(node, src);
469    let use_path = full_text
470        .trim()
471        .strip_prefix("use ")
472        .unwrap_or(&full_text)
473        .trim_end_matches(';')
474        .trim()
475        .to_string();
476
477    // Parse the use path for ImportInfo
478    let (module, names) = parse_use_path(&use_path);
479
480    let id = format!("rust_use:{path}::{use_path}");
481    let body_hash = sha256_hex(full_text.as_bytes());
482
483    let code_node = CodeNode {
484        id,
485        kind: CodeNodeKind::RustUse,
486        parent_id: Some(parent_id.to_string()),
487        name: use_path.clone(),
488        signature: Some(full_text.trim().to_string()),
489        docstring: None,
490        body_hash: Some(body_hash),
491        body: Some(full_text),
492        loc: Some(1),
493        start_line: Some(node.start_position().row as u32 + 1),
494        end_line: Some(node.end_position().row as u32 + 1),
495        start_col: Some(node.start_position().column as u32),
496        end_col: Some(node.end_position().column as u32),
497        file_path: Some(path.to_string()),
498        byte_offset: Some(node.start_byte() as u64),
499        ..Default::default()
500    };
501
502    let import = ImportInfo {
503        file_node_id: format!("file:{path}"),
504        module,
505        names,
506        is_relative: false,
507    };
508
509    (code_node, Some(import))
510}
511
512fn extract_const(node: &tree_sitter::Node, src: &[u8], path: &str, parent_id: &str) -> CodeNode {
513    let name = node_child_text(node, "name", src).unwrap_or_default();
514    let id = format!("rust_const:{path}::{name}");
515    let body = node_text(node, src);
516    let body_hash = sha256_hex(body.as_bytes());
517    let loc = (node.end_position().row - node.start_position().row + 1) as i32;
518
519    CodeNode {
520        id,
521        kind: CodeNodeKind::RustConst,
522        parent_id: Some(parent_id.to_string()),
523        name,
524        signature: Some(extract_first_line_signature(&body)),
525        docstring: extract_rust_doc_comment(node, src),
526        body_hash: Some(body_hash),
527        body: Some(body),
528        loc: Some(loc),
529        start_line: Some(node.start_position().row as u32 + 1),
530        end_line: Some(node.end_position().row as u32 + 1),
531        start_col: Some(node.start_position().column as u32),
532        end_col: Some(node.end_position().column as u32),
533        file_path: Some(path.to_string()),
534        byte_offset: Some(node.start_byte() as u64),
535        ..Default::default()
536    }
537}
538
539fn extract_static(node: &tree_sitter::Node, src: &[u8], path: &str, parent_id: &str) -> CodeNode {
540    let name = node_child_text(node, "name", src).unwrap_or_default();
541    let id = format!("rust_static:{path}::{name}");
542    let body = node_text(node, src);
543    let body_hash = sha256_hex(body.as_bytes());
544    let loc = (node.end_position().row - node.start_position().row + 1) as i32;
545
546    CodeNode {
547        id,
548        kind: CodeNodeKind::RustStatic,
549        parent_id: Some(parent_id.to_string()),
550        name,
551        signature: Some(extract_first_line_signature(&body)),
552        docstring: extract_rust_doc_comment(node, src),
553        body_hash: Some(body_hash),
554        body: Some(body),
555        loc: Some(loc),
556        start_line: Some(node.start_position().row as u32 + 1),
557        end_line: Some(node.end_position().row as u32 + 1),
558        start_col: Some(node.start_position().column as u32),
559        end_col: Some(node.end_position().column as u32),
560        file_path: Some(path.to_string()),
561        byte_offset: Some(node.start_byte() as u64),
562        ..Default::default()
563    }
564}
565
566fn extract_type_alias(
567    node: &tree_sitter::Node,
568    src: &[u8],
569    path: &str,
570    parent_id: &str,
571) -> CodeNode {
572    let name = node_child_text(node, "name", src).unwrap_or_default();
573    let id = format!("rust_type_alias:{path}::{name}");
574    let body = node_text(node, src);
575    let body_hash = sha256_hex(body.as_bytes());
576    let loc = (node.end_position().row - node.start_position().row + 1) as i32;
577
578    CodeNode {
579        id,
580        kind: CodeNodeKind::RustTypeAlias,
581        parent_id: Some(parent_id.to_string()),
582        name,
583        signature: Some(body.trim().to_string()),
584        docstring: extract_rust_doc_comment(node, src),
585        body_hash: Some(body_hash),
586        body: Some(body),
587        loc: Some(loc),
588        start_line: Some(node.start_position().row as u32 + 1),
589        end_line: Some(node.end_position().row as u32 + 1),
590        start_col: Some(node.start_position().column as u32),
591        end_col: Some(node.end_position().column as u32),
592        file_path: Some(path.to_string()),
593        byte_offset: Some(node.start_byte() as u64),
594        ..Default::default()
595    }
596}
597
598// ---- AST utility helpers ----------------------------------------------------
599
600fn node_text(node: &tree_sitter::Node, src: &[u8]) -> String {
601    node.utf8_text(src).unwrap_or("").to_string()
602}
603
604fn node_child_text(node: &tree_sitter::Node, field: &str, src: &[u8]) -> Option<String> {
605    node.child_by_field_name(field).map(|n| node_text(&n, src))
606}
607
608/// Extract function signature: `fn name(params) -> ReturnType`
609fn extract_rust_fn_signature(node: &tree_sitter::Node, src: &[u8]) -> String {
610    let full_text = node_text(node, src);
611    // Signature is everything up to the opening brace (or semicolon for trait methods)
612    if let Some(brace_pos) = full_text.find('{') {
613        full_text[..brace_pos].trim().to_string()
614    } else if let Some(semi_pos) = full_text.find(';') {
615        full_text[..semi_pos].trim().to_string()
616    } else {
617        full_text.lines().next().unwrap_or("").trim().to_string()
618    }
619}
620
621/// Extract the first line of a body as a signature (for structs, enums, etc.)
622fn extract_first_line_signature(body: &str) -> String {
623    let first_line = body.lines().next().unwrap_or("").trim();
624    // Strip trailing brace if present
625    let sig = first_line.trim_end_matches('{').trim();
626    sig.to_string()
627}
628
629/// Extract impl name: "Foo" for `impl Foo { ... }` or "Trait for Foo" for `impl Trait for Foo { ... }`
630fn extract_impl_name(node: &tree_sitter::Node, src: &[u8]) -> String {
631    let full_text = node_text(node, src);
632    // Extract text between "impl" and "{"
633    let after_impl = full_text
634        .trim()
635        .strip_prefix("impl")
636        .unwrap_or(&full_text)
637        .trim();
638
639    if let Some(brace_pos) = after_impl.find('{') {
640        after_impl[..brace_pos].trim().to_string()
641    } else {
642        // External impl (no body)
643        after_impl.trim().to_string()
644    }
645}
646
647/// Check if a function has a `#[test]` attribute by looking at preceding siblings.
648fn has_test_attribute(node: &tree_sitter::Node, src: &[u8]) -> bool {
649    // Check preceding siblings for attribute_item containing "test"
650    let mut sibling = node.prev_sibling();
651    while let Some(sib) = sibling {
652        if sib.kind() == "attribute_item" {
653            let text = node_text(&sib, src);
654            if text.contains("#[test]") || text.contains("#[tokio::test]") {
655                return true;
656            }
657        } else if sib.kind() != "line_comment" && sib.kind() != "block_comment" {
658            // Stop at non-attribute, non-comment nodes
659            break;
660        }
661        sibling = sib.prev_sibling();
662    }
663    false
664}
665
666/// Extract Rust doc comments (/// or //!) preceding a node.
667fn extract_rust_doc_comment(node: &tree_sitter::Node, src: &[u8]) -> Option<String> {
668    let mut doc_lines = Vec::new();
669    let mut sibling = node.prev_sibling();
670
671    while let Some(sib) = sibling {
672        if sib.kind() == "line_comment" {
673            let text = node_text(&sib, src);
674            if let Some(doc) = text.strip_prefix("///") {
675                doc_lines.push(doc.trim_start_matches(' ').to_string());
676            } else if let Some(doc) = text.strip_prefix("//!") {
677                doc_lines.push(doc.trim_start_matches(' ').to_string());
678            } else {
679                break;
680            }
681        } else if sib.kind() == "attribute_item" {
682            // Skip attributes (they can appear between doc comments and the item)
683            sibling = sib.prev_sibling();
684            continue;
685        } else {
686            break;
687        }
688        sibling = sib.prev_sibling();
689    }
690
691    if doc_lines.is_empty() {
692        return None;
693    }
694
695    doc_lines.reverse();
696    Some(doc_lines.join("\n").trim().to_string())
697}
698
699/// Parse a Rust use path into module and imported names.
700///
701/// Examples:
702/// - `std::collections::HashMap` -> ("std::collections", ["HashMap"])
703/// - `std::collections::{HashMap, BTreeMap}` -> ("std::collections", ["HashMap", "BTreeMap"])
704/// - `crate::parser::ParseResult` -> ("crate::parser", ["ParseResult"])
705/// - `std::io` -> ("std", ["io"])
706fn parse_use_path(path: &str) -> (String, Vec<String>) {
707    let path = path.trim();
708
709    // Handle glob imports: `use std::io::*;`
710    if path.ends_with("::*") {
711        let module = path.strip_suffix("::*").unwrap_or(path).to_string();
712        return (module, vec!["*".to_string()]);
713    }
714
715    // Handle group imports: `use std::collections::{HashMap, BTreeMap};`
716    if let Some(brace_start) = path.find("::{") {
717        let module = path[..brace_start].to_string();
718        let group = &path[brace_start + 3..];
719        let group = group.trim_end_matches('}');
720        let names: Vec<String> = group
721            .split(',')
722            .map(|s| s.trim().to_string())
723            .filter(|s| !s.is_empty())
724            .collect();
725        return (module, names);
726    }
727
728    // Simple path: `use std::collections::HashMap;`
729    if let Some(last_sep) = path.rfind("::") {
730        let module = path[..last_sep].to_string();
731        let name = path[last_sep + 2..].to_string();
732        (module, vec![name])
733    } else {
734        // Single segment: `use std;`
735        (String::new(), vec![path.to_string()])
736    }
737}
738
739#[cfg(test)]
740mod tests {
741    use super::*;
742    use std::path::PathBuf;
743
744    #[test]
745    fn test_parse_simple_function() {
746        let source = r#"fn foo() -> i32 {
747    42
748}"#;
749        let path = PathBuf::from("src/lib.rs");
750        let result = parse_rust_file(&path, source).expect("parse should succeed");
751
752        // Should have: file + function
753        assert!(
754            result.nodes.len() >= 2,
755            "Expected >= 2 nodes, got {}",
756            result.nodes.len()
757        );
758
759        let func = result
760            .nodes
761            .iter()
762            .find(|n| n.kind == CodeNodeKind::RustFn && n.name == "foo");
763        assert!(func.is_some(), "should have RustFn node for foo");
764        let func = func.unwrap();
765        assert!(func.signature.as_ref().unwrap().contains("fn foo() -> i32"));
766        assert!(func.body.as_ref().unwrap().contains("42"));
767        assert!(func.body_hash.is_some());
768    }
769
770    #[test]
771    fn test_parse_struct_with_fields() {
772        let source = r#"struct Foo {
773    x: i32,
774    y: String,
775}"#;
776        let path = PathBuf::from("src/lib.rs");
777        let result = parse_rust_file(&path, source).expect("parse should succeed");
778
779        let st = result
780            .nodes
781            .iter()
782            .find(|n| n.kind == CodeNodeKind::RustStruct && n.name == "Foo");
783        assert!(st.is_some(), "should have RustStruct node for Foo");
784        let st = st.unwrap();
785        assert!(st.signature.as_ref().unwrap().contains("struct Foo"));
786    }
787
788    #[test]
789    fn test_parse_enum() {
790        let source = r#"enum Color {
791    Red,
792    Blue,
793}"#;
794        let path = PathBuf::from("src/lib.rs");
795        let result = parse_rust_file(&path, source).expect("parse should succeed");
796
797        let en = result
798            .nodes
799            .iter()
800            .find(|n| n.kind == CodeNodeKind::RustEnum && n.name == "Color");
801        assert!(en.is_some(), "should have RustEnum node for Color");
802    }
803
804    #[test]
805    fn test_parse_impl_block_with_methods() {
806        let source = r#"struct Foo;
807
808impl Foo {
809    fn bar(&self) -> i32 {
810        42
811    }
812
813    fn baz(&mut self, x: i32) {
814        self.x = x;
815    }
816}"#;
817        let path = PathBuf::from("src/lib.rs");
818        let result = parse_rust_file(&path, source).expect("parse should succeed");
819
820        // Check impl node
821        let imp = result
822            .nodes
823            .iter()
824            .find(|n| n.kind == CodeNodeKind::RustImpl);
825        assert!(imp.is_some(), "should have RustImpl node");
826        let imp = imp.unwrap();
827        assert!(imp.name.contains("Foo"), "impl name should contain Foo");
828
829        // Check methods
830        let methods: Vec<_> = result
831            .nodes
832            .iter()
833            .filter(|n| n.kind == CodeNodeKind::RustMethod)
834            .collect();
835        assert_eq!(methods.len(), 2, "should have 2 methods");
836
837        let bar = methods.iter().find(|n| n.name == "bar");
838        assert!(bar.is_some(), "should have method bar");
839        let bar = bar.unwrap();
840        assert_eq!(
841            bar.parent_id.as_deref(),
842            Some(imp.id.as_str()),
843            "method parent should be impl"
844        );
845        assert!(
846            bar.signature
847                .as_ref()
848                .unwrap()
849                .contains("fn bar(&self) -> i32")
850        );
851    }
852
853    #[test]
854    fn test_parse_trait() {
855        let source = r#"trait MyTrait {
856    fn required(&self) -> i32;
857
858    fn provided(&self) -> bool {
859        true
860    }
861}"#;
862        let path = PathBuf::from("src/lib.rs");
863        let result = parse_rust_file(&path, source).expect("parse should succeed");
864
865        let tr = result
866            .nodes
867            .iter()
868            .find(|n| n.kind == CodeNodeKind::RustTrait && n.name == "MyTrait");
869        assert!(tr.is_some(), "should have RustTrait node for MyTrait");
870
871        // Check trait methods
872        let methods: Vec<_> = result
873            .nodes
874            .iter()
875            .filter(|n| n.kind == CodeNodeKind::RustMethod)
876            .collect();
877        assert!(methods.len() >= 1, "should have at least 1 method in trait");
878    }
879
880    #[test]
881    fn test_parse_use_declaration() {
882        let source = "use std::collections::HashMap;\n";
883        let path = PathBuf::from("src/lib.rs");
884        let result = parse_rust_file(&path, source).expect("parse should succeed");
885
886        let use_node = result
887            .nodes
888            .iter()
889            .find(|n| n.kind == CodeNodeKind::RustUse);
890        assert!(use_node.is_some(), "should have RustUse node");
891
892        assert!(!result.imports.is_empty(), "should have imports");
893        let imp = &result.imports[0];
894        assert_eq!(imp.module, "std::collections");
895        assert_eq!(imp.names, vec!["HashMap"]);
896    }
897
898    #[test]
899    fn test_parse_test_function() {
900        let source = r#"#[test]
901fn test_foo() {
902    assert_eq!(1 + 1, 2);
903}"#;
904        let path = PathBuf::from("src/lib.rs");
905        let result = parse_rust_file(&path, source).expect("parse should succeed");
906
907        let test_fn = result
908            .nodes
909            .iter()
910            .find(|n| n.kind == CodeNodeKind::RustTest && n.name == "test_foo");
911        assert!(test_fn.is_some(), "should have RustTest node for test_foo");
912    }
913
914    #[test]
915    fn test_parse_macro() {
916        let source = r#"macro_rules! my_macro {
917    ($x:expr) => {
918        $x + 1
919    };
920}"#;
921        let path = PathBuf::from("src/lib.rs");
922        let result = parse_rust_file(&path, source).expect("parse should succeed");
923
924        let mac = result
925            .nodes
926            .iter()
927            .find(|n| n.kind == CodeNodeKind::RustMacro && n.name == "my_macro");
928        assert!(mac.is_some(), "should have RustMacro node for my_macro");
929    }
930
931    #[test]
932    fn test_parse_const_and_static() {
933        let source = r#"const X: i32 = 5;
934static Y: i32 = 10;"#;
935        let path = PathBuf::from("src/lib.rs");
936        let result = parse_rust_file(&path, source).expect("parse should succeed");
937
938        let const_node = result
939            .nodes
940            .iter()
941            .find(|n| n.kind == CodeNodeKind::RustConst && n.name == "X");
942        assert!(const_node.is_some(), "should have RustConst node for X");
943
944        let static_node = result
945            .nodes
946            .iter()
947            .find(|n| n.kind == CodeNodeKind::RustStatic && n.name == "Y");
948        assert!(static_node.is_some(), "should have RustStatic node for Y");
949    }
950
951    #[test]
952    fn test_position_metadata_populated() {
953        let source = r#"fn hello() -> &'static str {
954    "world"
955}"#;
956        let path = PathBuf::from("src/main.rs");
957        let result = parse_rust_file(&path, source).expect("parse should succeed");
958
959        let func = result
960            .nodes
961            .iter()
962            .find(|n| n.kind == CodeNodeKind::RustFn && n.name == "hello")
963            .expect("should have function hello");
964
965        assert!(func.start_line.is_some(), "start_line should be set");
966        assert!(func.end_line.is_some(), "end_line should be set");
967        assert!(func.start_col.is_some(), "start_col should be set");
968        assert!(func.end_col.is_some(), "end_col should be set");
969        assert_eq!(func.file_path.as_deref(), Some("src/main.rs"));
970        assert!(func.byte_offset.is_some(), "byte_offset should be set");
971
972        // start_line should be 1 (1-indexed)
973        assert_eq!(func.start_line, Some(1));
974        assert!(func.end_line.unwrap() >= 1);
975    }
976
977    #[test]
978    fn test_containment_hierarchy() {
979        let source = r#"mod inner {
980    struct Data {
981        value: i32,
982    }
983
984    impl Data {
985        fn get_value(&self) -> i32 {
986            self.value
987        }
988    }
989}"#;
990        let path = PathBuf::from("src/lib.rs");
991        let result = parse_rust_file(&path, source).expect("parse should succeed");
992
993        let file_id = "file:src/lib.rs";
994
995        // Module's parent is file
996        let mod_node = result
997            .nodes
998            .iter()
999            .find(|n| n.kind == CodeNodeKind::RustMod && n.name == "inner")
1000            .expect("should have mod inner");
1001        assert_eq!(mod_node.parent_id.as_deref(), Some(file_id));
1002
1003        // Struct's parent is module
1004        let struct_node = result
1005            .nodes
1006            .iter()
1007            .find(|n| n.kind == CodeNodeKind::RustStruct && n.name == "Data")
1008            .expect("should have struct Data");
1009        assert_eq!(struct_node.parent_id.as_deref(), Some(mod_node.id.as_str()));
1010
1011        // Impl's parent is module
1012        let impl_node = result
1013            .nodes
1014            .iter()
1015            .find(|n| n.kind == CodeNodeKind::RustImpl)
1016            .expect("should have impl Data");
1017        assert_eq!(impl_node.parent_id.as_deref(), Some(mod_node.id.as_str()));
1018
1019        // Method's parent is impl
1020        let method = result
1021            .nodes
1022            .iter()
1023            .find(|n| n.kind == CodeNodeKind::RustMethod && n.name == "get_value")
1024            .expect("should have method get_value");
1025        assert_eq!(method.parent_id.as_deref(), Some(impl_node.id.as_str()));
1026    }
1027
1028    #[test]
1029    fn test_parse_use_group() {
1030        let source = "use std::collections::{HashMap, BTreeMap};\n";
1031        let path = PathBuf::from("src/lib.rs");
1032        let result = parse_rust_file(&path, source).expect("parse should succeed");
1033
1034        assert!(!result.imports.is_empty(), "should have imports");
1035        let imp = &result.imports[0];
1036        assert_eq!(imp.module, "std::collections");
1037        assert!(imp.names.contains(&"HashMap".to_string()));
1038        assert!(imp.names.contains(&"BTreeMap".to_string()));
1039    }
1040
1041    #[test]
1042    fn test_parse_type_alias() {
1043        let source = "type Result<T> = std::result::Result<T, MyError>;\n";
1044        let path = PathBuf::from("src/lib.rs");
1045        let result = parse_rust_file(&path, source).expect("parse should succeed");
1046
1047        let ta = result
1048            .nodes
1049            .iter()
1050            .find(|n| n.kind == CodeNodeKind::RustTypeAlias && n.name == "Result");
1051        assert!(ta.is_some(), "should have RustTypeAlias node for Result");
1052    }
1053
1054    #[test]
1055    fn test_parse_impl_test_method() {
1056        let source = r#"struct Foo;
1057
1058impl Foo {
1059    fn normal(&self) {}
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064    use super::*;
1065
1066    #[test]
1067    fn test_foo() {
1068        assert!(true);
1069    }
1070}"#;
1071        let path = PathBuf::from("src/lib.rs");
1072        let result = parse_rust_file(&path, source).expect("parse should succeed");
1073
1074        let test_fn = result
1075            .nodes
1076            .iter()
1077            .find(|n| n.kind == CodeNodeKind::RustTest && n.name == "test_foo");
1078        assert!(test_fn.is_some(), "should have RustTest for test_foo");
1079    }
1080
1081    #[test]
1082    fn test_parse_doc_comments() {
1083        let source = r#"/// This is a documented function.
1084/// It does important things.
1085fn documented() -> bool {
1086    true
1087}"#;
1088        let path = PathBuf::from("src/lib.rs");
1089        let result = parse_rust_file(&path, source).expect("parse should succeed");
1090
1091        let func = result
1092            .nodes
1093            .iter()
1094            .find(|n| n.kind == CodeNodeKind::RustFn && n.name == "documented")
1095            .expect("should have documented function");
1096
1097        assert!(func.docstring.is_some(), "should have docstring");
1098        let doc = func.docstring.as_ref().unwrap();
1099        assert!(
1100            doc.contains("documented function"),
1101            "docstring should contain 'documented function', got: {doc}"
1102        );
1103    }
1104
1105    #[test]
1106    fn test_parse_empty_file() {
1107        let source = "";
1108        let path = PathBuf::from("src/empty.rs");
1109        let result = parse_rust_file(&path, source).expect("parse should succeed on empty file");
1110
1111        // Should have at least the file node
1112        assert_eq!(
1113            result.nodes.len(),
1114            1,
1115            "empty file should have just the file node"
1116        );
1117        assert_eq!(result.nodes[0].kind, CodeNodeKind::File);
1118    }
1119
1120    #[test]
1121    fn test_parse_malformed_rust() {
1122        let source = "fn { broken syntax ;;;";
1123        let path = PathBuf::from("src/broken.rs");
1124        // tree-sitter is lenient — should not panic
1125        let result = parse_rust_file(&path, source);
1126        assert!(result.is_ok(), "malformed Rust should not panic");
1127    }
1128
1129    #[test]
1130    fn test_use_path_parsing() {
1131        let (module, names) = parse_use_path("std::collections::HashMap");
1132        assert_eq!(module, "std::collections");
1133        assert_eq!(names, vec!["HashMap"]);
1134
1135        let (module, names) = parse_use_path("std::collections::{HashMap, BTreeMap}");
1136        assert_eq!(module, "std::collections");
1137        assert_eq!(names, vec!["HashMap", "BTreeMap"]);
1138
1139        let (module, names) = parse_use_path("std::io::*");
1140        assert_eq!(module, "std::io");
1141        assert_eq!(names, vec!["*"]);
1142
1143        let (module, names) = parse_use_path("crate::parser::ParseResult");
1144        assert_eq!(module, "crate::parser");
1145        assert_eq!(names, vec!["ParseResult"]);
1146    }
1147}