1use std::path::{Path, PathBuf};
26
27use clap::Args;
28use tldr_core::walker::walk_project;
29use tree_sitter::Node;
30
31use super::error::{PatternsError, PatternsResult};
32use super::types::{ClassInfo, FunctionInfo, InterfaceInfo, MethodInfo};
33use super::validation::{read_file_safe, validate_directory_path, validate_file_path};
34use crate::output::OutputFormat;
35use tldr_core::ast::ParserPool;
36use tldr_core::types::Language;
37
38#[derive(Debug, Clone, Args)]
44pub struct InterfaceArgs {
45 #[arg(required = true)]
47 pub path: PathBuf,
48
49 #[arg(long)]
51 pub project_root: Option<PathBuf>,
52}
53
54fn function_node_kinds(lang: Language) -> &'static [&'static str] {
60 match lang {
61 Language::Python => &["function_definition"],
62 Language::Rust => &["function_item"],
63 Language::Go => &["function_declaration", "method_declaration"],
64 Language::Java => &["method_declaration", "constructor_declaration"],
65 Language::TypeScript | Language::JavaScript => &[
66 "function_declaration",
67 "method_definition",
68 "arrow_function",
69 ],
70 Language::C | Language::Cpp => &["function_definition"],
71 Language::Ruby => &["method", "singleton_method"],
72 Language::CSharp => &["method_declaration", "constructor_declaration"],
73 Language::Scala => &["function_definition", "def_definition"],
74 Language::Php => &["function_definition", "method_declaration"],
75 Language::Lua | Language::Luau => {
76 &["function_declaration", "function_definition_statement"]
77 }
78 Language::Elixir => &["call"], Language::Ocaml => &["let_binding", "value_definition"],
80 Language::Kotlin | Language::Swift => &["function_declaration"],
83 }
84}
85
86fn class_node_kinds(lang: Language) -> &'static [&'static str] {
88 match lang {
89 Language::Python => &["class_definition"],
90 Language::Rust => &["struct_item", "impl_item", "trait_item", "enum_item"],
91 Language::Go => &["type_declaration"],
92 Language::Java => &[
93 "class_declaration",
94 "interface_declaration",
95 "enum_declaration",
96 ],
97 Language::TypeScript | Language::JavaScript => &[
98 "class_declaration",
99 "interface_declaration",
100 "type_alias_declaration",
101 ],
102 Language::C => &["struct_specifier"],
103 Language::Cpp => &["struct_specifier", "class_specifier"],
104 Language::Ruby => &["class", "module"],
105 Language::CSharp => &[
106 "class_declaration",
107 "interface_declaration",
108 "struct_declaration",
109 ],
110 Language::Scala => &["class_definition", "object_definition", "trait_definition"],
111 Language::Php => &["class_declaration", "interface_declaration"],
112 Language::Lua | Language::Luau => &[], Language::Elixir => &["call"], Language::Ocaml => &["module_definition", "type_definition"],
115 Language::Kotlin => &["class_declaration", "object_declaration"],
120 Language::Swift => &["class_declaration", "protocol_declaration"],
127 }
128}
129
130fn decorator_node_kinds(lang: Language) -> &'static [&'static str] {
132 match lang {
133 Language::Python => &["decorated_definition"],
134 Language::Java => &["annotation"],
135 Language::TypeScript | Language::JavaScript => &["decorator"],
136 Language::CSharp => &["attribute_list"],
137 Language::Rust => &["attribute_item"],
138 _ => &[],
139 }
140}
141
142fn method_node_kinds(lang: Language) -> &'static [&'static str] {
144 match lang {
145 Language::Python => &["function_definition"],
146 Language::Rust => &["function_item"],
147 Language::Go => &["method_declaration"],
148 Language::Java => &["method_declaration", "constructor_declaration"],
149 Language::TypeScript | Language::JavaScript => {
150 &["method_definition", "public_field_definition"]
151 }
152 Language::C | Language::Cpp => &["function_definition"],
153 Language::Ruby => &["method", "singleton_method"],
154 Language::CSharp => &["method_declaration", "constructor_declaration"],
155 Language::Scala => &["function_definition", "def_definition"],
156 Language::Php => &["method_declaration"],
157 Language::Elixir => &["call"],
158 Language::Ocaml => &["let_binding", "value_definition"],
159 _ => &[],
160 }
161}
162
163#[inline]
176pub fn is_public_name(name: &str) -> bool {
177 !name.starts_with('_')
178}
179
180fn is_public_for_lang(name: &str, lang: Language) -> bool {
182 match lang {
183 Language::Python | Language::Ruby | Language::Lua | Language::Luau => {
184 !name.starts_with('_')
185 }
186 Language::Go => {
187 name.chars().next().is_some_and(|c| c.is_uppercase())
189 }
190 _ => true,
193 }
194}
195
196fn is_rust_pub(node: Node, source: &[u8]) -> bool {
198 for i in 0..node.child_count() {
199 if let Some(child) = node.child(i) {
200 if child.kind() == "visibility_modifier" {
201 let text = node_text(child, source);
202 return text.starts_with("pub");
203 }
204 }
205 }
206 false
207}
208
209fn has_public_modifier(node: Node, source: &[u8]) -> bool {
211 if let Some(modifiers) = node.child_by_field_name("modifiers") {
213 let text = node_text(modifiers, source);
214 return text.contains("public");
215 }
216 for i in 0..node.child_count() {
218 if let Some(child) = node.child(i) {
219 let kind = child.kind();
220 if kind == "modifiers" || kind == "modifier" || kind == "access_modifier" {
221 let text = node_text(child, source);
222 if text.contains("public") {
223 return true;
224 }
225 }
226 if kind == "accessibility_modifier" {
228 let text = node_text(child, source);
229 return text == "public";
230 }
231 }
232 }
233 true
236}
237
238fn is_c_static(node: Node, source: &[u8]) -> bool {
240 if let Some(prev) = node.prev_sibling() {
242 if prev.kind() == "storage_class_specifier" {
243 return node_text(prev, source) == "static";
244 }
245 }
246 for i in 0..node.child_count() {
248 if let Some(child) = node.child(i) {
249 if child.kind() == "storage_class_specifier" && node_text(child, source) == "static" {
250 return true;
251 }
252 }
253 }
254 false
255}
256
257fn is_node_public(node: Node, source: &[u8], lang: Language) -> bool {
259 let name = get_node_name(node, source, lang);
260 let name_str = name.as_deref().unwrap_or("");
261
262 match lang {
263 Language::Rust => {
264 if node.kind() == "impl_item" {
274 return true;
275 }
276 is_rust_pub(node, source)
277 }
278 Language::Go => name_str.chars().next().is_some_and(|c| c.is_uppercase()),
279 Language::Python | Language::Ruby | Language::Lua | Language::Luau => {
280 !name_str.starts_with('_')
281 }
282 Language::Java | Language::CSharp => has_public_modifier(node, source),
283 Language::C | Language::Cpp => !is_c_static(node, source),
284 _ => true,
286 }
287}
288
289fn get_node_name<'a>(node: Node<'a>, source: &'a [u8], lang: Language) -> Option<String> {
295 if let Some(name_node) = node.child_by_field_name("name") {
297 return Some(node_text(name_node, source).to_string());
298 }
299
300 match lang {
301 Language::C | Language::Cpp => {
302 if let Some(declarator) = node.child_by_field_name("declarator") {
304 return extract_c_declarator_name(declarator, source);
305 }
306 }
307 Language::Go => {
308 if node.kind() == "type_declaration" {
310 for i in 0..node.child_count() {
311 if let Some(child) = node.child(i) {
312 if child.kind() == "type_spec" {
313 if let Some(name_node) = child.child_by_field_name("name") {
314 return Some(node_text(name_node, source).to_string());
315 }
316 }
317 }
318 }
319 }
320 }
321 Language::Rust => {
322 if node.kind() == "impl_item" {
324 if let Some(type_node) = node.child_by_field_name("type") {
326 return Some(node_text(type_node, source).to_string());
327 }
328 for i in 0..node.child_count() {
330 if let Some(child) = node.child(i) {
331 if child.kind() == "type_identifier" || child.kind() == "generic_type" {
332 return Some(node_text(child, source).to_string());
333 }
334 }
335 }
336 }
337 }
338 Language::Elixir => {
339 if node.kind() == "call" {
342 if let Some(target) = node.child(0) {
343 let target_text = node_text(target, source);
344 if target_text == "def"
345 || target_text == "defp"
346 || target_text == "defmacro"
347 || target_text == "defmacrop"
348 || target_text == "defmodule"
349 {
350 let args_node = node
357 .child_by_field_name("arguments")
358 .or_else(|| node.child(1));
359 if let Some(args) = args_node {
360 let first_arg = if args.kind() == "arguments" {
364 args.child(0)
365 } else {
366 Some(args)
367 };
368 if let Some(first_arg) = first_arg {
369 if first_arg.kind() == "call" {
371 if let Some(fn_name) = first_arg.child(0) {
372 return Some(node_text(fn_name, source).to_string());
373 }
374 }
375 if first_arg.kind() == "binary_operator" {
379 let mut bin_cursor = first_arg.walk();
380 for bin_child in first_arg.children(&mut bin_cursor) {
381 if bin_child.kind() == "call" {
382 if let Some(fname) = bin_child.child(0) {
383 return Some(
384 node_text(fname, source).to_string(),
385 );
386 }
387 }
388 }
389 }
390 return Some(node_text(first_arg, source).to_string());
391 }
392 }
393 }
394 }
395 }
396 }
397 Language::Ocaml => {
398 if node.kind() == "value_definition" {
404 let mut cursor = node.walk();
405 for child in node.children(&mut cursor) {
406 if child.kind() == "let_binding" {
407 if let Some(pat) = child.child_by_field_name("pattern") {
408 return Some(node_text(pat, source).to_string());
409 }
410 }
411 }
412 }
413 if node.kind() == "let_binding" {
414 if let Some(pat) = node.child_by_field_name("pattern") {
415 return Some(node_text(pat, source).to_string());
416 }
417 }
418 if node.kind() == "module_definition" {
430 let mut cursor = node.walk();
431 for child in node.children(&mut cursor) {
432 if child.kind() == "module_binding" {
433 let mut bind_cursor = child.walk();
434 for bind_child in child.children(&mut bind_cursor) {
435 if bind_child.kind() == "module_name" {
436 return Some(node_text(bind_child, source).to_string());
437 }
438 }
439 }
440 }
441 }
442 if node.kind() == "type_definition" {
445 let mut cursor = node.walk();
446 for child in node.children(&mut cursor) {
447 if child.kind() == "type_binding" {
448 let mut bind_cursor = child.walk();
449 for bind_child in child.children(&mut bind_cursor) {
450 if matches!(
451 bind_child.kind(),
452 "type_constructor" | "type_constructor_path"
453 ) {
454 return Some(node_text(bind_child, source).to_string());
455 }
456 }
457 }
458 if matches!(child.kind(), "type_constructor" | "type_constructor_path") {
459 return Some(node_text(child, source).to_string());
460 }
461 }
462 }
463 }
464 Language::Lua | Language::Luau => {
465 for i in 0..node.child_count() {
467 if let Some(child) = node.child(i) {
468 if child.kind() == "identifier" || child.kind() == "dot_index_expression" {
469 return Some(node_text(child, source).to_string());
470 }
471 }
472 }
473 }
474 Language::Ruby => {
475 for i in 0..node.child_count() {
477 if let Some(child) = node.child(i) {
478 if child.kind() == "identifier" || child.kind() == "constant" {
479 return Some(node_text(child, source).to_string());
480 }
481 }
482 }
483 }
484 _ => {}
485 }
486
487 None
488}
489
490fn extract_c_declarator_name(declarator: Node, source: &[u8]) -> Option<String> {
492 if declarator.kind() == "identifier" {
494 return Some(node_text(declarator, source).to_string());
495 }
496 if declarator.kind() == "field_identifier" {
497 return Some(node_text(declarator, source).to_string());
498 }
499 if let Some(inner) = declarator.child_by_field_name("declarator") {
501 return extract_c_declarator_name(inner, source);
502 }
503 if let Some(first) = declarator.child(0) {
505 if first.kind() == "identifier" || first.kind() == "field_identifier" {
506 return Some(node_text(first, source).to_string());
507 }
508 }
509 None
510}
511
512pub fn extract_all_exports(root: Node, source: &[u8]) -> Option<Vec<String>> {
518 let mut cursor = root.walk();
519
520 for child in root.children(&mut cursor) {
521 if child.kind() == "expression_statement" {
522 if let Some(assignment) = child.child(0) {
523 if assignment.kind() == "assignment" {
524 if let Some(left) = assignment.child_by_field_name("left") {
525 if left.kind() == "identifier" {
526 let name = node_text(left, source);
527 if name == "__all__" {
528 if let Some(right) = assignment.child_by_field_name("right") {
529 return extract_list_strings(right, source);
530 }
531 }
532 }
533 }
534 }
535 }
536 }
537 }
538
539 None
540}
541
542fn extract_list_strings(node: Node, source: &[u8]) -> Option<Vec<String>> {
544 if node.kind() != "list" {
545 return None;
546 }
547
548 let mut exports = Vec::new();
549 let mut cursor = node.walk();
550
551 for child in node.children(&mut cursor) {
552 if child.kind() == "string" {
553 let text = node_text(child, source);
554 let cleaned = text
555 .trim_start_matches(['"', '\''])
556 .trim_end_matches(['"', '\'']);
557 exports.push(cleaned.to_string());
558 }
559 }
560
561 if exports.is_empty() {
562 None
563 } else {
564 Some(exports)
565 }
566}
567
568pub fn extract_function_signature(func_node: Node, source: &[u8], lang: Language) -> String {
577 match lang {
578 Language::Python => extract_python_signature(func_node, source),
579 Language::Rust => extract_rust_signature(func_node, source),
580 Language::Go => extract_go_signature(func_node, source),
581 Language::Java | Language::CSharp => extract_java_like_signature(func_node, source),
582 Language::TypeScript | Language::JavaScript => extract_ts_signature(func_node, source),
583 Language::C | Language::Cpp => extract_c_signature(func_node, source),
584 Language::Ruby => extract_ruby_signature(func_node, source),
585 Language::Php => extract_php_signature(func_node, source),
586 Language::Scala => extract_scala_signature(func_node, source),
587 Language::Ocaml => extract_ocaml_signature(func_node, source),
588 Language::Elixir => extract_elixir_signature(func_node, source),
589 _ => extract_generic_signature(func_node, source),
590 }
591}
592
593fn extract_ocaml_signature(func_node: Node, source: &[u8]) -> String {
599 let binding_owned;
601 let binding = if func_node.kind() == "value_definition" {
602 let mut found: Option<Node> = None;
603 let mut cursor = func_node.walk();
604 for child in func_node.children(&mut cursor) {
605 if child.kind() == "let_binding" {
606 found = Some(child);
607 break;
608 }
609 }
610 match found {
611 Some(b) => {
612 binding_owned = b;
613 binding_owned
614 }
615 None => return String::new(),
616 }
617 } else {
618 func_node
619 };
620
621 let mut params = Vec::new();
622 let mut cursor = binding.walk();
623 for child in binding.children(&mut cursor) {
624 if child.kind() == "parameter" {
625 if let Some(pattern) = child.child_by_field_name("pattern") {
629 let text = node_text(pattern, source).trim();
630 if !text.is_empty() {
631 params.push(text.to_string());
632 continue;
633 }
634 }
635 let mut inner_cursor = child.walk();
637 let mut handled = false;
638 for inner in child.children(&mut inner_cursor) {
639 if inner.kind() == "value_pattern" || inner.kind() == "value_name" {
640 params.push(node_text(inner, source).to_string());
641 handled = true;
642 break;
643 }
644 }
645 if !handled {
646 let text = node_text(child, source).trim();
647 if !text.is_empty() {
648 params.push(text.to_string());
649 }
650 }
651 }
652 }
653
654 let mut sig = format!("({})", params.join(", "));
655
656 let return_type = extract_ocaml_signature_return_type(binding, source);
658 if let Some(ret) = return_type {
659 sig.push_str(" : ");
660 sig.push_str(&ret);
661 }
662
663 sig
664}
665
666fn extract_ocaml_signature_return_type(binding: Node, source: &[u8]) -> Option<String> {
667 let mut last_was_colon = false;
668 let mut past_all_params = false;
669 let mut cursor = binding.walk();
670 for child in binding.children(&mut cursor) {
671 let kind = child.kind();
672 if kind == "parameter" {
673 past_all_params = false;
674 last_was_colon = false;
675 continue;
676 }
677 if kind != "parameter" && !past_all_params {
678 past_all_params = true;
679 }
680 if past_all_params && kind == ":" {
681 last_was_colon = true;
682 continue;
683 }
684 if last_was_colon && kind == "=" {
685 return None;
686 }
687 if last_was_colon && kind != "=" {
688 let t = node_text(child, source).trim().to_string();
689 if !t.is_empty() {
690 return Some(t);
691 }
692 last_was_colon = false;
693 }
694 if kind == "=" {
695 break;
696 }
697 }
698 None
699}
700
701fn extract_elixir_signature(func_node: Node, source: &[u8]) -> String {
706 if func_node.kind() != "call" {
707 return String::new();
708 }
709 let args_node = match func_node
711 .child_by_field_name("arguments")
712 .or_else(|| func_node.child(1))
713 {
714 Some(a) => a,
715 None => return String::new(),
716 };
717 let first_arg = if args_node.kind() == "arguments" {
718 match args_node.child(0) {
719 Some(a) => a,
720 None => return String::new(),
721 }
722 } else {
723 args_node
724 };
725
726 if first_arg.kind() == "identifier" {
728 return "()".to_string();
729 }
730
731 let inner_call = match first_arg.kind() {
733 "call" => first_arg,
734 "binary_operator" => {
735 let mut found: Option<Node> = None;
737 let mut cursor = first_arg.walk();
738 for c in first_arg.children(&mut cursor) {
739 if c.kind() == "call" {
740 found = Some(c);
741 break;
742 }
743 }
744 match found {
745 Some(c) => c,
746 None => return String::new(),
747 }
748 }
749 _ => return String::new(),
750 };
751
752 if let Some(call_args) = inner_call.child(1) {
757 if call_args.kind() == "arguments" {
758 let raw = node_text(call_args, source).trim();
759 if raw.starts_with('(') && raw.ends_with(')') {
760 return raw.to_string();
761 }
762 return format!("({})", raw);
763 }
764 }
765 "()".to_string()
766}
767
768fn extract_python_signature(func_node: Node, source: &[u8]) -> String {
770 let mut params = Vec::new();
771
772 if let Some(params_node) = func_node.child_by_field_name("parameters") {
773 let mut cursor = params_node.walk();
774
775 for child in params_node.children(&mut cursor) {
776 match child.kind() {
777 "identifier" => {
778 params.push(node_text(child, source).to_string());
779 }
780 "typed_parameter" => {
781 params.push(extract_typed_parameter(child, source));
782 }
783 "default_parameter" => {
784 params.push(extract_default_parameter(child, source));
785 }
786 "typed_default_parameter" => {
787 params.push(extract_typed_default_parameter(child, source));
788 }
789 "list_splat_pattern" | "dictionary_splat_pattern" => {
790 params.push(node_text(child, source).to_string());
791 }
792 _ => {}
793 }
794 }
795 }
796
797 let params_str = params.join(", ");
798 let mut signature = format!("({})", params_str);
799
800 if let Some(return_type) = func_node.child_by_field_name("return_type") {
801 let return_text = node_text(return_type, source);
802 signature.push_str(" -> ");
803 signature.push_str(return_text);
804 }
805
806 signature
807}
808
809fn extract_rust_signature(func_node: Node, source: &[u8]) -> String {
811 let mut sig = String::new();
812
813 if let Some(params) = func_node.child_by_field_name("parameters") {
814 sig.push_str(node_text(params, source));
815 }
816
817 if let Some(ret) = func_node.child_by_field_name("return_type") {
818 sig.push_str(" -> ");
819 sig.push_str(node_text(ret, source));
820 }
821
822 sig
823}
824
825fn extract_go_signature(func_node: Node, source: &[u8]) -> String {
827 let mut sig = String::new();
828
829 if let Some(params) = func_node.child_by_field_name("parameters") {
830 sig.push_str(node_text(params, source));
831 }
832
833 if let Some(result) = func_node.child_by_field_name("result") {
834 sig.push(' ');
835 sig.push_str(node_text(result, source));
836 }
837
838 sig
839}
840
841fn extract_java_like_signature(func_node: Node, source: &[u8]) -> String {
843 let mut sig = String::new();
844
845 let params_node = func_node.child_by_field_name("parameters").or_else(|| {
847 let mut cursor = func_node.walk();
849 let found = func_node
850 .children(&mut cursor)
851 .find(|&child| child.kind() == "formal_parameters" || child.kind() == "parameter_list");
852 found
853 });
854
855 if let Some(params) = params_node {
856 sig.push_str(node_text(params, source));
857 }
858
859 if let Some(ret) = func_node.child_by_field_name("type") {
861 let ret_text = node_text(ret, source);
863 sig = format!("{}: {}", sig, ret_text);
864 }
865
866 sig
867}
868
869fn extract_ts_signature(func_node: Node, source: &[u8]) -> String {
871 let mut sig = String::new();
872
873 if let Some(params) = func_node.child_by_field_name("parameters") {
874 sig.push_str(node_text(params, source));
875 }
876
877 if let Some(ret) = func_node.child_by_field_name("return_type") {
878 sig.push_str(": ");
879 sig.push_str(node_text(ret, source));
880 }
881
882 sig
883}
884
885fn extract_c_signature(func_node: Node, source: &[u8]) -> String {
887 let mut sig = String::new();
888
889 if let Some(declarator) = func_node.child_by_field_name("declarator") {
890 if let Some(params) = declarator.child_by_field_name("parameters") {
893 sig.push_str(node_text(params, source));
894 }
895 }
896
897 if let Some(type_node) = func_node.child_by_field_name("type") {
899 let type_text = node_text(type_node, source);
900 if !type_text.is_empty() {
901 sig = format!("{}: {}", sig, type_text);
902 }
903 }
904
905 sig
906}
907
908fn extract_ruby_signature(func_node: Node, source: &[u8]) -> String {
910 if let Some(params) = func_node.child_by_field_name("parameters") {
911 node_text(params, source).to_string()
912 } else {
913 let mut cursor = func_node.walk();
915 for child in func_node.children(&mut cursor) {
916 if child.kind() == "method_parameters" {
917 return node_text(child, source).to_string();
918 }
919 }
920 "()".to_string()
921 }
922}
923
924fn extract_php_signature(func_node: Node, source: &[u8]) -> String {
926 let mut sig = String::new();
927
928 if let Some(params) = func_node.child_by_field_name("parameters") {
929 sig.push_str(node_text(params, source));
930 }
931
932 if let Some(ret) = func_node.child_by_field_name("return_type") {
933 sig.push_str(": ");
934 sig.push_str(node_text(ret, source));
935 }
936
937 sig
938}
939
940fn extract_scala_signature(func_node: Node, source: &[u8]) -> String {
942 let mut sig = String::new();
943
944 if let Some(params) = func_node.child_by_field_name("parameters") {
945 sig.push_str(node_text(params, source));
946 }
947
948 if let Some(ret) = func_node.child_by_field_name("return_type") {
949 sig.push_str(": ");
950 sig.push_str(node_text(ret, source));
951 }
952
953 sig
954}
955
956fn extract_generic_signature(func_node: Node, source: &[u8]) -> String {
958 let mut sig = String::new();
959
960 if let Some(params) = func_node.child_by_field_name("parameters") {
961 sig.push_str(node_text(params, source));
962 }
963
964 sig
965}
966
967fn extract_typed_parameter(node: Node, source: &[u8]) -> String {
969 let name = node
970 .child(0)
971 .filter(|c| c.kind() == "identifier")
972 .map(|n| node_text(n, source))
973 .unwrap_or("");
974 let type_hint = node
975 .child_by_field_name("type")
976 .map(|n| node_text(n, source))
977 .unwrap_or("");
978
979 if type_hint.is_empty() {
980 name.to_string()
981 } else {
982 format!("{}: {}", name, type_hint)
983 }
984}
985
986fn extract_default_parameter(node: Node, source: &[u8]) -> String {
988 let name = node
989 .child_by_field_name("name")
990 .map(|n| node_text(n, source))
991 .unwrap_or("");
992 let value = node
993 .child_by_field_name("value")
994 .map(|n| node_text(n, source))
995 .unwrap_or("");
996
997 format!("{} = {}", name, value)
998}
999
1000fn extract_typed_default_parameter(node: Node, source: &[u8]) -> String {
1002 let name = node
1003 .child_by_field_name("name")
1004 .map(|n| node_text(n, source))
1005 .unwrap_or("");
1006 let type_hint = node
1007 .child_by_field_name("type")
1008 .map(|n| node_text(n, source))
1009 .unwrap_or("");
1010 let value = node
1011 .child_by_field_name("value")
1012 .map(|n| node_text(n, source))
1013 .unwrap_or("");
1014
1015 if type_hint.is_empty() {
1016 format!("{} = {}", name, value)
1017 } else {
1018 format!("{}: {} = {}", name, type_hint, value)
1019 }
1020}
1021
1022pub fn extract_function_info(func_node: Node, source: &[u8], lang: Language) -> FunctionInfo {
1028 let name = get_node_name(func_node, source, lang).unwrap_or_default();
1029 let signature = extract_function_signature(func_node, source, lang);
1030 let lineno = func_node.start_position().row as u32 + 1;
1031 let is_async = detect_async(func_node, source, lang);
1032 let docstring = extract_docstring(func_node, source, lang);
1033
1034 FunctionInfo {
1035 name,
1036 signature,
1037 docstring,
1038 lineno,
1039 is_async,
1040 }
1041}
1042
1043fn detect_async(func_node: Node, source: &[u8], lang: Language) -> bool {
1045 match lang {
1046 Language::Python => {
1047 let func_text = node_text(func_node, source);
1048 func_text.starts_with("async ")
1049 }
1050 Language::Rust => {
1051 for i in 0..func_node.child_count() {
1053 if let Some(child) = func_node.child(i) {
1054 if node_text(child, source) == "async" {
1055 return true;
1056 }
1057 }
1058 }
1059 false
1060 }
1061 Language::TypeScript | Language::JavaScript => {
1062 let func_text = node_text(func_node, source);
1064 func_text.starts_with("async ")
1065 }
1066 Language::CSharp => {
1067 if let Some(modifiers) = func_node.child_by_field_name("modifiers") {
1069 return node_text(modifiers, source).contains("async");
1070 }
1071 false
1072 }
1073 Language::Elixir => {
1074 false
1076 }
1077 _ => false,
1078 }
1079}
1080
1081fn extract_docstring(node: Node, source: &[u8], lang: Language) -> Option<String> {
1087 match lang {
1088 Language::Python => extract_python_docstring(node, source),
1089 Language::Rust => extract_rust_doc_comment(node, source),
1090 Language::Go => extract_go_doc_comment(node, source),
1091 Language::Java | Language::CSharp | Language::Scala | Language::Php => {
1092 extract_javadoc_comment(node, source)
1093 }
1094 Language::TypeScript | Language::JavaScript => extract_jsdoc_comment(node, source),
1095 Language::Ruby => extract_ruby_comment(node, source),
1096 Language::Elixir => extract_elixir_doc(node, source),
1097 _ => None,
1098 }
1099}
1100
1101fn extract_python_docstring(node: Node, source: &[u8]) -> Option<String> {
1103 if let Some(body) = node.child_by_field_name("body") {
1104 let mut cursor = body.walk();
1105 let first_stmt = body.children(&mut cursor).next();
1106 if let Some(child) = first_stmt {
1107 if child.kind() == "expression_statement" {
1108 if let Some(expr) = child.child(0) {
1109 if expr.kind() == "string" {
1110 let text = node_text(expr, source);
1111 let cleaned = text
1112 .trim_start_matches("\"\"\"")
1113 .trim_start_matches("'''")
1114 .trim_end_matches("\"\"\"")
1115 .trim_end_matches("'''")
1116 .trim();
1117 return Some(cleaned.to_string());
1118 }
1119 }
1120 }
1121 }
1122 }
1123 None
1124}
1125
1126fn extract_rust_doc_comment(node: Node, source: &[u8]) -> Option<String> {
1128 let mut comments = Vec::new();
1129 let mut prev = node.prev_sibling();
1130
1131 while let Some(sib) = prev {
1132 let kind = sib.kind();
1133 if kind == "line_comment" {
1134 let text = node_text(sib, source);
1135 if text.starts_with("///") || text.starts_with("//!") {
1136 let content = text
1137 .trim_start_matches("///")
1138 .trim_start_matches("//!")
1139 .trim();
1140 comments.push(content.to_string());
1141 } else {
1142 break;
1143 }
1144 } else if kind == "attribute_item" {
1145 } else {
1147 break;
1148 }
1149 prev = sib.prev_sibling();
1150 }
1151
1152 if comments.is_empty() {
1153 None
1154 } else {
1155 comments.reverse();
1156 Some(comments.join("\n"))
1157 }
1158}
1159
1160fn extract_go_doc_comment(node: Node, source: &[u8]) -> Option<String> {
1162 let mut comments = Vec::new();
1163 let mut prev = node.prev_sibling();
1164
1165 while let Some(sib) = prev {
1166 if sib.kind() == "comment" {
1167 let text = node_text(sib, source);
1168 let content = text.trim_start_matches("//").trim();
1169 comments.push(content.to_string());
1170 } else {
1171 break;
1172 }
1173 prev = sib.prev_sibling();
1174 }
1175
1176 if comments.is_empty() {
1177 None
1178 } else {
1179 comments.reverse();
1180 Some(comments.join("\n"))
1181 }
1182}
1183
1184fn extract_javadoc_comment(node: Node, source: &[u8]) -> Option<String> {
1186 let mut prev = node.prev_sibling();
1187
1188 while let Some(sib) = prev {
1189 let kind = sib.kind();
1190 if kind == "block_comment" || kind == "comment" || kind == "multiline_comment" {
1191 let text = node_text(sib, source);
1192 if text.starts_with("/**") {
1193 let cleaned = text
1194 .trim_start_matches("/**")
1195 .trim_end_matches("*/")
1196 .lines()
1197 .map(|l| l.trim().trim_start_matches('*').trim())
1198 .filter(|l| !l.is_empty())
1199 .collect::<Vec<_>>()
1200 .join("\n");
1201 return Some(cleaned);
1202 }
1203 } else if kind == "annotation" || kind == "marker_annotation" || kind == "attribute_list" {
1204 } else {
1206 break;
1207 }
1208 prev = sib.prev_sibling();
1209 }
1210 None
1211}
1212
1213fn extract_jsdoc_comment(node: Node, source: &[u8]) -> Option<String> {
1215 extract_javadoc_comment(node, source)
1216}
1217
1218fn extract_ruby_comment(node: Node, source: &[u8]) -> Option<String> {
1220 let mut comments = Vec::new();
1221 let mut prev = node.prev_sibling();
1222
1223 while let Some(sib) = prev {
1224 if sib.kind() == "comment" {
1225 let text = node_text(sib, source);
1226 let content = text.trim_start_matches('#').trim();
1227 comments.push(content.to_string());
1228 } else {
1229 break;
1230 }
1231 prev = sib.prev_sibling();
1232 }
1233
1234 if comments.is_empty() {
1235 None
1236 } else {
1237 comments.reverse();
1238 Some(comments.join("\n"))
1239 }
1240}
1241
1242fn extract_elixir_doc(node: Node, source: &[u8]) -> Option<String> {
1244 let mut prev = node.prev_sibling();
1245
1246 while let Some(sib) = prev {
1247 if sib.kind() == "unary_operator" || sib.kind() == "call" {
1248 let text = node_text(sib, source);
1249 if text.starts_with("@doc") || text.starts_with("@moduledoc") {
1250 let cleaned = text
1252 .trim_start_matches("@moduledoc")
1253 .trim_start_matches("@doc")
1254 .trim()
1255 .trim_start_matches("\"\"\"")
1256 .trim_end_matches("\"\"\"")
1257 .trim_start_matches('"')
1258 .trim_end_matches('"')
1259 .trim();
1260 if !cleaned.is_empty() {
1261 return Some(cleaned.to_string());
1262 }
1263 }
1264 } else if sib.kind() == "comment" {
1265 } else {
1267 break;
1268 }
1269 prev = sib.prev_sibling();
1270 }
1271 None
1272}
1273
1274pub fn extract_class_info(class_node: Node, source: &[u8], lang: Language) -> ClassInfo {
1280 let name = get_node_name(class_node, source, lang).unwrap_or_default();
1281 let lineno = class_node.start_position().row as u32 + 1;
1282
1283 let bases = extract_base_classes(class_node, source, lang);
1285
1286 let mut methods = Vec::new();
1288 let mut private_method_count = 0u32;
1289
1290 let method_kinds = method_node_kinds(lang);
1291 let body_node = find_body_node(class_node, lang);
1292
1293 if let Some(body) = body_node {
1294 collect_methods_from_body(
1295 body,
1296 source,
1297 lang,
1298 method_kinds,
1299 &mut methods,
1300 &mut private_method_count,
1301 );
1302 }
1303
1304 ClassInfo {
1305 name,
1306 lineno,
1307 bases,
1308 methods,
1309 private_method_count,
1310 }
1311}
1312
1313fn find_body_node<'a>(class_node: Node<'a>, lang: Language) -> Option<Node<'a>> {
1315 if let Some(body) = class_node.child_by_field_name("body") {
1317 return Some(body);
1318 }
1319 if let Some(body) = class_node.child_by_field_name("members") {
1320 return Some(body);
1321 }
1322
1323 match lang {
1324 Language::Rust => {
1325 let mut cursor = class_node.walk();
1327 for child in class_node.children(&mut cursor) {
1328 if child.kind() == "declaration_list" {
1329 return Some(child);
1330 }
1331 }
1332 None
1333 }
1334 Language::Java | Language::CSharp => {
1335 let mut cursor = class_node.walk();
1337 for child in class_node.children(&mut cursor) {
1338 if child.kind() == "class_body"
1339 || child.kind() == "interface_body"
1340 || child.kind() == "enum_body"
1341 || child.kind() == "declaration_list"
1342 {
1343 return Some(child);
1344 }
1345 }
1346 None
1347 }
1348 Language::TypeScript | Language::JavaScript => {
1349 let mut cursor = class_node.walk();
1350 for child in class_node.children(&mut cursor) {
1351 if child.kind() == "class_body" {
1352 return Some(child);
1353 }
1354 }
1355 None
1356 }
1357 Language::Cpp => {
1358 let mut cursor = class_node.walk();
1359 for child in class_node.children(&mut cursor) {
1360 if child.kind() == "field_declaration_list" {
1361 return Some(child);
1362 }
1363 }
1364 None
1365 }
1366 Language::Ruby => {
1367 let mut cursor = class_node.walk();
1369 for child in class_node.children(&mut cursor) {
1370 if child.kind() == "body_statement" {
1371 return Some(child);
1372 }
1373 }
1374 Some(class_node)
1376 }
1377 _ => {
1378 let mut cursor = class_node.walk();
1380 for child in class_node.children(&mut cursor) {
1381 let kind = child.kind();
1382 if kind.contains("body")
1383 || kind.contains("block")
1384 || kind == "declaration_list"
1385 || kind == "template_body"
1386 {
1387 return Some(child);
1388 }
1389 }
1390 None
1391 }
1392 }
1393}
1394
1395fn collect_methods_from_body(
1397 body: Node,
1398 source: &[u8],
1399 lang: Language,
1400 method_kinds: &[&str],
1401 methods: &mut Vec<MethodInfo>,
1402 private_count: &mut u32,
1403) {
1404 let mut cursor = body.walk();
1405 let decorator_kinds = decorator_node_kinds(lang);
1406
1407 for child in body.children(&mut cursor) {
1408 let kind = child.kind();
1409
1410 if method_kinds.contains(&kind) {
1411 let method_name = get_node_name(child, source, lang).unwrap_or_default();
1412 if is_method_public(&method_name, child, source, lang) {
1413 methods.push(extract_method_info(child, source, lang));
1414 } else {
1415 *private_count += 1;
1416 }
1417 } else if decorator_kinds.contains(&kind) {
1418 if let Some(def) = find_definition_in_decorated(child, method_kinds) {
1420 let method_name = get_node_name(def, source, lang).unwrap_or_default();
1421 if is_method_public(&method_name, def, source, lang) {
1422 methods.push(extract_method_info(def, source, lang));
1423 } else {
1424 *private_count += 1;
1425 }
1426 }
1427 }
1428 }
1429}
1430
1431fn is_method_public(name: &str, node: Node, source: &[u8], lang: Language) -> bool {
1433 match lang {
1434 Language::Python | Language::Ruby | Language::Lua | Language::Luau => {
1435 is_public_for_lang(name, lang)
1436 }
1437 Language::Rust => is_rust_pub(node, source),
1438 Language::Go => name.chars().next().is_some_and(|c| c.is_uppercase()),
1439 Language::Java | Language::CSharp => has_public_modifier(node, source),
1440 _ => true,
1441 }
1442}
1443
1444fn extract_base_classes(class_node: Node, source: &[u8], lang: Language) -> Vec<String> {
1446 let mut bases = Vec::new();
1447
1448 match lang {
1449 Language::Python => {
1450 if let Some(superclasses) = class_node.child_by_field_name("superclasses") {
1451 let mut cursor = superclasses.walk();
1452 for child in superclasses.children(&mut cursor) {
1453 if child.kind() == "identifier" || child.kind() == "attribute" {
1454 bases.push(node_text(child, source).to_string());
1455 }
1456 }
1457 }
1458 }
1459 Language::Java | Language::CSharp => {
1460 if let Some(super_node) = class_node.child_by_field_name("superclass") {
1462 bases.push(node_text(super_node, source).to_string());
1463 }
1464 if let Some(interfaces) = class_node.child_by_field_name("interfaces") {
1465 let mut cursor = interfaces.walk();
1466 for child in interfaces.children(&mut cursor) {
1467 if child.kind() == "type_identifier" || child.kind() == "generic_type" {
1468 bases.push(node_text(child, source).to_string());
1469 }
1470 }
1471 }
1472 if let Some(extends) = class_node.child_by_field_name("type_parameters") {
1474 let _ = extends;
1476 }
1477 }
1478 Language::Rust => {
1479 if class_node.kind() == "impl_item" {
1482 if let Some(trait_node) = class_node.child_by_field_name("trait") {
1483 bases.push(node_text(trait_node, source).to_string());
1484 }
1485 }
1486 }
1487 Language::TypeScript | Language::JavaScript => {
1488 let mut cursor = class_node.walk();
1490 for child in class_node.children(&mut cursor) {
1491 if child.kind() == "class_heritage" {
1492 let mut inner_cursor = child.walk();
1493 for clause in child.children(&mut inner_cursor) {
1494 if clause.kind() == "extends_clause" || clause.kind() == "implements_clause"
1495 {
1496 let mut type_cursor = clause.walk();
1497 for type_child in clause.children(&mut type_cursor) {
1498 if type_child.kind() == "identifier"
1499 || type_child.kind() == "type_identifier"
1500 {
1501 bases.push(node_text(type_child, source).to_string());
1502 }
1503 }
1504 }
1505 }
1506 }
1507 }
1508 }
1509 Language::Ruby => {
1510 if let Some(super_node) = class_node.child_by_field_name("superclass") {
1511 bases.push(node_text(super_node, source).to_string());
1512 }
1513 }
1514 Language::Go => {
1515 }
1518 Language::Scala => {
1519 if let Some(extends) = class_node.child_by_field_name("extends") {
1520 bases.push(node_text(extends, source).to_string());
1521 }
1522 }
1523 _ => {}
1524 }
1525
1526 bases
1527}
1528
1529fn find_definition_in_decorated<'a>(node: Node<'a>, target_kinds: &[&str]) -> Option<Node<'a>> {
1531 let mut cursor = node.walk();
1532 let found = node
1533 .children(&mut cursor)
1534 .find(|&child| target_kinds.contains(&child.kind()));
1535 found
1536}
1537
1538fn extract_method_info(func_node: Node, source: &[u8], lang: Language) -> MethodInfo {
1540 let name = get_node_name(func_node, source, lang).unwrap_or_default();
1541 let signature = extract_function_signature(func_node, source, lang);
1542 let is_async = detect_async(func_node, source, lang);
1543
1544 MethodInfo {
1545 name,
1546 signature,
1547 is_async,
1548 }
1549}
1550
1551pub fn extract_interface(path: &Path, source: &str) -> PatternsResult<InterfaceInfo> {
1563 let lang = Language::from_path_with_siblings(path).unwrap_or(Language::Python);
1564 extract_interface_with_lang(path, source, lang)
1565}
1566
1567pub fn extract_interface_with_lang(
1569 path: &Path,
1570 source: &str,
1571 lang: Language,
1572) -> PatternsResult<InterfaceInfo> {
1573 let source_bytes = source.as_bytes();
1574
1575 let pool = ParserPool::new();
1577 let tree = pool
1578 .parse(source, lang)
1579 .map_err(|e| PatternsError::parse_error(path, format!("Failed to parse: {}", e)))?;
1580
1581 let root = tree.root_node();
1582
1583 let explicit_all_exports = if lang == Language::Python {
1585 extract_all_exports(root, source_bytes)
1586 } else {
1587 None
1588 };
1589
1590 let func_kinds = function_node_kinds(lang);
1592 let class_kinds = class_node_kinds(lang);
1593 let decorator_kinds = decorator_node_kinds(lang);
1594
1595 let (functions, classes) = collect_top_level_definitions(
1597 root,
1598 source_bytes,
1599 lang,
1600 func_kinds,
1601 class_kinds,
1602 decorator_kinds,
1603 );
1604
1605 let all_exports = if let Some(explicit) = explicit_all_exports {
1610 explicit
1611 } else {
1612 let mut names: Vec<String> = functions
1613 .iter()
1614 .map(|f| f.name.clone())
1615 .chain(classes.iter().map(|c| c.name.clone()))
1616 .collect();
1617 names.sort();
1618 names.dedup();
1619 names
1620 };
1621
1622 Ok(InterfaceInfo {
1623 file: path.display().to_string(),
1624 all_exports,
1625 functions,
1626 classes,
1627 })
1628}
1629
1630fn is_interface_container(kind: &str) -> bool {
1641 matches!(
1642 kind,
1643 "namespace_definition"
1644 | "namespace_declaration"
1645 | "file_scoped_namespace_declaration"
1646 | "linkage_specification"
1647 | "preproc_if"
1648 | "preproc_ifdef"
1649 | "preproc_else"
1650 | "preproc_elif"
1651 | "preproc_elifdef"
1652 | "declaration_list"
1653 | "ERROR"
1658 | "compound_statement"
1659 | "global_statement"
1662 )
1663}
1664
1665fn needs_deep_walk(lang: Language) -> bool {
1672 matches!(
1673 lang,
1674 Language::Cpp
1675 | Language::C
1676 | Language::CSharp
1677 | Language::Kotlin
1678 | Language::Swift
1679 )
1680}
1681
1682fn collect_top_level_definitions(
1690 root: Node,
1691 source: &[u8],
1692 lang: Language,
1693 func_kinds: &[&str],
1694 class_kinds: &[&str],
1695 decorator_kinds: &[&str],
1696) -> (Vec<FunctionInfo>, Vec<ClassInfo>) {
1697 let mut functions = Vec::new();
1698 let mut classes = Vec::new();
1699 if needs_deep_walk(lang) {
1700 deep_collect(
1705 root,
1706 source,
1707 lang,
1708 func_kinds,
1709 class_kinds,
1710 &mut functions,
1711 &mut classes,
1712 0,
1713 );
1714 } else {
1715 visit_top_level(
1716 root,
1717 source,
1718 lang,
1719 func_kinds,
1720 class_kinds,
1721 decorator_kinds,
1722 &mut functions,
1723 &mut classes,
1724 0,
1725 );
1726 }
1727
1728 if matches!(lang, Language::Rust) {
1735 merge_rust_impl_entries(&mut classes);
1736 }
1737
1738 if matches!(lang, Language::Java | Language::Kotlin) {
1749 flatten_class_methods_to_functions(&classes, &mut functions);
1750 }
1751
1752 (functions, classes)
1753}
1754
1755fn flatten_class_methods_to_functions(
1764 classes: &[ClassInfo],
1765 functions: &mut Vec<FunctionInfo>,
1766) {
1767 use std::collections::HashSet;
1768 let mut seen: HashSet<(String, u32)> = HashSet::new();
1769 for f in functions.iter() {
1770 seen.insert((f.name.clone(), f.lineno));
1771 }
1772 for class in classes {
1773 for method in &class.methods {
1774 let key = (method.name.clone(), class.lineno);
1783 if seen.contains(&key) {
1784 continue;
1785 }
1786 seen.insert(key);
1787 functions.push(FunctionInfo {
1794 name: method.name.clone(),
1795 signature: method.signature.clone(),
1796 docstring: None,
1797 lineno: class.lineno,
1798 is_async: method.is_async,
1799 });
1800 }
1801 }
1802}
1803
1804fn merge_rust_impl_entries(classes: &mut Vec<ClassInfo>) {
1816 use std::collections::HashSet;
1817
1818 let mut struct_like_indices: HashSet<String> = HashSet::new();
1821 for c in classes.iter() {
1822 let key = strip_generics(&c.name);
1827 struct_like_indices.insert(key);
1828 }
1829 let _ = struct_like_indices; let mut name_counts: std::collections::HashMap<String, usize> =
1835 std::collections::HashMap::new();
1836 for c in classes.iter() {
1837 *name_counts.entry(strip_generics(&c.name)).or_insert(0) += 1;
1838 }
1839
1840 let mut canonical_index: std::collections::HashMap<String, usize> =
1845 std::collections::HashMap::new();
1846 let mut to_remove: Vec<usize> = Vec::new();
1847 for (i, c) in classes.iter().enumerate() {
1848 let key = strip_generics(&c.name);
1849 canonical_index.entry(key).or_insert(i);
1850 }
1851
1852 for i in 0..classes.len() {
1853 let key = strip_generics(&classes[i].name);
1854 let canonical = match canonical_index.get(&key) {
1855 Some(&idx) => idx,
1856 None => continue,
1857 };
1858 if i == canonical {
1859 continue;
1860 }
1861 let methods = std::mem::take(&mut classes[i].methods);
1863 let bases = std::mem::take(&mut classes[i].bases);
1864 let private_count = classes[i].private_method_count;
1865
1866 let canonical_entry = &mut classes[canonical];
1867 for m in methods {
1868 let already = canonical_entry.methods.iter().any(|existing| {
1869 existing.name == m.name && existing.signature == m.signature
1870 });
1871 if !already {
1872 canonical_entry.methods.push(m);
1873 }
1874 }
1875 for b in bases {
1876 if !canonical_entry.bases.contains(&b) {
1877 canonical_entry.bases.push(b);
1878 }
1879 }
1880 canonical_entry.private_method_count =
1881 canonical_entry.private_method_count.saturating_add(private_count);
1882 to_remove.push(i);
1883 }
1884
1885 for idx in to_remove.into_iter().rev() {
1887 classes.remove(idx);
1888 }
1889
1890 let _ = name_counts;
1898}
1899
1900fn strip_generics(name: &str) -> String {
1903 if let Some(idx) = name.find('<') {
1904 name[..idx].trim().to_string()
1905 } else {
1906 name.trim().to_string()
1907 }
1908}
1909
1910#[allow(clippy::too_many_arguments)]
1923fn deep_collect(
1924 node: Node,
1925 source: &[u8],
1926 lang: Language,
1927 func_kinds: &[&str],
1928 class_kinds: &[&str],
1929 functions: &mut Vec<FunctionInfo>,
1930 classes: &mut Vec<ClassInfo>,
1931 depth: usize,
1932) {
1933 const MAX_DEEP_WALK_DEPTH: usize = 8;
1938 if depth > MAX_DEEP_WALK_DEPTH {
1939 return;
1940 }
1941 let mut cursor = node.walk();
1942 for child in node.children(&mut cursor) {
1943 let kind = child.kind();
1944 if class_kinds.contains(&kind) {
1945 if !is_inside_class_ancestor(child, class_kinds)
1950 && is_node_public(child, source, lang)
1951 {
1952 let info = extract_class_info(child, source, lang);
1953 if !info.name.is_empty() {
1955 classes.push(info);
1956 }
1957 }
1958 deep_collect(
1962 child,
1963 source,
1964 lang,
1965 func_kinds,
1966 class_kinds,
1967 functions,
1968 classes,
1969 depth + 1,
1970 );
1971 continue;
1972 }
1973 if func_kinds.contains(&kind)
1974 && !is_inside_class_ancestor(child, class_kinds)
1975 && is_node_public(child, source, lang)
1976 {
1977 functions.push(extract_function_info(child, source, lang));
1978 }
1979 deep_collect(
1980 child,
1981 source,
1982 lang,
1983 func_kinds,
1984 class_kinds,
1985 functions,
1986 classes,
1987 depth + 1,
1988 );
1989 }
1990}
1991
1992fn is_inside_class_ancestor(node: Node, class_kinds: &[&str]) -> bool {
1995 let mut current = node.parent();
1996 while let Some(parent) = current {
1997 if class_kinds.contains(&parent.kind()) {
1998 return true;
1999 }
2000 current = parent.parent();
2001 }
2002 false
2003}
2004
2005#[allow(clippy::too_many_arguments)]
2006fn visit_top_level(
2007 node: Node,
2008 source: &[u8],
2009 lang: Language,
2010 func_kinds: &[&str],
2011 class_kinds: &[&str],
2012 decorator_kinds: &[&str],
2013 functions: &mut Vec<FunctionInfo>,
2014 classes: &mut Vec<ClassInfo>,
2015 depth: usize,
2016) {
2017 const MAX_CONTAINER_DEPTH: usize = 8;
2020 if depth > MAX_CONTAINER_DEPTH {
2021 return;
2022 }
2023
2024 let mut cursor = node.walk();
2025
2026 for child in node.children(&mut cursor) {
2027 let kind = child.kind();
2028
2029 if lang == Language::Elixir && kind == "call" {
2041 let target_text = child.child(0).map(|t| node_text(t, source)).unwrap_or("");
2042 match target_text {
2043 "def" | "defmacro" => {
2044 functions.push(extract_function_info(child, source, lang));
2045 }
2046 "defp" | "defmacrop" => {
2047 }
2049 "defmodule" => {
2050 let mut mod_cursor = child.walk();
2053 for mod_child in child.children(&mut mod_cursor) {
2054 if mod_child.kind() == "do_block" {
2055 visit_top_level(
2056 mod_child,
2057 source,
2058 lang,
2059 func_kinds,
2060 class_kinds,
2061 decorator_kinds,
2062 functions,
2063 classes,
2064 depth + 1,
2065 );
2066 }
2067 }
2068 }
2069 _ => {}
2070 }
2071 continue;
2072 }
2073
2074 if func_kinds.contains(&kind) {
2075 if is_node_public(child, source, lang) {
2076 functions.push(extract_function_info(child, source, lang));
2077 }
2078 } else if class_kinds.contains(&kind) {
2079 if is_node_public(child, source, lang) {
2080 classes.push(extract_class_info(child, source, lang));
2081 }
2082 } else if decorator_kinds.contains(&kind) {
2083 if let Some(def) = find_definition_in_decorated(child, func_kinds) {
2085 if is_node_public(def, source, lang) {
2086 functions.push(extract_function_info(def, source, lang));
2087 }
2088 } else if let Some(class_def) = find_definition_in_decorated(child, class_kinds) {
2089 if is_node_public(class_def, source, lang) {
2090 classes.push(extract_class_info(class_def, source, lang));
2091 }
2092 }
2093 } else if is_interface_container(kind) {
2094 visit_top_level(
2098 child,
2099 source,
2100 lang,
2101 func_kinds,
2102 class_kinds,
2103 decorator_kinds,
2104 functions,
2105 classes,
2106 depth + 1,
2107 );
2108 } else if lang == Language::Php {
2109 let mut inner_cursor = child.walk();
2112 for inner_child in child.children(&mut inner_cursor) {
2113 let inner_kind = inner_child.kind();
2114 if func_kinds.contains(&inner_kind) {
2115 if is_node_public(inner_child, source, lang) {
2116 functions.push(extract_function_info(inner_child, source, lang));
2117 }
2118 } else if class_kinds.contains(&inner_kind)
2119 && is_node_public(inner_child, source, lang)
2120 {
2121 classes.push(extract_class_info(inner_child, source, lang));
2122 }
2123 }
2124 }
2125 }
2126}
2127
2128pub fn format_interface_text(info: &InterfaceInfo) -> String {
2134 let mut lines = Vec::new();
2135
2136 lines.push(format!("File: {}", info.file));
2138 lines.push(String::new());
2139
2140 if !info.all_exports.is_empty() {
2143 lines.push("Exports:".to_string());
2144 for name in &info.all_exports {
2145 lines.push(format!(" {}", name));
2146 }
2147 lines.push(String::new());
2148 }
2149
2150 if !info.functions.is_empty() {
2152 lines.push("Functions:".to_string());
2153 for func in &info.functions {
2154 let async_marker = if func.is_async { "async " } else { "" };
2155 lines.push(format!(
2156 " {}def {}{} [line {}]",
2157 async_marker, func.name, func.signature, func.lineno
2158 ));
2159 if let Some(ref doc) = func.docstring {
2160 let doc_preview = if doc.len() > 60 {
2162 format!("{}...", &doc[..57])
2163 } else {
2164 doc.clone()
2165 };
2166 lines.push(format!(" \"{}\"", doc_preview));
2167 }
2168 }
2169 lines.push(String::new());
2170 }
2171
2172 if !info.classes.is_empty() {
2174 lines.push("Classes:".to_string());
2175 for class in &info.classes {
2176 let bases_str = if class.bases.is_empty() {
2177 String::new()
2178 } else {
2179 format!("({})", class.bases.join(", "))
2180 };
2181 lines.push(format!(
2182 " class {}{} [line {}]",
2183 class.name, bases_str, class.lineno
2184 ));
2185
2186 for method in &class.methods {
2187 let async_marker = if method.is_async { "async " } else { "" };
2188 lines.push(format!(
2189 " {}def {}{}",
2190 async_marker, method.name, method.signature
2191 ));
2192 }
2193
2194 if class.private_method_count > 0 {
2195 lines.push(format!(
2196 " ({} private methods)",
2197 class.private_method_count
2198 ));
2199 }
2200 }
2201 lines.push(String::new());
2202 }
2203
2204 let total_methods: u32 = info.classes.iter().map(|c| c.methods.len() as u32).sum();
2206 lines.push(format!(
2207 "Summary: {} functions, {} classes, {} public methods",
2208 info.functions.len(),
2209 info.classes.len(),
2210 total_methods
2211 ));
2212
2213 lines.join("\n")
2214}
2215
2216fn is_supported_source_file(path: &Path) -> bool {
2222 Language::from_path(path).is_some()
2223}
2224
2225pub fn run(args: InterfaceArgs, format: OutputFormat) -> anyhow::Result<()> {
2227 let path = &args.path;
2228
2229 if path.is_dir() {
2230 let canonical_dir = if let Some(ref root) = args.project_root {
2232 super::validation::validate_file_path_in_project(path, root)?
2233 } else {
2234 validate_directory_path(path)?
2235 };
2236
2237 let mut results = Vec::new();
2239 let mut entries: Vec<PathBuf> = walk_project(&canonical_dir)
2240 .filter(|e| e.path().is_file() && is_supported_source_file(e.path()))
2241 .map(|e| e.path().to_path_buf())
2242 .collect();
2243
2244 entries.sort();
2246
2247 for file_path in entries {
2248 let source = read_file_safe(&file_path)?;
2249 match extract_interface(&file_path, &source) {
2250 Ok(info) => results.push(info),
2251 Err(_) => {
2252 continue;
2254 }
2255 }
2256 }
2257
2258 match format {
2260 OutputFormat::Text => {
2261 for info in &results {
2262 println!("{}", format_interface_text(info));
2263 println!();
2264 }
2265 }
2266 OutputFormat::Compact => {
2267 let json = serde_json::to_string(&results)?;
2268 println!("{}", json);
2269 }
2270 _ => {
2271 let json = serde_json::to_string_pretty(&results)?;
2272 println!("{}", json);
2273 }
2274 }
2275 } else {
2276 let canonical_path = if let Some(ref root) = args.project_root {
2278 super::validation::validate_file_path_in_project(path, root)?
2279 } else {
2280 validate_file_path(path)?
2281 };
2282
2283 let source = read_file_safe(&canonical_path)?;
2284 let mut info = extract_interface(&canonical_path, &source)?;
2285
2286 info.file = path.display().to_string();
2291
2292 match format {
2294 OutputFormat::Text => {
2295 println!("{}", format_interface_text(&info));
2296 }
2297 OutputFormat::Compact => {
2298 let json = serde_json::to_string(&info)?;
2299 println!("{}", json);
2300 }
2301 _ => {
2302 let json = serde_json::to_string_pretty(&info)?;
2303 println!("{}", json);
2304 }
2305 }
2306 }
2307
2308 Ok(())
2309}
2310
2311fn node_text<'a>(node: Node, source: &'a [u8]) -> &'a str {
2317 node.utf8_text(source).unwrap_or("")
2318}
2319
2320#[cfg(test)]
2325mod tests {
2326 use super::*;
2327
2328 #[test]
2333 fn test_is_public_name_public() {
2334 assert!(is_public_name("my_function"));
2335 assert!(is_public_name("MyClass"));
2336 assert!(is_public_name("process"));
2337 assert!(is_public_name("x"));
2338 }
2339
2340 #[test]
2341 fn test_is_public_name_private() {
2342 assert!(!is_public_name("_private"));
2343 assert!(!is_public_name("__dunder__"));
2344 assert!(!is_public_name("_PrivateClass"));
2345 assert!(!is_public_name("__init__"));
2346 }
2347
2348 #[test]
2353 fn test_extract_all_exports_present() {
2354 let source = r#"
2355__all__ = ['foo', 'bar', 'Baz']
2356
2357def foo():
2358 pass
2359"#;
2360 let pool = ParserPool::new();
2361 let tree = pool.parse(source, Language::Python).unwrap();
2362 let root = tree.root_node();
2363
2364 let exports = extract_all_exports(root, source.as_bytes());
2365 assert!(exports.is_some());
2366 let exports = exports.unwrap();
2367 assert_eq!(exports.len(), 3);
2368 assert!(exports.contains(&"foo".to_string()));
2369 assert!(exports.contains(&"bar".to_string()));
2370 assert!(exports.contains(&"Baz".to_string()));
2371 }
2372
2373 #[test]
2374 fn test_extract_all_exports_absent() {
2375 let source = r#"
2376def foo():
2377 pass
2378"#;
2379 let pool = ParserPool::new();
2380 let tree = pool.parse(source, Language::Python).unwrap();
2381 let root = tree.root_node();
2382
2383 let exports = extract_all_exports(root, source.as_bytes());
2384 assert!(exports.is_none());
2385 }
2386
2387 #[test]
2392 fn test_extract_function_signature_simple() {
2393 let source = "def foo(x, y): pass";
2394 let pool = ParserPool::new();
2395 let tree = pool.parse(source, Language::Python).unwrap();
2396 let root = tree.root_node();
2397 let func_node = root.child(0).unwrap();
2398
2399 let sig = extract_function_signature(func_node, source.as_bytes(), Language::Python);
2400 assert_eq!(sig, "(x, y)");
2401 }
2402
2403 #[test]
2404 fn test_extract_function_signature_typed() {
2405 let source = "def foo(x: int, y: str) -> bool: pass";
2406 let pool = ParserPool::new();
2407 let tree = pool.parse(source, Language::Python).unwrap();
2408 let root = tree.root_node();
2409 let func_node = root.child(0).unwrap();
2410
2411 let sig = extract_function_signature(func_node, source.as_bytes(), Language::Python);
2412 assert!(sig.contains("x: int"), "sig = {:?}", sig);
2413 assert!(sig.contains("y: str"), "sig = {:?}", sig);
2414 assert!(sig.contains("-> bool"), "sig = {:?}", sig);
2415 }
2416
2417 #[test]
2418 fn test_extract_function_signature_default() {
2419 let source = "def foo(x: int = 10): pass";
2420 let pool = ParserPool::new();
2421 let tree = pool.parse(source, Language::Python).unwrap();
2422 let root = tree.root_node();
2423 let func_node = root.child(0).unwrap();
2424
2425 let sig = extract_function_signature(func_node, source.as_bytes(), Language::Python);
2426 assert!(sig.contains("x: int = 10") || sig.contains("x: int=10"));
2427 }
2428
2429 #[test]
2434 fn test_extract_interface_public_functions() {
2435 let source = r#"
2436def public_func():
2437 """A public function."""
2438 pass
2439
2440def _private_func():
2441 pass
2442"#;
2443 let info = extract_interface(Path::new("test.py"), source).unwrap();
2444
2445 assert_eq!(info.functions.len(), 1);
2446 assert_eq!(info.functions[0].name, "public_func");
2447 }
2448
2449 #[test]
2450 fn test_extract_interface_public_classes() {
2451 let source = r#"
2452class PublicClass:
2453 def public_method(self):
2454 pass
2455
2456 def _private_method(self):
2457 pass
2458
2459class _PrivateClass:
2460 pass
2461"#;
2462 let info = extract_interface(Path::new("test.py"), source).unwrap();
2463
2464 assert_eq!(info.classes.len(), 1);
2465 assert_eq!(info.classes[0].name, "PublicClass");
2466 assert_eq!(info.classes[0].methods.len(), 1);
2467 assert_eq!(info.classes[0].methods[0].name, "public_method");
2468 assert_eq!(info.classes[0].private_method_count, 1);
2469 }
2470
2471 #[test]
2472 fn test_extract_interface_async_function() {
2473 let source = r#"
2474async def async_func():
2475 pass
2476
2477def sync_func():
2478 pass
2479"#;
2480 let info = extract_interface(Path::new("test.py"), source).unwrap();
2481
2482 assert_eq!(info.functions.len(), 2);
2483
2484 let async_fn = info.functions.iter().find(|f| f.name == "async_func");
2485 assert!(async_fn.is_some());
2486 assert!(async_fn.unwrap().is_async);
2487
2488 let sync_fn = info.functions.iter().find(|f| f.name == "sync_func");
2489 assert!(sync_fn.is_some());
2490 assert!(!sync_fn.unwrap().is_async);
2491 }
2492
2493 #[test]
2494 fn test_extract_interface_with_all() {
2495 let source = r#"
2496__all__ = ['foo', 'Bar']
2497
2498def foo():
2499 pass
2500
2501def bar():
2502 pass
2503
2504class Bar:
2505 pass
2506"#;
2507 let info = extract_interface(Path::new("test.py"), source).unwrap();
2508
2509 assert!(!info.all_exports.is_empty());
2512 assert!(info.all_exports.contains(&"foo".to_string()));
2513 assert!(info.all_exports.contains(&"Bar".to_string()));
2514 }
2515
2516 #[test]
2517 fn test_extract_interface_docstrings() {
2518 let source = r#"
2519def documented():
2520 """This is a docstring."""
2521 pass
2522
2523def undocumented():
2524 pass
2525"#;
2526 let info = extract_interface(Path::new("test.py"), source).unwrap();
2527
2528 let documented = info.functions.iter().find(|f| f.name == "documented");
2529 assert!(documented.is_some());
2530 assert!(documented.unwrap().docstring.is_some());
2531 assert!(documented
2532 .unwrap()
2533 .docstring
2534 .as_ref()
2535 .unwrap()
2536 .contains("docstring"));
2537
2538 let undocumented = info.functions.iter().find(|f| f.name == "undocumented");
2539 assert!(undocumented.is_some());
2540 assert!(undocumented.unwrap().docstring.is_none());
2541 }
2542
2543 #[test]
2544 fn test_extract_interface_class_bases() {
2545 let source = r#"
2546class Child(Parent, Mixin):
2547 pass
2548"#;
2549 let info = extract_interface(Path::new("test.py"), source).unwrap();
2550
2551 assert_eq!(info.classes.len(), 1);
2552 assert_eq!(info.classes[0].bases.len(), 2);
2553 assert!(info.classes[0].bases.contains(&"Parent".to_string()));
2554 assert!(info.classes[0].bases.contains(&"Mixin".to_string()));
2555 }
2556
2557 #[test]
2562 fn test_format_interface_text() {
2563 let info = InterfaceInfo {
2564 file: "test.py".to_string(),
2565 all_exports: vec!["foo".to_string()],
2566 functions: vec![FunctionInfo {
2567 name: "foo".to_string(),
2568 signature: "(x: int) -> str".to_string(),
2569 docstring: Some("A function.".to_string()),
2570 lineno: 5,
2571 is_async: false,
2572 }],
2573 classes: vec![ClassInfo {
2574 name: "MyClass".to_string(),
2575 lineno: 10,
2576 bases: vec!["Base".to_string()],
2577 methods: vec![MethodInfo {
2578 name: "method".to_string(),
2579 signature: "(self)".to_string(),
2580 is_async: false,
2581 }],
2582 private_method_count: 2,
2583 }],
2584 };
2585
2586 let text = format_interface_text(&info);
2587 assert!(text.contains("File: test.py"));
2588 assert!(text.contains("foo"));
2589 assert!(text.contains("MyClass"));
2590 assert!(text.contains("Base"));
2591 assert!(text.contains("method"));
2592 assert!(text.contains("2 private methods"));
2593 }
2594
2595 #[test]
2604 fn test_extract_interface_rust_pub_functions() {
2605 let source = r#"
2606/// Adds two numbers.
2607pub fn add(a: i32, b: i32) -> i32 {
2608 a + b
2609}
2610
2611fn private_helper() -> bool {
2612 true
2613}
2614
2615pub async fn async_fetch() -> String {
2616 String::new()
2617}
2618"#;
2619 let info = extract_interface(Path::new("test.rs"), source).unwrap();
2620
2621 assert_eq!(
2622 info.functions.len(),
2623 2,
2624 "Should find 2 pub functions, got: {:?}",
2625 info.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
2626 );
2627
2628 let add_fn = info.functions.iter().find(|f| f.name == "add");
2629 assert!(add_fn.is_some(), "Should find 'add' function");
2630 let add_fn = add_fn.unwrap();
2631 assert!(
2632 add_fn.signature.contains("a: i32"),
2633 "sig = {:?}",
2634 add_fn.signature
2635 );
2636 assert!(
2637 add_fn.signature.contains("-> i32"),
2638 "sig = {:?}",
2639 add_fn.signature
2640 );
2641 assert!(add_fn.docstring.is_some(), "Should have doc comment");
2642 assert!(add_fn
2643 .docstring
2644 .as_ref()
2645 .unwrap()
2646 .contains("Adds two numbers"));
2647 assert!(!add_fn.is_async);
2648
2649 let async_fn = info.functions.iter().find(|f| f.name == "async_fetch");
2650 assert!(async_fn.is_some(), "Should find 'async_fetch' function");
2651 assert!(async_fn.unwrap().is_async);
2652 }
2653
2654 #[test]
2655 fn test_extract_interface_rust_struct_impl() {
2656 let source = r#"
2657pub struct Point {
2658 pub x: f64,
2659 pub y: f64,
2660}
2661
2662impl Point {
2663 pub fn new(x: f64, y: f64) -> Self {
2664 Point { x, y }
2665 }
2666
2667 fn internal(&self) {}
2668}
2669"#;
2670 let info = extract_interface(Path::new("test.rs"), source).unwrap();
2671
2672 assert!(
2674 !info.classes.is_empty(),
2675 "Should find at least struct/impl, got: {:?}",
2676 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2677 );
2678
2679 let point_struct = info.classes.iter().find(|c| c.name == "Point");
2681 assert!(point_struct.is_some(), "Should find Point struct/impl");
2682 }
2683
2684 #[test]
2685 fn test_extract_interface_rust_trait() {
2686 let source = r#"
2687pub trait Drawable {
2688 fn draw(&self);
2689 fn resize(&mut self, factor: f64);
2690}
2691"#;
2692 let info = extract_interface(Path::new("test.rs"), source).unwrap();
2693
2694 let trait_info = info.classes.iter().find(|c| c.name == "Drawable");
2695 assert!(trait_info.is_some(), "Should find Drawable trait");
2696 }
2697
2698 #[test]
2703 fn test_extract_interface_go_exported_functions() {
2704 let source = r#"
2705package main
2706
2707// ProcessData handles data processing.
2708func ProcessData(input string) (string, error) {
2709 return input, nil
2710}
2711
2712func internalHelper() bool {
2713 return true
2714}
2715"#;
2716 let info = extract_interface(Path::new("test.go"), source).unwrap();
2717
2718 assert_eq!(
2720 info.functions.len(),
2721 1,
2722 "Should find 1 exported function, got: {:?}",
2723 info.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
2724 );
2725 assert_eq!(info.functions[0].name, "ProcessData");
2726 assert!(
2727 info.functions[0].docstring.is_some(),
2728 "Should have doc comment"
2729 );
2730 }
2731
2732 #[test]
2737 fn test_extract_interface_typescript_class() {
2738 let source = r#"
2739class UserService {
2740 async fetchUser(id: string): Promise<User> {
2741 return {} as User;
2742 }
2743
2744 private internalMethod(): void {}
2745}
2746
2747function processData(input: string): number {
2748 return input.length;
2749}
2750"#;
2751 let info = extract_interface(Path::new("test.ts"), source).unwrap();
2752
2753 assert!(
2755 !info.functions.is_empty() || !info.classes.is_empty(),
2756 "Should find definitions: functions={:?}, classes={:?}",
2757 info.functions.iter().map(|f| &f.name).collect::<Vec<_>>(),
2758 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2759 );
2760 }
2761
2762 #[test]
2763 fn test_extract_interface_typescript_interface() {
2764 let source = r#"
2765interface User {
2766 id: string;
2767 name: string;
2768 email: string;
2769}
2770
2771type Status = "active" | "inactive";
2772"#;
2773 let info = extract_interface(Path::new("test.ts"), source).unwrap();
2774
2775 assert!(
2776 !info.classes.is_empty(),
2777 "Should find interface/type declarations, got: {:?}",
2778 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2779 );
2780 }
2781
2782 #[test]
2787 fn test_extract_interface_java_class() {
2788 let source = r#"
2789/**
2790 * Service for managing users.
2791 */
2792public class UserService {
2793 public String getUser(String id) {
2794 return id;
2795 }
2796
2797 private void internalCleanup() {}
2798}
2799"#;
2800 let info = extract_interface(Path::new("test.java"), source).unwrap();
2801
2802 assert!(
2803 !info.classes.is_empty(),
2804 "Should find Java class, got: {:?}",
2805 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2806 );
2807
2808 if let Some(cls) = info.classes.iter().find(|c| c.name == "UserService") {
2809 assert!(!cls.methods.is_empty(), "Should find public methods");
2810 }
2811 }
2812
2813 #[test]
2818 fn test_extract_interface_c_functions() {
2819 let source = r#"
2820int add(int a, int b) {
2821 return a + b;
2822}
2823
2824static int internal_helper(void) {
2825 return 42;
2826}
2827"#;
2828 let info = extract_interface(Path::new("test.c"), source).unwrap();
2829
2830 assert_eq!(
2832 info.functions.len(),
2833 1,
2834 "Should find 1 non-static function, got: {:?}",
2835 info.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
2836 );
2837 assert_eq!(info.functions[0].name, "add");
2838 }
2839
2840 #[test]
2845 fn test_extract_interface_ruby_class() {
2846 let source = r#"
2847class UserManager
2848 def find_user(id)
2849 # find user
2850 end
2851
2852 def _private_method
2853 # private
2854 end
2855end
2856"#;
2857 let info = extract_interface(Path::new("test.rb"), source).unwrap();
2858
2859 assert!(
2860 !info.classes.is_empty(),
2861 "Should find Ruby class, got: {:?}",
2862 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2863 );
2864
2865 if let Some(cls) = info.classes.iter().find(|c| c.name == "UserManager") {
2866 assert_eq!(
2867 cls.methods.len(),
2868 1,
2869 "Should find 1 public method, got: {:?}",
2870 cls.methods.iter().map(|m| &m.name).collect::<Vec<_>>()
2871 );
2872 assert_eq!(cls.methods[0].name, "find_user");
2873 assert_eq!(cls.private_method_count, 1);
2874 }
2875 }
2876
2877 #[test]
2882 fn test_is_public_for_go() {
2883 assert!(is_public_for_lang("ProcessData", Language::Go));
2884 assert!(!is_public_for_lang("processData", Language::Go));
2885 }
2886
2887 #[test]
2888 fn test_is_public_for_python() {
2889 assert!(is_public_for_lang("process_data", Language::Python));
2890 assert!(!is_public_for_lang("_private", Language::Python));
2891 }
2892
2893 #[test]
2898 fn test_is_supported_source_file() {
2899 assert!(is_supported_source_file(Path::new("test.py")));
2900 assert!(is_supported_source_file(Path::new("test.rs")));
2901 assert!(is_supported_source_file(Path::new("test.go")));
2902 assert!(is_supported_source_file(Path::new("test.ts")));
2903 assert!(is_supported_source_file(Path::new("test.java")));
2904 assert!(is_supported_source_file(Path::new("test.c")));
2905 assert!(is_supported_source_file(Path::new("test.rb")));
2906 assert!(is_supported_source_file(Path::new("test.cs")));
2907 assert!(!is_supported_source_file(Path::new("test.txt")));
2908 assert!(!is_supported_source_file(Path::new("test.md")));
2909 }
2910}