1use anyhow::{Context, Result};
19use streaming_iterator::StreamingIterator;
20use tree_sitter::{Parser, Query, QueryCursor};
21use crate::models::{Language, SearchResult, Span, SymbolKind};
22
23pub fn parse(path: &str, source: &str) -> Result<Vec<SearchResult>> {
25 let mut parser = Parser::new();
26 let language = tree_sitter_c_sharp::LANGUAGE;
27
28 parser
29 .set_language(&language.into())
30 .context("Failed to set C# language")?;
31
32 let tree = parser
33 .parse(source, None)
34 .context("Failed to parse C# source")?;
35
36 let root_node = tree.root_node();
37
38 let mut symbols = Vec::new();
39
40 symbols.extend(extract_namespaces(source, &root_node, &language.into())?);
42 symbols.extend(extract_classes(source, &root_node, &language.into())?);
43 symbols.extend(extract_interfaces(source, &root_node, &language.into())?);
44 symbols.extend(extract_structs(source, &root_node, &language.into())?);
45 symbols.extend(extract_enums(source, &root_node, &language.into())?);
46 symbols.extend(extract_records(source, &root_node, &language.into())?);
47 symbols.extend(extract_delegates(source, &root_node, &language.into())?);
48 symbols.extend(extract_attributes(source, &root_node, &language.into())?);
49 symbols.extend(extract_methods(source, &root_node, &language.into())?);
50 symbols.extend(extract_properties(source, &root_node, &language.into())?);
51 symbols.extend(extract_events(source, &root_node, &language.into())?);
52 symbols.extend(extract_indexers(source, &root_node, &language.into())?);
53 symbols.extend(extract_local_variables(source, &root_node, &language.into())?);
54
55 for symbol in &mut symbols {
57 symbol.path = path.to_string();
58 symbol.lang = Language::CSharp;
59 }
60
61 Ok(symbols)
62}
63
64fn extract_namespaces(
66 source: &str,
67 root: &tree_sitter::Node,
68 language: &tree_sitter::Language,
69) -> Result<Vec<SearchResult>> {
70 let query_str = r#"
71 (namespace_declaration
72 name: (_) @name) @namespace
73
74 (file_scoped_namespace_declaration
75 name: (_) @name) @namespace
76 "#;
77
78 let query = Query::new(language, query_str)
79 .context("Failed to create namespace query")?;
80
81 extract_symbols(source, root, &query, SymbolKind::Namespace, None)
82}
83
84fn extract_classes(
86 source: &str,
87 root: &tree_sitter::Node,
88 language: &tree_sitter::Language,
89) -> Result<Vec<SearchResult>> {
90 let query_str = r#"
91 (class_declaration
92 name: (identifier) @name) @class
93 "#;
94
95 let query = Query::new(language, query_str)
96 .context("Failed to create class query")?;
97
98 extract_symbols(source, root, &query, SymbolKind::Class, None)
99}
100
101fn extract_interfaces(
103 source: &str,
104 root: &tree_sitter::Node,
105 language: &tree_sitter::Language,
106) -> Result<Vec<SearchResult>> {
107 let query_str = r#"
108 (interface_declaration
109 name: (identifier) @name) @interface
110 "#;
111
112 let query = Query::new(language, query_str)
113 .context("Failed to create interface query")?;
114
115 extract_symbols(source, root, &query, SymbolKind::Interface, None)
116}
117
118fn extract_structs(
120 source: &str,
121 root: &tree_sitter::Node,
122 language: &tree_sitter::Language,
123) -> Result<Vec<SearchResult>> {
124 let query_str = r#"
125 (struct_declaration
126 name: (identifier) @name) @struct
127 "#;
128
129 let query = Query::new(language, query_str)
130 .context("Failed to create struct query")?;
131
132 extract_symbols(source, root, &query, SymbolKind::Struct, None)
133}
134
135fn extract_enums(
137 source: &str,
138 root: &tree_sitter::Node,
139 language: &tree_sitter::Language,
140) -> Result<Vec<SearchResult>> {
141 let query_str = r#"
142 (enum_declaration
143 name: (identifier) @name) @enum
144 "#;
145
146 let query = Query::new(language, query_str)
147 .context("Failed to create enum query")?;
148
149 extract_symbols(source, root, &query, SymbolKind::Enum, None)
150}
151
152fn extract_records(
154 source: &str,
155 root: &tree_sitter::Node,
156 language: &tree_sitter::Language,
157) -> Result<Vec<SearchResult>> {
158 let query_str = r#"
159 (record_declaration
160 name: (identifier) @name) @record
161 "#;
162
163 let query = Query::new(language, query_str)
164 .context("Failed to create record query")?;
165
166 extract_symbols(source, root, &query, SymbolKind::Type, None)
167}
168
169fn extract_delegates(
171 source: &str,
172 root: &tree_sitter::Node,
173 language: &tree_sitter::Language,
174) -> Result<Vec<SearchResult>> {
175 let query_str = r#"
176 (delegate_declaration
177 name: (identifier) @name) @delegate
178 "#;
179
180 let query = Query::new(language, query_str)
181 .context("Failed to create delegate query")?;
182
183 extract_symbols(source, root, &query, SymbolKind::Type, None)
184}
185
186fn extract_attributes(
190 source: &str,
191 root: &tree_sitter::Node,
192 language: &tree_sitter::Language,
193) -> Result<Vec<SearchResult>> {
194 let mut symbols = Vec::new();
195
196 let def_query_str = r#"
198 (class_declaration
199 name: (identifier) @name) @class
200 "#;
201
202 let def_query = Query::new(language, def_query_str)
203 .context("Failed to create attribute definition query")?;
204
205 let mut cursor = QueryCursor::new();
206 let mut matches = cursor.matches(&def_query, *root, source.as_bytes());
207
208 while let Some(match_) = matches.next() {
209 let mut name = None;
210 let mut full_node = None;
211
212 for capture in match_.captures {
213 let capture_name: &str = &def_query.capture_names()[capture.index as usize];
214 match capture_name {
215 "name" => {
216 name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
217 }
218 "class" => {
219 full_node = Some(capture.node);
220 }
221 _ => {}
222 }
223 }
224
225 if let (Some(name), Some(node)) = (name, full_node) {
228 let mut is_attribute = name.ends_with("Attribute");
229
230 if !is_attribute {
232 for i in 0..node.child_count() {
234 if let Some(child) = node.child(i) {
235 if child.kind() == "base_list" {
236 let base_text = child.utf8_text(source.as_bytes()).unwrap_or("");
237 if base_text.contains("Attribute") {
238 is_attribute = true;
239 break;
240 }
241 }
242 }
243 }
244 }
245
246 if is_attribute {
247 let span = node_to_span(&node);
248 let preview = extract_preview(source, &span);
249
250 symbols.push(SearchResult::new(
251 String::new(),
252 Language::CSharp,
253 SymbolKind::Attribute,
254 Some(name),
255 span,
256 None,
257 preview,
258 ));
259 }
260 }
261 }
262
263 let use_query_str = r#"
265 (attribute_list
266 (attribute
267 name: (_) @name)) @attr
268 "#;
269
270 let use_query = Query::new(language, use_query_str)
271 .context("Failed to create attribute use query")?;
272
273 symbols.extend(extract_symbols(source, root, &use_query, SymbolKind::Attribute, None)?);
274
275 Ok(symbols)
276}
277
278fn extract_methods(
280 source: &str,
281 root: &tree_sitter::Node,
282 language: &tree_sitter::Language,
283) -> Result<Vec<SearchResult>> {
284 let query_str = r#"
285 (class_declaration
286 name: (identifier) @class_name
287 body: (declaration_list
288 (method_declaration
289 name: (identifier) @method_name))) @class
290
291 (struct_declaration
292 name: (identifier) @struct_name
293 body: (declaration_list
294 (method_declaration
295 name: (identifier) @method_name))) @struct
296
297 (interface_declaration
298 name: (identifier) @interface_name
299 body: (declaration_list
300 (method_declaration
301 name: (identifier) @method_name))) @interface
302
303 (record_declaration
304 name: (identifier) @record_name
305 body: (declaration_list
306 (method_declaration
307 name: (identifier) @method_name))) @record
308 "#;
309
310 let query = Query::new(language, query_str)
311 .context("Failed to create method query")?;
312
313 let mut cursor = QueryCursor::new();
314 let mut matches = cursor.matches(&query, *root, source.as_bytes());
315
316 let mut symbols = Vec::new();
317
318 while let Some(match_) = matches.next() {
319 let mut scope_name = None;
320 let mut scope_type = None;
321 let mut method_name = None;
322 let mut method_node = None;
323
324 for capture in match_.captures {
325 let capture_name: &str = &query.capture_names()[capture.index as usize];
326 match capture_name {
327 "class_name" => {
328 scope_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
329 scope_type = Some("class");
330 }
331 "struct_name" => {
332 scope_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
333 scope_type = Some("struct");
334 }
335 "interface_name" => {
336 scope_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
337 scope_type = Some("interface");
338 }
339 "record_name" => {
340 scope_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
341 scope_type = Some("record");
342 }
343 "method_name" => {
344 method_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
345 let mut current = capture.node;
347 while let Some(parent) = current.parent() {
348 if parent.kind() == "method_declaration" {
349 method_node = Some(parent);
350 break;
351 }
352 current = parent;
353 }
354 }
355 _ => {}
356 }
357 }
358
359 if let (Some(scope_name), Some(scope_type), Some(method_name), Some(node)) =
360 (scope_name, scope_type, method_name, method_node) {
361 let scope = format!("{} {}", scope_type, scope_name);
362 let span = node_to_span(&node);
363 let preview = extract_preview(source, &span);
364
365 symbols.push(SearchResult::new(
366 String::new(),
367 Language::CSharp,
368 SymbolKind::Method,
369 Some(method_name),
370 span,
371 Some(scope),
372 preview,
373 ));
374 }
375 }
376
377 Ok(symbols)
378}
379
380fn extract_properties(
382 source: &str,
383 root: &tree_sitter::Node,
384 language: &tree_sitter::Language,
385) -> Result<Vec<SearchResult>> {
386 let query_str = r#"
387 (class_declaration
388 name: (identifier) @class_name
389 body: (declaration_list
390 (property_declaration
391 name: (identifier) @property_name))) @class
392
393 (struct_declaration
394 name: (identifier) @struct_name
395 body: (declaration_list
396 (property_declaration
397 name: (identifier) @property_name))) @struct
398
399 (interface_declaration
400 name: (identifier) @interface_name
401 body: (declaration_list
402 (property_declaration
403 name: (identifier) @property_name))) @interface
404
405 (record_declaration
406 name: (identifier) @record_name
407 body: (declaration_list
408 (property_declaration
409 name: (identifier) @property_name))) @record
410 "#;
411
412 let query = Query::new(language, query_str)
413 .context("Failed to create property query")?;
414
415 let mut cursor = QueryCursor::new();
416 let mut matches = cursor.matches(&query, *root, source.as_bytes());
417
418 let mut symbols = Vec::new();
419
420 while let Some(match_) = matches.next() {
421 let mut scope_name = None;
422 let mut scope_type = None;
423 let mut property_name = None;
424 let mut property_node = None;
425
426 for capture in match_.captures {
427 let capture_name: &str = &query.capture_names()[capture.index as usize];
428 match capture_name {
429 "class_name" => {
430 scope_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
431 scope_type = Some("class");
432 }
433 "struct_name" => {
434 scope_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
435 scope_type = Some("struct");
436 }
437 "interface_name" => {
438 scope_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
439 scope_type = Some("interface");
440 }
441 "record_name" => {
442 scope_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
443 scope_type = Some("record");
444 }
445 "property_name" => {
446 property_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
447 let mut current = capture.node;
449 while let Some(parent) = current.parent() {
450 if parent.kind() == "property_declaration" {
451 property_node = Some(parent);
452 break;
453 }
454 current = parent;
455 }
456 }
457 _ => {}
458 }
459 }
460
461 if let (Some(scope_name), Some(scope_type), Some(property_name), Some(node)) =
462 (scope_name, scope_type, property_name, property_node) {
463 let scope = format!("{} {}", scope_type, scope_name);
464 let span = node_to_span(&node);
465 let preview = extract_preview(source, &span);
466
467 symbols.push(SearchResult::new(
468 String::new(),
469 Language::CSharp,
470 SymbolKind::Variable,
471 Some(property_name),
472 span,
473 Some(scope),
474 preview,
475 ));
476 }
477 }
478
479 Ok(symbols)
480}
481
482fn extract_events(
484 source: &str,
485 root: &tree_sitter::Node,
486 language: &tree_sitter::Language,
487) -> Result<Vec<SearchResult>> {
488 let query_str = r#"
489 (class_declaration
490 name: (identifier) @class_name
491 body: (declaration_list
492 (event_field_declaration
493 (variable_declaration
494 (variable_declarator
495 (identifier) @event_name))))) @class
496
497 (struct_declaration
498 name: (identifier) @struct_name
499 body: (declaration_list
500 (event_field_declaration
501 (variable_declaration
502 (variable_declarator
503 (identifier) @event_name))))) @struct
504
505 (interface_declaration
506 name: (identifier) @interface_name
507 body: (declaration_list
508 (event_field_declaration
509 (variable_declaration
510 (variable_declarator
511 (identifier) @event_name))))) @interface
512 "#;
513
514 let query = Query::new(language, query_str)
515 .context("Failed to create event query")?;
516
517 let mut cursor = QueryCursor::new();
518 let mut matches = cursor.matches(&query, *root, source.as_bytes());
519
520 let mut symbols = Vec::new();
521
522 while let Some(match_) = matches.next() {
523 let mut scope_name = None;
524 let mut scope_type = None;
525 let mut event_name = None;
526 let mut event_node = None;
527
528 for capture in match_.captures {
529 let capture_name: &str = &query.capture_names()[capture.index as usize];
530 match capture_name {
531 "class_name" => {
532 scope_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
533 scope_type = Some("class");
534 }
535 "struct_name" => {
536 scope_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
537 scope_type = Some("struct");
538 }
539 "interface_name" => {
540 scope_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
541 scope_type = Some("interface");
542 }
543 "event_name" => {
544 event_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
545 let mut current = capture.node;
547 while let Some(parent) = current.parent() {
548 if parent.kind() == "event_field_declaration" {
549 event_node = Some(parent);
550 break;
551 }
552 current = parent;
553 }
554 }
555 _ => {}
556 }
557 }
558
559 if let (Some(scope_name), Some(scope_type), Some(event_name), Some(node)) =
560 (scope_name, scope_type, event_name, event_node) {
561 let scope = format!("{} {}", scope_type, scope_name);
562 let span = node_to_span(&node);
563 let preview = extract_preview(source, &span);
564
565 symbols.push(SearchResult::new(
566 String::new(),
567 Language::CSharp,
568 SymbolKind::Event,
569 Some(event_name),
570 span,
571 Some(scope),
572 preview,
573 ));
574 }
575 }
576
577 Ok(symbols)
578}
579
580fn extract_indexers(
582 source: &str,
583 root: &tree_sitter::Node,
584 language: &tree_sitter::Language,
585) -> Result<Vec<SearchResult>> {
586 let query_str = r#"
587 (class_declaration
588 name: (identifier) @class_name
589 body: (declaration_list
590 (indexer_declaration) @indexer_name)) @class
591
592 (struct_declaration
593 name: (identifier) @struct_name
594 body: (declaration_list
595 (indexer_declaration) @indexer_name)) @struct
596 "#;
597
598 let query = Query::new(language, query_str)
599 .context("Failed to create indexer query")?;
600
601 let mut cursor = QueryCursor::new();
602 let mut matches = cursor.matches(&query, *root, source.as_bytes());
603
604 let mut symbols = Vec::new();
605
606 while let Some(match_) = matches.next() {
607 let mut scope_name = None;
608 let mut scope_type = None;
609 let mut indexer_node = None;
610
611 for capture in match_.captures {
612 let capture_name: &str = &query.capture_names()[capture.index as usize];
613 match capture_name {
614 "class_name" => {
615 scope_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
616 scope_type = Some("class");
617 }
618 "struct_name" => {
619 scope_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
620 scope_type = Some("struct");
621 }
622 "indexer_name" => {
623 indexer_node = Some(capture.node);
624 }
625 _ => {}
626 }
627 }
628
629 if let (Some(scope_name), Some(scope_type), Some(node)) =
630 (scope_name, scope_type, indexer_node) {
631 let scope = format!("{} {}", scope_type, scope_name);
632 let span = node_to_span(&node);
633 let preview = extract_preview(source, &span);
634
635 symbols.push(SearchResult::new(
637 String::new(),
638 Language::CSharp,
639 SymbolKind::Property,
640 Some("this[]".to_string()),
641 span,
642 Some(scope),
643 preview,
644 ));
645 }
646 }
647
648 Ok(symbols)
649}
650
651fn extract_local_variables(
653 source: &str,
654 root: &tree_sitter::Node,
655 language: &tree_sitter::Language,
656) -> Result<Vec<SearchResult>> {
657 let query_str = r#"
658 (local_declaration_statement
659 (variable_declaration
660 (variable_declarator
661 (identifier) @name))) @var
662 "#;
663
664 let query = Query::new(language, query_str)
665 .context("Failed to create local variable query")?;
666
667 extract_symbols(source, root, &query, SymbolKind::Variable, None)
668}
669
670fn extract_symbols(
672 source: &str,
673 root: &tree_sitter::Node,
674 query: &Query,
675 kind: SymbolKind,
676 scope: Option<String>,
677) -> Result<Vec<SearchResult>> {
678 let mut cursor = QueryCursor::new();
679 let mut matches = cursor.matches(query, *root, source.as_bytes());
680
681 let mut symbols = Vec::new();
682
683 while let Some(match_) = matches.next() {
684 let mut name = None;
686 let mut full_node = None;
687
688 for capture in match_.captures {
689 let capture_name: &str = &query.capture_names()[capture.index as usize];
690 if capture_name == "name" {
691 name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
692 } else {
693 full_node = Some(capture.node);
695 }
696 }
697
698 if let (Some(name), Some(node)) = (name, full_node) {
699 let span = node_to_span(&node);
700 let preview = extract_preview(source, &span);
701
702 symbols.push(SearchResult::new(
703 String::new(),
704 Language::CSharp,
705 kind.clone(),
706 Some(name),
707 span,
708 scope.clone(),
709 preview,
710 ));
711 }
712 }
713
714 Ok(symbols)
715}
716
717fn node_to_span(node: &tree_sitter::Node) -> Span {
719 let start = node.start_position();
720 let end = node.end_position();
721
722 Span::new(
723 start.row + 1, start.column,
725 end.row + 1,
726 end.column,
727 )
728}
729
730fn extract_preview(source: &str, span: &Span) -> String {
732 let lines: Vec<&str> = source.lines().collect();
733
734 let start_idx = (span.start_line - 1) as usize; let end_idx = (start_idx + 7).min(lines.len());
737
738 lines[start_idx..end_idx].join("\n")
739}
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744
745 #[test]
746 fn test_parse_class() {
747 let source = r#"
748public class User
749{
750 private string name;
751 private int age;
752}
753 "#;
754
755 let symbols = parse("test.cs", source).unwrap();
756
757 let class_symbols: Vec<_> = symbols.iter()
758 .filter(|s| matches!(s.kind, SymbolKind::Class))
759 .collect();
760
761 assert_eq!(class_symbols.len(), 1);
762 assert_eq!(class_symbols[0].symbol.as_deref(), Some("User"));
763 }
764
765 #[test]
766 fn test_parse_namespace() {
767 let source = r#"
768namespace MyApp.Models
769{
770 public class User { }
771}
772 "#;
773
774 let symbols = parse("test.cs", source).unwrap();
775
776 let namespace_symbols: Vec<_> = symbols.iter()
777 .filter(|s| matches!(s.kind, SymbolKind::Namespace))
778 .collect();
779
780 assert!(namespace_symbols.len() >= 1);
781 }
782
783 #[test]
784 fn test_parse_file_scoped_namespace() {
785 let source = r#"
786namespace MyApp.Models;
787
788public class User { }
789 "#;
790
791 let symbols = parse("test.cs", source).unwrap();
792
793 let namespace_symbols: Vec<_> = symbols.iter()
794 .filter(|s| matches!(s.kind, SymbolKind::Namespace))
795 .collect();
796
797 assert!(namespace_symbols.len() >= 1);
798 }
799
800 #[test]
801 fn test_parse_interface() {
802 let source = r#"
803public interface IRepository
804{
805 void Save();
806 void Delete();
807}
808 "#;
809
810 let symbols = parse("test.cs", source).unwrap();
811
812 let interface_symbols: Vec<_> = symbols.iter()
813 .filter(|s| matches!(s.kind, SymbolKind::Interface))
814 .collect();
815
816 assert_eq!(interface_symbols.len(), 1);
817 assert_eq!(interface_symbols[0].symbol.as_deref(), Some("IRepository"));
818 }
819
820 #[test]
821 fn test_parse_struct() {
822 let source = r#"
823public struct Point
824{
825 public int X;
826 public int Y;
827}
828 "#;
829
830 let symbols = parse("test.cs", source).unwrap();
831
832 let struct_symbols: Vec<_> = symbols.iter()
833 .filter(|s| matches!(s.kind, SymbolKind::Struct))
834 .collect();
835
836 assert_eq!(struct_symbols.len(), 1);
837 assert_eq!(struct_symbols[0].symbol.as_deref(), Some("Point"));
838 }
839
840 #[test]
841 fn test_parse_enum() {
842 let source = r#"
843public enum Status
844{
845 Active,
846 Inactive,
847 Pending
848}
849 "#;
850
851 let symbols = parse("test.cs", source).unwrap();
852
853 let enum_symbols: Vec<_> = symbols.iter()
854 .filter(|s| matches!(s.kind, SymbolKind::Enum))
855 .collect();
856
857 assert_eq!(enum_symbols.len(), 1);
858 assert_eq!(enum_symbols[0].symbol.as_deref(), Some("Status"));
859 }
860
861 #[test]
862 fn test_parse_record() {
863 let source = r#"
864public record Person(string FirstName, string LastName);
865 "#;
866
867 let symbols = parse("test.cs", source).unwrap();
868
869 let record_symbols: Vec<_> = symbols.iter()
870 .filter(|s| matches!(s.kind, SymbolKind::Type))
871 .filter(|s| s.symbol.as_deref() == Some("Person"))
872 .collect();
873
874 assert_eq!(record_symbols.len(), 1);
875 }
876
877 #[test]
878 fn test_parse_methods() {
879 let source = r#"
880public class Calculator
881{
882 public int Add(int a, int b)
883 {
884 return a + b;
885 }
886
887 public int Subtract(int a, int b)
888 {
889 return a - b;
890 }
891}
892 "#;
893
894 let symbols = parse("test.cs", source).unwrap();
895
896 let method_symbols: Vec<_> = symbols.iter()
897 .filter(|s| matches!(s.kind, SymbolKind::Method))
898 .collect();
899
900 assert_eq!(method_symbols.len(), 2);
901 assert!(method_symbols.iter().any(|s| s.symbol.as_deref() == Some("Add")));
902 assert!(method_symbols.iter().any(|s| s.symbol.as_deref() == Some("Subtract")));
903
904 for method in method_symbols {
906 }
908 }
909
910 #[test]
911 fn test_parse_properties() {
912 let source = r#"
913public class User
914{
915 public string Name { get; set; }
916 public int Age { get; set; }
917 public string Email { get; init; }
918}
919 "#;
920
921 let symbols = parse("test.cs", source).unwrap();
922
923 let property_symbols: Vec<_> = symbols.iter()
924 .filter(|s| matches!(s.kind, SymbolKind::Variable))
925 .collect();
926
927 assert_eq!(property_symbols.len(), 3);
928 assert!(property_symbols.iter().any(|s| s.symbol.as_deref() == Some("Name")));
929 assert!(property_symbols.iter().any(|s| s.symbol.as_deref() == Some("Age")));
930 assert!(property_symbols.iter().any(|s| s.symbol.as_deref() == Some("Email")));
931 }
932
933 #[test]
934 fn test_parse_delegate() {
935 let source = r#"
936public delegate void EventHandler(object sender, EventArgs e);
937 "#;
938
939 let symbols = parse("test.cs", source).unwrap();
940
941 let delegate_symbols: Vec<_> = symbols.iter()
942 .filter(|s| matches!(s.kind, SymbolKind::Type))
943 .filter(|s| s.symbol.as_deref() == Some("EventHandler"))
944 .collect();
945
946 assert_eq!(delegate_symbols.len(), 1);
947 }
948
949 #[test]
950 fn test_parse_mixed_symbols() {
951 let source = r#"
952namespace MyApp
953{
954 public interface IService
955 {
956 void Execute();
957 }
958
959 public class Service : IService
960 {
961 public void Execute()
962 {
963 // Implementation
964 }
965 }
966
967 public enum Priority
968 {
969 Low, Medium, High
970 }
971}
972 "#;
973
974 let symbols = parse("test.cs", source).unwrap();
975
976 assert!(symbols.len() >= 5);
978
979 let kinds: Vec<&SymbolKind> = symbols.iter().map(|s| &s.kind).collect();
980 assert!(kinds.contains(&&SymbolKind::Namespace));
981 assert!(kinds.contains(&&SymbolKind::Interface));
982 assert!(kinds.contains(&&SymbolKind::Class));
983 assert!(kinds.contains(&&SymbolKind::Enum));
984 assert!(kinds.contains(&&SymbolKind::Method));
985 }
986
987 #[test]
988 fn test_local_variables_included() {
989 let source = r#"
990public class Calculator
991{
992 public int Multiplier { get; set; } = 2;
993
994 public int Compute(int input)
995 {
996 int localVar = input * Multiplier;
997 var result = localVar + 10;
998 return result;
999 }
1000}
1001
1002public class Helper
1003{
1004 public static string Format()
1005 {
1006 string message = "Hello";
1007 var count = 5;
1008 return message;
1009 }
1010}
1011 "#;
1012
1013 let symbols = parse("test.cs", source).unwrap();
1014
1015 let variables: Vec<_> = symbols.iter()
1017 .filter(|s| matches!(s.kind, SymbolKind::Variable))
1018 .collect();
1019
1020 assert!(variables.iter().any(|v| v.symbol.as_deref() == Some("localVar")));
1022 assert!(variables.iter().any(|v| v.symbol.as_deref() == Some("result")));
1023 assert!(variables.iter().any(|v| v.symbol.as_deref() == Some("message")));
1024 assert!(variables.iter().any(|v| v.symbol.as_deref() == Some("count")));
1025
1026 assert!(variables.iter().any(|v| v.symbol.as_deref() == Some("Multiplier")));
1028
1029 let local_vars: Vec<_> = variables.iter()
1031 .filter(|v| v.symbol.as_deref() == Some("localVar")
1032 || v.symbol.as_deref() == Some("result")
1033 || v.symbol.as_deref() == Some("message")
1034 || v.symbol.as_deref() == Some("count"))
1035 .collect();
1036
1037 for var in local_vars {
1038 }
1040
1041 let property = variables.iter()
1043 .find(|v| v.symbol.as_deref() == Some("Multiplier"))
1044 .unwrap();
1045 }
1047
1048 #[test]
1049 fn test_parse_events() {
1050 let source = r#"
1051public class Button
1052{
1053 public event EventHandler Click;
1054 public event Action Hover;
1055}
1056
1057public interface INotifier
1058{
1059 event EventHandler<string> Notify;
1060}
1061 "#;
1062
1063 let symbols = parse("test.cs", source).unwrap();
1064
1065 let event_symbols: Vec<_> = symbols.iter()
1066 .filter(|s| matches!(s.kind, SymbolKind::Event))
1067 .collect();
1068
1069 assert_eq!(event_symbols.len(), 3);
1070 assert!(event_symbols.iter().any(|s| s.symbol.as_deref() == Some("Click")));
1071 assert!(event_symbols.iter().any(|s| s.symbol.as_deref() == Some("Hover")));
1072 assert!(event_symbols.iter().any(|s| s.symbol.as_deref() == Some("Notify")));
1073
1074 let click_event = event_symbols.iter()
1076 .find(|s| s.symbol.as_deref() == Some("Click"))
1077 .unwrap();
1078 let notify_event = event_symbols.iter()
1081 .find(|s| s.symbol.as_deref() == Some("Notify"))
1082 .unwrap();
1083 }
1085
1086 #[test]
1087 fn test_parse_indexers() {
1088 let source = r#"
1089public class StringCollection
1090{
1091 private string[] items = new string[100];
1092
1093 public string this[int index]
1094 {
1095 get { return items[index]; }
1096 set { items[index] = value; }
1097 }
1098}
1099
1100public struct Matrix
1101{
1102 public int this[int row, int col]
1103 {
1104 get { return 0; }
1105 set { }
1106 }
1107}
1108 "#;
1109
1110 let symbols = parse("test.cs", source).unwrap();
1111
1112 let indexer_symbols: Vec<_> = symbols.iter()
1113 .filter(|s| matches!(s.kind, SymbolKind::Property))
1114 .filter(|s| s.symbol.as_deref() == Some("this[]"))
1115 .collect();
1116
1117 assert_eq!(indexer_symbols.len(), 2);
1118
1119 }
1122
1123 #[test]
1124 fn test_parse_attribute_class() {
1125 let source = r#"
1126using System;
1127
1128// Attribute with naming convention (ends with "Attribute")
1129public class TestAttribute : Attribute
1130{
1131 public string Name { get; set; }
1132 public TestAttribute(string name) { Name = name; }
1133}
1134
1135// Attribute without suffix but inherits from Attribute
1136public class Obsolete : Attribute
1137{
1138 public string Message { get; set; }
1139}
1140
1141// Not an attribute (regular class without "Attribute" suffix)
1142public class RegularClass
1143{
1144 public void DoSomething() { }
1145}
1146
1147// Attribute with only suffix (no explicit inheritance)
1148public class CustomAttribute
1149{
1150 public int Value { get; set; }
1151}
1152 "#;
1153
1154 let symbols = parse("test.cs", source).unwrap();
1155
1156 let attribute_symbols: Vec<_> = symbols.iter()
1157 .filter(|s| matches!(s.kind, SymbolKind::Attribute))
1158 .collect();
1159
1160 assert_eq!(attribute_symbols.len(), 3);
1162 assert!(attribute_symbols.iter().any(|s| s.symbol.as_deref() == Some("TestAttribute")));
1163 assert!(attribute_symbols.iter().any(|s| s.symbol.as_deref() == Some("Obsolete")));
1164 assert!(attribute_symbols.iter().any(|s| s.symbol.as_deref() == Some("CustomAttribute")));
1165
1166 assert!(!attribute_symbols.iter().any(|s| s.symbol.as_deref() == Some("RegularClass")));
1168 }
1169
1170 #[test]
1171 fn test_parse_attribute_uses() {
1172 let source = r#"
1173using System;
1174
1175public class TestAttribute : Attribute { }
1176public class ObsoleteAttribute : Attribute { }
1177
1178[Test]
1179public class TestClass
1180{
1181 [Test]
1182 public void TestMethod1()
1183 {
1184 // Test code
1185 }
1186
1187 [Test]
1188 [Obsolete]
1189 public void TestMethod2()
1190 {
1191 // Another test
1192 }
1193}
1194
1195[Obsolete]
1196public class LegacyClass
1197{
1198 [Test]
1199 public void OldTest()
1200 {
1201 // Legacy test
1202 }
1203}
1204 "#;
1205
1206 let symbols = parse("test.cs", source).unwrap();
1207
1208 let attribute_symbols: Vec<_> = symbols.iter()
1209 .filter(|s| matches!(s.kind, SymbolKind::Attribute))
1210 .collect();
1211
1212 assert!(attribute_symbols.len() >= 6);
1216
1217 let test_count = attribute_symbols.iter()
1219 .filter(|s| {
1220 let symbol = s.symbol.as_deref().unwrap_or("");
1221 symbol == "Test" || symbol == "TestAttribute"
1222 })
1223 .count();
1224
1225 let obsolete_count = attribute_symbols.iter()
1226 .filter(|s| {
1227 let symbol = s.symbol.as_deref().unwrap_or("");
1228 symbol == "Obsolete" || symbol == "ObsoleteAttribute"
1229 })
1230 .count();
1231
1232 assert!(test_count >= 4);
1234
1235 assert!(obsolete_count >= 3);
1237 }
1238
1239 #[test]
1240 fn test_extract_csharp_usings() {
1241 let source = r#"
1242 using System;
1243 using System.Collections.Generic;
1244 using System.Linq;
1245 using Microsoft.AspNetCore.Mvc;
1246
1247 namespace MyApp.Controllers
1248 {
1249 public class HomeController : Controller
1250 {
1251 public IActionResult Index()
1252 {
1253 return View();
1254 }
1255 }
1256 }
1257 "#;
1258
1259 use crate::parsers::DependencyExtractor;
1260
1261 let deps = CSharpDependencyExtractor::extract_dependencies(source).unwrap();
1262
1263 assert_eq!(deps.len(), 4, "Should extract 4 using directives");
1264 assert!(deps.iter().any(|d| d.imported_path == "System"));
1265 assert!(deps.iter().any(|d| d.imported_path == "System.Collections.Generic"));
1266 assert!(deps.iter().any(|d| d.imported_path == "System.Linq"));
1267 assert!(deps.iter().any(|d| d.imported_path == "Microsoft.AspNetCore.Mvc"));
1268
1269 for dep in &deps {
1270 assert!(matches!(dep.import_type, ImportType::Stdlib),
1271 "System and Microsoft namespaces should be classified as Stdlib");
1272 }
1273 }
1274}
1275
1276use crate::models::ImportType;
1281use crate::parsers::{DependencyExtractor, ImportInfo};
1282
1283pub struct CSharpDependencyExtractor;
1285
1286impl DependencyExtractor for CSharpDependencyExtractor {
1287 fn extract_dependencies(source: &str) -> Result<Vec<ImportInfo>> {
1288 let mut parser = Parser::new();
1289 let language = tree_sitter_c_sharp::LANGUAGE;
1290
1291 parser
1292 .set_language(&language.into())
1293 .context("Failed to set C# language")?;
1294
1295 let tree = parser
1296 .parse(source, None)
1297 .context("Failed to parse C# source")?;
1298
1299 let root_node = tree.root_node();
1300
1301 let mut imports = Vec::new();
1302
1303 imports.extend(extract_csharp_usings(source, &root_node)?);
1305
1306 Ok(imports)
1307 }
1308}
1309
1310fn extract_csharp_usings(
1312 source: &str,
1313 root: &tree_sitter::Node,
1314) -> Result<Vec<ImportInfo>> {
1315 let language = tree_sitter_c_sharp::LANGUAGE;
1316
1317 let query_str = r#"
1318 (using_directive
1319 [
1320 (qualified_name) @using_path
1321 (identifier) @using_path
1322 ])
1323 "#;
1324
1325 let query = Query::new(&language.into(), query_str)
1326 .context("Failed to create C# using query")?;
1327
1328 let mut cursor = QueryCursor::new();
1329 let mut matches = cursor.matches(&query, *root, source.as_bytes());
1330
1331 let mut imports = Vec::new();
1332
1333 while let Some(match_) = matches.next() {
1334 for capture in match_.captures {
1335 let capture_name: &str = &query.capture_names()[capture.index as usize];
1336 if capture_name == "using_path" {
1337 let path = capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string();
1338 let import_type = classify_csharp_using(&path);
1339 let line_number = capture.node.start_position().row + 1;
1340
1341 imports.push(ImportInfo {
1342 imported_path: path,
1343 import_type,
1344 line_number,
1345 imported_symbols: None, });
1347 }
1348 }
1349 }
1350
1351 Ok(imports)
1352}
1353
1354fn classify_csharp_using(using_path: &str) -> ImportType {
1356 const CSHARP_STDLIB_NAMESPACES: &[&str] = &[
1358 "System", "System.Collections", "System.Collections.Generic", "System.Collections.Concurrent",
1360 "System.Collections.Immutable", "System.Collections.ObjectModel", "System.Collections.Specialized",
1361 "System.ComponentModel", "System.ComponentModel.DataAnnotations",
1362 "System.Configuration", "System.Data", "System.Data.Common", "System.Data.SqlClient",
1363 "System.Diagnostics", "System.Diagnostics.CodeAnalysis", "System.Diagnostics.Contracts",
1364 "System.Drawing", "System.Globalization", "System.IO", "System.IO.Compression",
1365 "System.IO.Pipes", "System.Linq", "System.Linq.Expressions", "System.Net",
1366 "System.Net.Http", "System.Net.Mail", "System.Net.Sockets", "System.Numerics",
1367 "System.Reflection", "System.Reflection.Emit", "System.Resources", "System.Runtime",
1368 "System.Runtime.CompilerServices", "System.Runtime.InteropServices", "System.Runtime.Serialization",
1369 "System.Security", "System.Security.Cryptography", "System.Security.Principal",
1370 "System.Text", "System.Text.Json", "System.Text.RegularExpressions", "System.Threading",
1371 "System.Threading.Tasks", "System.Timers", "System.Xml", "System.Xml.Linq",
1372 "System.Xml.Serialization",
1373
1374 "Microsoft.AspNetCore", "Microsoft.AspNetCore.Builder", "Microsoft.AspNetCore.Hosting",
1376 "Microsoft.AspNetCore.Http", "Microsoft.AspNetCore.Mvc", "Microsoft.AspNetCore.Routing",
1377 "Microsoft.AspNetCore.Authentication", "Microsoft.AspNetCore.Authorization",
1378
1379 "Microsoft.EntityFrameworkCore", "Microsoft.EntityFrameworkCore.Design",
1381 "Microsoft.EntityFrameworkCore.Migrations",
1382
1383 "Microsoft.Extensions.Configuration", "Microsoft.Extensions.DependencyInjection",
1385 "Microsoft.Extensions.Hosting", "Microsoft.Extensions.Logging", "Microsoft.Extensions.Options",
1386
1387 "Microsoft.Win32", "System.Windows", "System.Windows.Forms", "System.Windows.Controls",
1389 "System.Windows.Data", "System.Windows.Input", "System.Windows.Media",
1390
1391 "System.Xaml",
1393
1394 "System.Web", "System.Web.Mvc", "System.Web.Http",
1396 ];
1397
1398 for stdlib_ns in CSHARP_STDLIB_NAMESPACES {
1400 if using_path == *stdlib_ns || using_path.starts_with(&format!("{}.", stdlib_ns)) {
1401 return ImportType::Stdlib;
1402 }
1403 }
1404
1405 ImportType::Internal
1412}
1413
1414pub fn resolve_csharp_using_to_path(
1436 using_path: &str,
1437 _current_file_path: Option<&str>,
1438) -> Option<String> {
1439 let path_without_extension = using_path.replace('.', "/");
1444
1445 Some(format!("{}.cs", path_without_extension))
1447}
1448
1449#[cfg(test)]
1454mod resolution_tests {
1455 use super::*;
1456
1457 #[test]
1458 fn test_resolve_csharp_using_simple_namespace() {
1459 let result = resolve_csharp_using_to_path(
1460 "MyApp.Models.User",
1461 None,
1462 );
1463
1464 assert_eq!(result, Some("MyApp/Models/User.cs".to_string()));
1465 }
1466
1467 #[test]
1468 fn test_resolve_csharp_using_services() {
1469 let result = resolve_csharp_using_to_path(
1470 "MyApp.Services.UserService",
1471 None,
1472 );
1473
1474 assert_eq!(result, Some("MyApp/Services/UserService.cs".to_string()));
1475 }
1476
1477 #[test]
1478 fn test_resolve_csharp_using_single_level() {
1479 let result = resolve_csharp_using_to_path(
1480 "MyApp",
1481 None,
1482 );
1483
1484 assert_eq!(result, Some("MyApp.cs".to_string()));
1485 }
1486
1487 #[test]
1488 fn test_resolve_csharp_using_deep_namespace() {
1489 let result = resolve_csharp_using_to_path(
1490 "MyApp.Core.Domain.Models.User",
1491 None,
1492 );
1493
1494 assert_eq!(result, Some("MyApp/Core/Domain/Models/User.cs".to_string()));
1495 }
1496}