1use crate::parser::{ImportInfo, ParseError, ParseResult, sha256_hex};
9use crate::schema::{CodeNode, CodeNodeKind};
10use std::path::Path;
11
12pub 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 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, 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 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
64fn 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 "attribute_item" => {
119 }
122 _ => {}
123 }
124}
125
126fn 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 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 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 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 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 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 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
598fn 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
608fn extract_rust_fn_signature(node: &tree_sitter::Node, src: &[u8]) -> String {
610 let full_text = node_text(node, src);
611 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
621fn extract_first_line_signature(body: &str) -> String {
623 let first_line = body.lines().next().unwrap_or("").trim();
624 let sig = first_line.trim_end_matches('{').trim();
626 sig.to_string()
627}
628
629fn extract_impl_name(node: &tree_sitter::Node, src: &[u8]) -> String {
631 let full_text = node_text(node, src);
632 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 after_impl.trim().to_string()
644 }
645}
646
647fn has_test_attribute(node: &tree_sitter::Node, src: &[u8]) -> bool {
649 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 break;
660 }
661 sibling = sib.prev_sibling();
662 }
663 false
664}
665
666fn 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 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
699fn parse_use_path(path: &str) -> (String, Vec<String>) {
707 let path = path.trim();
708
709 if path.ends_with("::*") {
711 let module = path.strip_suffix("::*").unwrap_or(path).to_string();
712 return (module, vec!["*".to_string()]);
713 }
714
715 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 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 (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 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 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 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 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 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 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 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 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 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 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 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}