1use crate::languages::ImportBindingInfo;
2use crate::store::{normalize_path, EdgeRecord, FileDependency, ReferenceRecord, SymbolRecord};
3use anyhow::{Context, Result};
4use once_cell::sync::Lazy;
5use std::collections::{HashMap, HashSet};
6use std::path::Path;
7use tree_sitter::{Language, Node, Parser, TreeCursor};
8
9static KOTLIN_LANGUAGE: Lazy<Language> = Lazy::new(tree_sitter_kotlin_codanna::language);
10
11#[allow(clippy::type_complexity)]
13pub fn index_file(
14 path: &Path,
15 source: &str,
16) -> Result<(
17 Vec<SymbolRecord>,
18 Vec<EdgeRecord>,
19 Vec<ReferenceRecord>,
20 Vec<FileDependency>,
21 Vec<ImportBindingInfo>,
22)> {
23 let mut parser = Parser::new();
24 parser
25 .set_language(&KOTLIN_LANGUAGE)
26 .context("failed to set Kotlin language")?;
27 let tree = parser
28 .parse(source, None)
29 .context("failed to parse Kotlin file")?;
30
31 let mut symbols = Vec::new();
32 let mut edges = Vec::new();
33 let mut declared_spans: HashSet<(usize, usize)> = HashSet::new();
34 let mut symbol_by_name: HashMap<String, String> = HashMap::new();
35
36 {
37 let mut cursor = tree.walk();
38 walk_symbols(
39 path,
40 source,
41 &mut cursor,
42 None,
43 &mut symbols,
44 &mut edges,
45 &mut declared_spans,
46 &mut symbol_by_name,
47 );
48 }
49
50 let references = collect_references(
51 path,
52 source,
53 &tree.root_node(),
54 &declared_spans,
55 &symbol_by_name,
56 );
57
58 let (dependencies, import_bindings) = collect_imports(path, source, &tree.root_node());
59
60 Ok((symbols, edges, references, dependencies, import_bindings))
61}
62
63#[allow(clippy::too_many_arguments)]
65fn walk_symbols(
66 path: &Path,
67 source: &str,
68 cursor: &mut TreeCursor,
69 container: Option<String>,
70 symbols: &mut Vec<SymbolRecord>,
71 edges: &mut Vec<EdgeRecord>,
72 declared_spans: &mut HashSet<(usize, usize)>,
73 symbol_by_name: &mut HashMap<String, String>,
74) {
75 loop {
76 let node = cursor.node();
77 match node.kind() {
78 "class_declaration" => {
79 handle_class(
80 path,
81 source,
82 &node,
83 container.clone(),
84 symbols,
85 edges,
86 declared_spans,
87 symbol_by_name,
88 );
89 }
90 "object_declaration" => {
91 handle_object(
92 path,
93 source,
94 &node,
95 container.clone(),
96 symbols,
97 edges,
98 declared_spans,
99 symbol_by_name,
100 );
101 }
102 "interface_declaration" => {
103 handle_interface(
104 path,
105 source,
106 &node,
107 container.clone(),
108 symbols,
109 edges,
110 declared_spans,
111 symbol_by_name,
112 );
113 }
114 "function_declaration" => {
115 handle_function(
116 path,
117 source,
118 &node,
119 container.clone(),
120 symbols,
121 declared_spans,
122 symbol_by_name,
123 );
124 }
125 "property_declaration" => {
126 handle_property(
127 path,
128 source,
129 &node,
130 container.clone(),
131 symbols,
132 declared_spans,
133 symbol_by_name,
134 );
135 }
136 "companion_object" => {
137 if let Some(name) = find_name(&node, source) {
139 let sym = make_symbol(
140 path,
141 &node,
142 &name,
143 "object",
144 container.clone(),
145 source.as_bytes(),
146 );
147 declared_spans.insert((sym.start as usize, sym.end as usize));
148 symbol_by_name.insert(name.clone(), sym.id.clone());
149 symbols.push(sym);
150 } else {
151 let sym = make_symbol(
153 path,
154 &node,
155 "Companion",
156 "object",
157 container.clone(),
158 source.as_bytes(),
159 );
160 declared_spans.insert((sym.start as usize, sym.end as usize));
161 symbol_by_name.insert("Companion".to_string(), sym.id.clone());
162 symbols.push(sym);
163 }
164 }
165 _ => {}
166 }
167
168 if cursor.goto_first_child() {
170 let child_container = match node.kind() {
171 "class_declaration" | "interface_declaration" | "object_declaration" => {
172 find_name(&node, source).or(container.clone())
173 }
174 _ => container.clone(),
175 };
176 walk_symbols(
177 path,
178 source,
179 cursor,
180 child_container,
181 symbols,
182 edges,
183 declared_spans,
184 symbol_by_name,
185 );
186 cursor.goto_parent();
187 }
188
189 if !cursor.goto_next_sibling() {
190 break;
191 }
192 }
193}
194
195#[allow(clippy::too_many_arguments)]
196fn handle_class(
197 path: &Path,
198 source: &str,
199 node: &Node,
200 container: Option<String>,
201 symbols: &mut Vec<SymbolRecord>,
202 edges: &mut Vec<EdgeRecord>,
203 declared_spans: &mut HashSet<(usize, usize)>,
204 symbol_by_name: &mut HashMap<String, String>,
205) {
206 if let Some(name) = find_name(node, source) {
207 let kind = determine_class_kind(node);
209
210 let sym = make_symbol(
211 path,
212 node,
213 &name,
214 &kind,
215 container.clone(),
216 source.as_bytes(),
217 );
218 declared_spans.insert((sym.start as usize, sym.end as usize));
219 symbol_by_name.insert(name.clone(), sym.id.clone());
220
221 record_inheritance_edges(path, source, node, &sym.id, edges, symbol_by_name);
223
224 symbols.push(sym);
225
226 if kind == "enum_class" {
228 extract_enum_entries(
229 path,
230 source,
231 node,
232 Some(name.clone()),
233 symbols,
234 declared_spans,
235 symbol_by_name,
236 );
237 }
238
239 extract_constructor_properties(
241 path,
242 source,
243 node,
244 Some(name),
245 symbols,
246 declared_spans,
247 symbol_by_name,
248 );
249 }
250}
251
252fn extract_constructor_properties(
254 path: &Path,
255 source: &str,
256 node: &Node,
257 container: Option<String>,
258 symbols: &mut Vec<SymbolRecord>,
259 declared_spans: &mut HashSet<(usize, usize)>,
260 symbol_by_name: &mut HashMap<String, String>,
261) {
262 let mut cursor = node.walk();
264 for child in node.children(&mut cursor) {
265 if child.kind() == "primary_constructor" {
266 let mut param_cursor = child.walk();
268 for param in child.children(&mut param_cursor) {
269 if param.kind() == "class_parameter" {
270 if has_property_binding(¶m) {
272 if let Some(prop_name) = find_parameter_name(¶m, source) {
273 let sym = make_symbol(
274 path,
275 ¶m,
276 &prop_name,
277 "property",
278 container.clone(),
279 source.as_bytes(),
280 );
281 declared_spans.insert((sym.start as usize, sym.end as usize));
282 symbol_by_name.insert(prop_name, sym.id.clone());
283 symbols.push(sym);
284 }
285 }
286 }
287 }
288 break;
289 }
290 }
291}
292
293fn has_property_binding(node: &Node) -> bool {
295 let mut cursor = node.walk();
296 for child in node.children(&mut cursor) {
297 if child.kind() == "binding_pattern_kind" {
298 return true;
299 }
300 }
301 false
302}
303
304fn find_parameter_name(node: &Node, source: &str) -> Option<String> {
306 let mut cursor = node.walk();
307 for child in node.children(&mut cursor) {
308 if child.kind() == "simple_identifier" {
309 let name = slice(source, &child);
310 if !name.is_empty() {
311 return Some(name);
312 }
313 }
314 }
315 None
316}
317
318fn determine_class_kind(node: &Node) -> String {
320 let mut is_data = false;
321 let mut is_sealed = false;
322 let mut is_enum = false;
323 let mut is_interface = false;
324
325 let mut cursor = node.walk();
326 for child in node.children(&mut cursor) {
327 match child.kind() {
328 "modifiers" => {
329 let mut mod_cursor = child.walk();
331 for modifier in child.children(&mut mod_cursor) {
332 if modifier.kind() == "class_modifier" {
333 let mut class_mod_cursor = modifier.walk();
334 for cm in modifier.children(&mut class_mod_cursor) {
335 match cm.kind() {
336 "data" => is_data = true,
337 "sealed" => is_sealed = true,
338 _ => {}
339 }
340 }
341 }
342 }
343 }
344 "enum" => is_enum = true,
345 "interface" => is_interface = true,
346 _ => {}
347 }
348 }
349
350 if is_enum {
352 "enum_class".to_string()
353 } else if is_sealed && is_interface {
354 "sealed_interface".to_string()
355 } else if is_sealed {
356 "sealed_class".to_string()
357 } else if is_data {
358 "data_class".to_string()
359 } else if is_interface {
360 "interface".to_string()
361 } else {
362 "class".to_string()
363 }
364}
365
366fn extract_enum_entries(
368 path: &Path,
369 source: &str,
370 node: &Node,
371 container: Option<String>,
372 symbols: &mut Vec<SymbolRecord>,
373 declared_spans: &mut HashSet<(usize, usize)>,
374 symbol_by_name: &mut HashMap<String, String>,
375) {
376 let mut stack = vec![*node];
377 while let Some(n) = stack.pop() {
378 if n.kind() == "enum_entry" {
379 if let Some(entry_name) = find_name(&n, source) {
380 let sym = make_symbol(
381 path,
382 &n,
383 &entry_name,
384 "enum_entry",
385 container.clone(),
386 source.as_bytes(),
387 );
388 declared_spans.insert((sym.start as usize, sym.end as usize));
389 symbol_by_name.insert(entry_name, sym.id.clone());
390 symbols.push(sym);
391 }
392 }
393 let mut cursor = n.walk();
394 for child in n.children(&mut cursor) {
395 stack.push(child);
396 }
397 }
398}
399
400#[allow(clippy::too_many_arguments)]
401fn handle_object(
402 path: &Path,
403 source: &str,
404 node: &Node,
405 container: Option<String>,
406 symbols: &mut Vec<SymbolRecord>,
407 edges: &mut Vec<EdgeRecord>,
408 declared_spans: &mut HashSet<(usize, usize)>,
409 symbol_by_name: &mut HashMap<String, String>,
410) {
411 if let Some(name) = find_name(node, source) {
412 let sym = make_symbol(
413 path,
414 node,
415 &name,
416 "object",
417 container.clone(),
418 source.as_bytes(),
419 );
420 declared_spans.insert((sym.start as usize, sym.end as usize));
421 symbol_by_name.insert(name.clone(), sym.id.clone());
422
423 record_inheritance_edges(path, source, node, &sym.id, edges, symbol_by_name);
425
426 symbols.push(sym);
427 }
428}
429
430#[allow(clippy::too_many_arguments)]
431fn handle_interface(
432 path: &Path,
433 source: &str,
434 node: &Node,
435 container: Option<String>,
436 symbols: &mut Vec<SymbolRecord>,
437 edges: &mut Vec<EdgeRecord>,
438 declared_spans: &mut HashSet<(usize, usize)>,
439 symbol_by_name: &mut HashMap<String, String>,
440) {
441 if let Some(name) = find_name(node, source) {
442 let sym = make_symbol(
443 path,
444 node,
445 &name,
446 "interface",
447 container.clone(),
448 source.as_bytes(),
449 );
450 declared_spans.insert((sym.start as usize, sym.end as usize));
451 symbol_by_name.insert(name.clone(), sym.id.clone());
452
453 record_inheritance_edges(path, source, node, &sym.id, edges, symbol_by_name);
455
456 symbols.push(sym);
457 }
458}
459
460fn handle_function(
461 path: &Path,
462 source: &str,
463 node: &Node,
464 container: Option<String>,
465 symbols: &mut Vec<SymbolRecord>,
466 declared_spans: &mut HashSet<(usize, usize)>,
467 symbol_by_name: &mut HashMap<String, String>,
468) {
469 if let Some(name) = find_name(node, source) {
470 let receiver_type = extract_receiver_type(node, source);
472
473 let (kind, qualifier) = if let Some(ref recv_type) = receiver_type {
474 let kind = if container.is_some() {
476 "extension_method"
477 } else {
478 "extension_function"
479 };
480 let qual = match &container {
482 Some(c) => Some(format!("{}.{}", c, recv_type)),
483 None => Some(recv_type.clone()),
484 };
485 (kind, qual)
486 } else {
487 let kind = if container.is_some() {
489 "method"
490 } else {
491 "function"
492 };
493 (kind, container.clone())
494 };
495
496 let sym = make_symbol_with_qualifier(
497 path,
498 node,
499 &name,
500 kind,
501 container,
502 qualifier,
503 source.as_bytes(),
504 );
505 declared_spans.insert((sym.start as usize, sym.end as usize));
506 symbol_by_name.insert(name.clone(), sym.id.clone());
507 symbols.push(sym);
508 }
509}
510
511fn extract_receiver_type(node: &Node, source: &str) -> Option<String> {
513 let mut cursor = node.walk();
514 for child in node.children(&mut cursor) {
515 if child.kind() == "receiver_type" {
516 return extract_type_name(&child, source);
518 }
519 }
520 None
521}
522
523fn handle_property(
524 path: &Path,
525 source: &str,
526 node: &Node,
527 container: Option<String>,
528 symbols: &mut Vec<SymbolRecord>,
529 declared_spans: &mut HashSet<(usize, usize)>,
530 symbol_by_name: &mut HashMap<String, String>,
531) {
532 if let Some(name) = find_property_name(node, source) {
533 let receiver_type = extract_receiver_type(node, source);
535
536 let (kind, qualifier) = if let Some(ref recv_type) = receiver_type {
537 let qual = match &container {
539 Some(c) => Some(format!("{}.{}", c, recv_type)),
540 None => Some(recv_type.clone()),
541 };
542 ("extension_property", qual)
543 } else {
544 ("property", container.clone())
545 };
546
547 let sym = make_symbol_with_qualifier(
548 path,
549 node,
550 &name,
551 kind,
552 container,
553 qualifier,
554 source.as_bytes(),
555 );
556 declared_spans.insert((sym.start as usize, sym.end as usize));
557 symbol_by_name.insert(name.clone(), sym.id.clone());
558 symbols.push(sym);
559 }
560}
561
562fn record_inheritance_edges(
564 path: &Path,
565 source: &str,
566 node: &Node,
567 src_id: &str,
568 edges: &mut Vec<EdgeRecord>,
569 symbol_by_name: &HashMap<String, String>,
570) {
571 let mut stack = vec![*node];
573 while let Some(n) = stack.pop() {
574 if n.kind() == "delegation_specifier" || n.kind() == "user_type" {
575 if let Some(type_name) = extract_type_name(&n, source) {
577 let dst_id = symbol_by_name
579 .get(&type_name)
580 .cloned()
581 .unwrap_or_else(|| format!("{}#{}", normalize_path(path), type_name));
582
583 let kind = if type_name
585 .chars()
586 .next()
587 .map(|c| c.is_uppercase())
588 .unwrap_or(false)
589 {
590 "implements"
593 } else {
594 "extends"
595 };
596
597 edges.push(EdgeRecord {
598 src: src_id.to_string(),
599 dst: dst_id,
600 kind: kind.to_string(),
601 });
602 }
603 }
604
605 let mut cursor = n.walk();
607 for child in n.children(&mut cursor) {
608 stack.push(child);
609 }
610 }
611}
612
613fn extract_type_name(node: &Node, source: &str) -> Option<String> {
615 let mut queue = std::collections::VecDeque::new();
618 queue.push_back(*node);
619
620 while let Some(n) = queue.pop_front() {
621 if n.kind() == "type_identifier"
623 || n.kind() == "simple_identifier"
624 || n.kind() == "identifier"
625 {
626 let name = slice(source, &n);
627 if !name.is_empty() {
628 return Some(name);
629 }
630 }
631
632 let mut cursor = n.walk();
634 for child in n.children(&mut cursor) {
635 if child.kind() != "type_arguments" {
637 queue.push_back(child);
638 }
639 }
640 }
641 None
642}
643
644fn collect_references(
646 path: &Path,
647 source: &str,
648 root: &Node,
649 declared_spans: &HashSet<(usize, usize)>,
650 symbol_by_name: &HashMap<String, String>,
651) -> Vec<ReferenceRecord> {
652 let mut refs = Vec::new();
653 let mut stack = vec![*root];
654 let file = normalize_path(path);
655
656 while let Some(node) = stack.pop() {
657 if node.kind() == "simple_identifier" {
658 let span = (node.start_byte(), node.end_byte());
659 if !declared_spans.contains(&span) {
660 let name = slice(source, &node);
661 if let Some(sym_id) = symbol_by_name.get(&name) {
662 refs.push(ReferenceRecord {
663 file: file.clone(),
664 start: node.start_byte() as i64,
665 end: node.end_byte() as i64,
666 symbol_id: sym_id.clone(),
667 });
668 }
669 }
670 }
671
672 let mut cursor = node.walk();
673 for child in node.children(&mut cursor) {
674 stack.push(child);
675 }
676 }
677
678 refs
679}
680
681fn collect_imports(
683 path: &Path,
684 source: &str,
685 root: &Node,
686) -> (Vec<FileDependency>, Vec<ImportBindingInfo>) {
687 let mut dependencies = Vec::new();
688 let mut import_bindings = Vec::new();
689 let from_file = normalize_path(path);
690
691 let mut stack = vec![*root];
692 while let Some(node) = stack.pop() {
693 if node.kind() == "import_header" {
694 if let Some((import_path, alias)) = parse_import(&node, source) {
695 let last_segment = import_path.rsplit('.').next().unwrap_or(&import_path);
698 let local_name = alias.unwrap_or_else(|| last_segment.to_string());
699
700 import_bindings.push(ImportBindingInfo {
701 local_name,
702 source_file: from_file.clone(), original_name: last_segment.to_string(),
704 });
705
706 dependencies.push(FileDependency {
708 from_file: from_file.clone(),
709 to_file: import_path,
710 kind: "import".to_string(),
711 });
712 }
713 }
714
715 let mut cursor = node.walk();
716 for child in node.children(&mut cursor) {
717 stack.push(child);
718 }
719 }
720
721 (dependencies, import_bindings)
722}
723
724fn parse_import(node: &Node, source: &str) -> Option<(String, Option<String>)> {
726 let mut import_path = String::new();
727 let mut alias = None;
728
729 let mut cursor = node.walk();
730 for child in node.children(&mut cursor) {
731 match child.kind() {
732 "identifier" | "simple_identifier" => {
733 if import_path.is_empty() {
734 import_path = slice(source, &child);
735 } else {
736 import_path.push('.');
737 import_path.push_str(&slice(source, &child));
738 }
739 }
740 "import_alias" => {
741 let mut alias_cursor = child.walk();
743 for alias_child in child.children(&mut alias_cursor) {
744 if alias_child.kind() == "simple_identifier"
745 || alias_child.kind() == "identifier"
746 {
747 alias = Some(slice(source, &alias_child));
748 break;
749 }
750 }
751 }
752 _ => {
753 let mut inner_stack = vec![child];
755 while let Some(inner) = inner_stack.pop() {
756 if inner.kind() == "simple_identifier" || inner.kind() == "identifier" {
757 if import_path.is_empty() {
758 import_path = slice(source, &inner);
759 } else {
760 import_path.push('.');
761 import_path.push_str(&slice(source, &inner));
762 }
763 }
764 let mut inner_cursor = inner.walk();
765 for inner_child in inner.children(&mut inner_cursor) {
766 inner_stack.push(inner_child);
767 }
768 }
769 }
770 }
771 }
772
773 if import_path.is_empty() {
774 None
775 } else {
776 Some((import_path, alias))
777 }
778}
779
780fn find_name(node: &Node, source: &str) -> Option<String> {
782 if let Some(name_node) = node.child_by_field_name("name") {
784 let name = slice(source, &name_node);
785 if !name.is_empty() {
786 return Some(name);
787 }
788 }
789
790 let mut cursor = node.walk();
792 for child in node.children(&mut cursor) {
793 if child.kind() == "simple_identifier" || child.kind() == "type_identifier" {
794 let name = slice(source, &child);
795 if !name.is_empty() {
796 return Some(name);
797 }
798 }
799 }
800 None
801}
802
803fn find_property_name(node: &Node, source: &str) -> Option<String> {
805 let mut stack = vec![*node];
807 while let Some(n) = stack.pop() {
808 if n.kind() == "variable_declaration" {
809 if let Some(name) = find_name(&n, source) {
810 return Some(name);
811 }
812 }
813 if n.kind() == "simple_identifier" {
814 let name = slice(source, &n);
815 if !name.is_empty() {
816 return Some(name);
817 }
818 }
819 let mut cursor = n.walk();
820 for child in n.children(&mut cursor) {
821 stack.push(child);
822 }
823 }
824 None
825}
826
827fn make_symbol(
828 path: &Path,
829 node: &Node,
830 name: &str,
831 kind: &str,
832 container: Option<String>,
833 source: &[u8],
834) -> SymbolRecord {
835 let visibility = extract_visibility(node);
836 let content_hash = super::compute_content_hash(source, node.start_byte(), node.end_byte());
837 let qualifier = container.as_ref().map(|c| c.to_string());
838
839 SymbolRecord {
840 id: format!(
841 "{}#{}-{}",
842 normalize_path(path),
843 node.start_byte(),
844 node.end_byte()
845 ),
846 file: normalize_path(path),
847 kind: kind.to_string(),
848 name: name.to_string(),
849 start: node.start_byte() as i64,
850 end: node.end_byte() as i64,
851 qualifier,
852 visibility,
853 container,
854 content_hash,
855 }
856}
857
858fn make_symbol_with_qualifier(
861 path: &Path,
862 node: &Node,
863 name: &str,
864 kind: &str,
865 container: Option<String>,
866 qualifier: Option<String>,
867 source: &[u8],
868) -> SymbolRecord {
869 let visibility = extract_visibility(node);
870 let content_hash = super::compute_content_hash(source, node.start_byte(), node.end_byte());
871
872 SymbolRecord {
873 id: format!(
874 "{}#{}-{}",
875 normalize_path(path),
876 node.start_byte(),
877 node.end_byte()
878 ),
879 file: normalize_path(path),
880 kind: kind.to_string(),
881 name: name.to_string(),
882 start: node.start_byte() as i64,
883 end: node.end_byte() as i64,
884 qualifier,
885 visibility,
886 container,
887 content_hash,
888 }
889}
890
891fn extract_visibility(node: &Node) -> Option<String> {
893 let mut cursor = node.walk();
894 for child in node.children(&mut cursor) {
895 if child.kind() == "modifiers" {
896 let mut mod_cursor = child.walk();
897 for modifier in child.children(&mut mod_cursor) {
898 if modifier.kind() == "visibility_modifier" {
899 let mut vis_cursor = modifier.walk();
900 for vis in modifier.children(&mut vis_cursor) {
901 match vis.kind() {
902 "public" => return Some("public".to_string()),
903 "private" => return Some("private".to_string()),
904 "protected" => return Some("protected".to_string()),
905 "internal" => return Some("internal".to_string()),
906 _ => {}
907 }
908 }
909 }
910 }
911 }
912 }
913 Some("public".to_string())
915}
916
917fn slice(source: &str, node: &Node) -> String {
918 let bytes = node.byte_range();
919 source.get(bytes).unwrap_or_default().trim().to_string()
920}
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925 use std::fs;
926 use tempfile::tempdir;
927
928 #[test]
929 fn extracts_kotlin_symbols() {
930 let dir = tempdir().unwrap();
931 let path = dir.path().join("Test.kt");
932 let source = r#"
933 class Person(val name: String) {
934 fun greet() {
935 println("Hello, $name")
936 }
937 }
938
939 interface Greeter {
940 fun greet()
941 }
942
943 object Singleton {
944 val instance = "single"
945 }
946
947 fun topLevel() {}
948 "#;
949 fs::write(&path, source).unwrap();
950
951 let (symbols, _edges, _refs, _deps, _imports) = index_file(&path, source).unwrap();
952 let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
953
954 assert!(names.contains(&"Person"), "Should find Person class");
955 assert!(names.contains(&"greet"), "Should find greet method");
956 assert!(names.contains(&"Greeter"), "Should find Greeter interface");
957 assert!(names.contains(&"Singleton"), "Should find Singleton object");
958 assert!(names.contains(&"topLevel"), "Should find topLevel function");
959 }
960
961 #[test]
962 fn extracts_visibility_modifiers() {
963 let dir = tempdir().unwrap();
964 let path = dir.path().join("Visibility.kt");
965 let source = r#"
966 public class PublicClass
967 private class PrivateClass
968 internal class InternalClass
969 protected class ProtectedClass
970 "#;
971 fs::write(&path, source).unwrap();
972
973 let (symbols, _edges, _refs, _deps, _imports) = index_file(&path, source).unwrap();
974
975 let public_class = symbols.iter().find(|s| s.name == "PublicClass").unwrap();
976 assert_eq!(public_class.visibility.as_deref(), Some("public"));
977
978 let private_class = symbols.iter().find(|s| s.name == "PrivateClass").unwrap();
979 assert_eq!(private_class.visibility.as_deref(), Some("private"));
980
981 let internal_class = symbols.iter().find(|s| s.name == "InternalClass").unwrap();
982 assert_eq!(internal_class.visibility.as_deref(), Some("internal"));
983 }
984
985 #[test]
986 fn captures_inheritance_edges() {
987 let dir = tempdir().unwrap();
988 let path = dir.path().join("Inheritance.kt");
989 let source = r#"
990 interface Animal {
991 fun speak()
992 }
993
994 open class Mammal
995
996 class Dog : Mammal(), Animal {
997 override fun speak() {}
998 }
999 "#;
1000 fs::write(&path, source).unwrap();
1001
1002 let (symbols, edges, _refs, _deps, _imports) = index_file(&path, source).unwrap();
1003
1004 assert!(symbols.iter().any(|s| s.name == "Dog"));
1005 assert!(symbols.iter().any(|s| s.name == "Animal"));
1006 assert!(symbols.iter().any(|s| s.name == "Mammal"));
1007
1008 assert!(
1010 edges
1011 .iter()
1012 .any(|e| e.kind == "implements" || e.kind == "extends"),
1013 "Should have inheritance edges"
1014 );
1015 }
1016
1017 #[test]
1018 fn extracts_companion_objects() {
1019 let dir = tempdir().unwrap();
1020 let path = dir.path().join("Companion.kt");
1021 let source = r#"
1022 class Factory {
1023 companion object {
1024 fun create(): Factory = Factory()
1025 }
1026 }
1027 "#;
1028 fs::write(&path, source).unwrap();
1029
1030 let (symbols, _edges, _refs, _deps, _imports) = index_file(&path, source).unwrap();
1031 let names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
1032
1033 assert!(names.contains(&"Factory"));
1034 assert!(names.contains(&"Companion"));
1035 assert!(names.contains(&"create"));
1036 }
1037
1038 #[test]
1039 fn extracts_data_sealed_enum_classes() {
1040 let dir = tempdir().unwrap();
1041 let path = dir.path().join("SpecialClasses.kt");
1042 let source = r#"
1043data class Person(val name: String, val age: Int)
1044
1045sealed class Result {
1046 data class Success(val value: String) : Result()
1047 data class Error(val message: String) : Result()
1048}
1049
1050sealed interface Event {
1051 data class Click(val x: Int) : Event
1052 object Close : Event
1053}
1054
1055enum class Color {
1056 RED,
1057 GREEN,
1058 BLUE
1059}
1060
1061enum class Direction(val degrees: Int) {
1062 NORTH(0),
1063 EAST(90),
1064 SOUTH(180),
1065 WEST(270)
1066}
1067 "#;
1068 fs::write(&path, source).unwrap();
1069
1070 let (symbols, _edges, _refs, _deps, _imports) = index_file(&path, source).unwrap();
1071
1072 let person = symbols.iter().find(|s| s.name == "Person").unwrap();
1074 assert_eq!(person.kind, "data_class", "Person should be a data_class");
1075
1076 let result = symbols.iter().find(|s| s.name == "Result").unwrap();
1078 assert_eq!(
1079 result.kind, "sealed_class",
1080 "Result should be a sealed_class"
1081 );
1082
1083 let success = symbols.iter().find(|s| s.name == "Success").unwrap();
1085 assert_eq!(success.kind, "data_class", "Success should be a data_class");
1086 assert_eq!(
1087 success.container.as_deref(),
1088 Some("Result"),
1089 "Success should be inside Result"
1090 );
1091
1092 let event = symbols.iter().find(|s| s.name == "Event").unwrap();
1094 assert_eq!(
1095 event.kind, "sealed_interface",
1096 "Event should be a sealed_interface"
1097 );
1098
1099 let color = symbols.iter().find(|s| s.name == "Color").unwrap();
1101 assert_eq!(color.kind, "enum_class", "Color should be an enum_class");
1102
1103 let red = symbols.iter().find(|s| s.name == "RED").unwrap();
1105 assert_eq!(red.kind, "enum_entry", "RED should be an enum_entry");
1106 assert_eq!(
1107 red.container.as_deref(),
1108 Some("Color"),
1109 "RED should be inside Color"
1110 );
1111
1112 let green = symbols.iter().find(|s| s.name == "GREEN").unwrap();
1113 assert_eq!(green.kind, "enum_entry", "GREEN should be an enum_entry");
1114
1115 let direction = symbols.iter().find(|s| s.name == "Direction").unwrap();
1117 assert_eq!(
1118 direction.kind, "enum_class",
1119 "Direction should be an enum_class"
1120 );
1121
1122 let north = symbols.iter().find(|s| s.name == "NORTH").unwrap();
1123 assert_eq!(north.kind, "enum_entry", "NORTH should be an enum_entry");
1124 assert_eq!(
1125 north.container.as_deref(),
1126 Some("Direction"),
1127 "NORTH should be inside Direction"
1128 );
1129 }
1130
1131 #[test]
1132 fn extracts_extension_functions_and_properties() {
1133 let dir = tempdir().unwrap();
1134 let path = dir.path().join("Extensions.kt");
1135 let source = r#"
1136fun String.addExclamation() = "$this!"
1137
1138fun List<Int>.sum(): Int = this.fold(0) { acc, i -> acc + i }
1139
1140fun <T> MutableList<T>.swap(i: Int, j: Int) {
1141 val tmp = this[i]
1142 this[i] = this[j]
1143 this[j] = tmp
1144}
1145
1146val String.lastChar: Char
1147 get() = this[length - 1]
1148
1149class StringUtils {
1150 fun String.toTitleCase(): String = this.capitalize()
1151}
1152 "#;
1153 fs::write(&path, source).unwrap();
1154
1155 let (symbols, _edges, _refs, _deps, _imports) = index_file(&path, source).unwrap();
1156
1157 let add_excl = symbols.iter().find(|s| s.name == "addExclamation").unwrap();
1159 assert_eq!(
1160 add_excl.kind, "extension_function",
1161 "addExclamation should be an extension_function"
1162 );
1163 assert_eq!(
1164 add_excl.qualifier.as_deref(),
1165 Some("String"),
1166 "addExclamation should have String as qualifier"
1167 );
1168
1169 let sum = symbols.iter().find(|s| s.name == "sum").unwrap();
1171 assert_eq!(
1172 sum.kind, "extension_function",
1173 "sum should be an extension_function"
1174 );
1175 assert_eq!(
1176 sum.qualifier.as_deref(),
1177 Some("List"),
1178 "sum should have List as qualifier"
1179 );
1180
1181 let swap = symbols.iter().find(|s| s.name == "swap").unwrap();
1183 assert_eq!(
1184 swap.kind, "extension_function",
1185 "swap should be an extension_function"
1186 );
1187 assert_eq!(
1188 swap.qualifier.as_deref(),
1189 Some("MutableList"),
1190 "swap should have MutableList as qualifier"
1191 );
1192
1193 let last_char = symbols.iter().find(|s| s.name == "lastChar").unwrap();
1195 assert_eq!(
1196 last_char.kind, "extension_property",
1197 "lastChar should be an extension_property"
1198 );
1199 assert_eq!(
1200 last_char.qualifier.as_deref(),
1201 Some("String"),
1202 "lastChar should have String as qualifier"
1203 );
1204
1205 let to_title = symbols.iter().find(|s| s.name == "toTitleCase").unwrap();
1207 assert_eq!(
1208 to_title.kind, "extension_method",
1209 "toTitleCase should be an extension_method"
1210 );
1211 assert_eq!(
1212 to_title.container.as_deref(),
1213 Some("StringUtils"),
1214 "toTitleCase should be inside StringUtils"
1215 );
1216 assert_eq!(
1217 to_title.qualifier.as_deref(),
1218 Some("StringUtils.String"),
1219 "toTitleCase should have StringUtils.String as qualifier"
1220 );
1221 }
1222
1223 #[test]
1224 fn extracts_constructor_properties() {
1225 let dir = tempdir().unwrap();
1226 let path = dir.path().join("Constructors.kt");
1227 let source = r#"
1228class Person(val name: String, var age: Int, email: String)
1229
1230data class User(val id: Long, val username: String)
1231
1232class Config(private val secret: String, public val host: String)
1233 "#;
1234 fs::write(&path, source).unwrap();
1235
1236 let (symbols, _edges, _refs, _deps, _imports) = index_file(&path, source).unwrap();
1237
1238 let person = symbols.iter().find(|s| s.name == "Person").unwrap();
1240 assert_eq!(person.kind, "class");
1241
1242 let name = symbols.iter().find(|s| s.name == "name").unwrap();
1244 assert_eq!(name.kind, "property", "name should be a property");
1245 assert_eq!(
1246 name.container.as_deref(),
1247 Some("Person"),
1248 "name should be inside Person"
1249 );
1250
1251 let age = symbols.iter().find(|s| s.name == "age").unwrap();
1252 assert_eq!(age.kind, "property", "age should be a property");
1253 assert_eq!(
1254 age.container.as_deref(),
1255 Some("Person"),
1256 "age should be inside Person"
1257 );
1258
1259 assert!(
1261 !symbols.iter().any(|s| s.name == "email"),
1262 "email should not be extracted (no val/var)"
1263 );
1264
1265 let id = symbols.iter().find(|s| s.name == "id").unwrap();
1267 assert_eq!(id.kind, "property", "id should be a property");
1268 assert_eq!(
1269 id.container.as_deref(),
1270 Some("User"),
1271 "id should be inside User"
1272 );
1273
1274 let username = symbols.iter().find(|s| s.name == "username").unwrap();
1275 assert_eq!(username.kind, "property", "username should be a property");
1276
1277 let secret = symbols.iter().find(|s| s.name == "secret").unwrap();
1279 assert_eq!(secret.kind, "property", "secret should be a property");
1280 assert_eq!(
1281 secret.visibility.as_deref(),
1282 Some("private"),
1283 "secret should be private"
1284 );
1285
1286 let host = symbols.iter().find(|s| s.name == "host").unwrap();
1287 assert_eq!(host.kind, "property", "host should be a property");
1288 assert_eq!(
1289 host.visibility.as_deref(),
1290 Some("public"),
1291 "host should be public"
1292 );
1293 }
1294}