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 _ => &[],
81 }
82}
83
84fn class_node_kinds(lang: Language) -> &'static [&'static str] {
86 match lang {
87 Language::Python => &["class_definition"],
88 Language::Rust => &["struct_item", "impl_item", "trait_item", "enum_item"],
89 Language::Go => &["type_declaration"],
90 Language::Java => &[
91 "class_declaration",
92 "interface_declaration",
93 "enum_declaration",
94 ],
95 Language::TypeScript | Language::JavaScript => &[
96 "class_declaration",
97 "interface_declaration",
98 "type_alias_declaration",
99 ],
100 Language::C => &["struct_specifier"],
101 Language::Cpp => &["struct_specifier", "class_specifier"],
102 Language::Ruby => &["class", "module"],
103 Language::CSharp => &[
104 "class_declaration",
105 "interface_declaration",
106 "struct_declaration",
107 ],
108 Language::Scala => &["class_definition", "object_definition", "trait_definition"],
109 Language::Php => &["class_declaration", "interface_declaration"],
110 Language::Lua | Language::Luau => &[], Language::Elixir => &["call"], Language::Ocaml => &["module_definition", "type_definition"],
113 _ => &[],
114 }
115}
116
117fn decorator_node_kinds(lang: Language) -> &'static [&'static str] {
119 match lang {
120 Language::Python => &["decorated_definition"],
121 Language::Java => &["annotation"],
122 Language::TypeScript | Language::JavaScript => &["decorator"],
123 Language::CSharp => &["attribute_list"],
124 Language::Rust => &["attribute_item"],
125 _ => &[],
126 }
127}
128
129fn method_node_kinds(lang: Language) -> &'static [&'static str] {
131 match lang {
132 Language::Python => &["function_definition"],
133 Language::Rust => &["function_item"],
134 Language::Go => &["method_declaration"],
135 Language::Java => &["method_declaration", "constructor_declaration"],
136 Language::TypeScript | Language::JavaScript => {
137 &["method_definition", "public_field_definition"]
138 }
139 Language::C | Language::Cpp => &["function_definition"],
140 Language::Ruby => &["method", "singleton_method"],
141 Language::CSharp => &["method_declaration", "constructor_declaration"],
142 Language::Scala => &["function_definition", "def_definition"],
143 Language::Php => &["method_declaration"],
144 Language::Elixir => &["call"],
145 Language::Ocaml => &["let_binding", "value_definition"],
146 _ => &[],
147 }
148}
149
150#[inline]
163pub fn is_public_name(name: &str) -> bool {
164 !name.starts_with('_')
165}
166
167fn is_public_for_lang(name: &str, lang: Language) -> bool {
169 match lang {
170 Language::Python | Language::Ruby | Language::Lua | Language::Luau => {
171 !name.starts_with('_')
172 }
173 Language::Go => {
174 name.chars().next().is_some_and(|c| c.is_uppercase())
176 }
177 _ => true,
180 }
181}
182
183fn is_rust_pub(node: Node, source: &[u8]) -> bool {
185 for i in 0..node.child_count() {
186 if let Some(child) = node.child(i) {
187 if child.kind() == "visibility_modifier" {
188 let text = node_text(child, source);
189 return text.starts_with("pub");
190 }
191 }
192 }
193 false
194}
195
196fn has_public_modifier(node: Node, source: &[u8]) -> bool {
198 if let Some(modifiers) = node.child_by_field_name("modifiers") {
200 let text = node_text(modifiers, source);
201 return text.contains("public");
202 }
203 for i in 0..node.child_count() {
205 if let Some(child) = node.child(i) {
206 let kind = child.kind();
207 if kind == "modifiers" || kind == "modifier" || kind == "access_modifier" {
208 let text = node_text(child, source);
209 if text.contains("public") {
210 return true;
211 }
212 }
213 if kind == "accessibility_modifier" {
215 let text = node_text(child, source);
216 return text == "public";
217 }
218 }
219 }
220 true
223}
224
225fn is_c_static(node: Node, source: &[u8]) -> bool {
227 if let Some(prev) = node.prev_sibling() {
229 if prev.kind() == "storage_class_specifier" {
230 return node_text(prev, source) == "static";
231 }
232 }
233 for i in 0..node.child_count() {
235 if let Some(child) = node.child(i) {
236 if child.kind() == "storage_class_specifier" && node_text(child, source) == "static" {
237 return true;
238 }
239 }
240 }
241 false
242}
243
244fn is_node_public(node: Node, source: &[u8], lang: Language) -> bool {
246 let name = get_node_name(node, source, lang);
247 let name_str = name.as_deref().unwrap_or("");
248
249 match lang {
250 Language::Rust => is_rust_pub(node, source),
251 Language::Go => name_str.chars().next().is_some_and(|c| c.is_uppercase()),
252 Language::Python | Language::Ruby | Language::Lua | Language::Luau => {
253 !name_str.starts_with('_')
254 }
255 Language::Java | Language::CSharp => has_public_modifier(node, source),
256 Language::C | Language::Cpp => !is_c_static(node, source),
257 _ => true,
259 }
260}
261
262fn get_node_name<'a>(node: Node<'a>, source: &'a [u8], lang: Language) -> Option<String> {
268 if let Some(name_node) = node.child_by_field_name("name") {
270 return Some(node_text(name_node, source).to_string());
271 }
272
273 match lang {
274 Language::C | Language::Cpp => {
275 if let Some(declarator) = node.child_by_field_name("declarator") {
277 return extract_c_declarator_name(declarator, source);
278 }
279 }
280 Language::Go => {
281 if node.kind() == "type_declaration" {
283 for i in 0..node.child_count() {
284 if let Some(child) = node.child(i) {
285 if child.kind() == "type_spec" {
286 if let Some(name_node) = child.child_by_field_name("name") {
287 return Some(node_text(name_node, source).to_string());
288 }
289 }
290 }
291 }
292 }
293 }
294 Language::Rust => {
295 if node.kind() == "impl_item" {
297 if let Some(type_node) = node.child_by_field_name("type") {
299 return Some(node_text(type_node, source).to_string());
300 }
301 for i in 0..node.child_count() {
303 if let Some(child) = node.child(i) {
304 if child.kind() == "type_identifier" || child.kind() == "generic_type" {
305 return Some(node_text(child, source).to_string());
306 }
307 }
308 }
309 }
310 }
311 Language::Elixir => {
312 if node.kind() == "call" {
315 if let Some(target) = node.child(0) {
316 let target_text = node_text(target, source);
317 if target_text == "def" || target_text == "defp" || target_text == "defmodule" {
318 if let Some(args) = node.child_by_field_name("arguments") {
319 if let Some(first_arg) = args.child(0) {
320 if first_arg.kind() == "call" {
322 if let Some(fn_name) = first_arg.child(0) {
323 return Some(node_text(fn_name, source).to_string());
324 }
325 }
326 return Some(node_text(first_arg, source).to_string());
327 }
328 }
329 }
330 }
331 }
332 }
333 Language::Lua | Language::Luau => {
334 for i in 0..node.child_count() {
336 if let Some(child) = node.child(i) {
337 if child.kind() == "identifier" || child.kind() == "dot_index_expression" {
338 return Some(node_text(child, source).to_string());
339 }
340 }
341 }
342 }
343 Language::Ruby => {
344 for i in 0..node.child_count() {
346 if let Some(child) = node.child(i) {
347 if child.kind() == "identifier" || child.kind() == "constant" {
348 return Some(node_text(child, source).to_string());
349 }
350 }
351 }
352 }
353 _ => {}
354 }
355
356 None
357}
358
359fn extract_c_declarator_name(declarator: Node, source: &[u8]) -> Option<String> {
361 if declarator.kind() == "identifier" {
363 return Some(node_text(declarator, source).to_string());
364 }
365 if declarator.kind() == "field_identifier" {
366 return Some(node_text(declarator, source).to_string());
367 }
368 if let Some(inner) = declarator.child_by_field_name("declarator") {
370 return extract_c_declarator_name(inner, source);
371 }
372 if let Some(first) = declarator.child(0) {
374 if first.kind() == "identifier" || first.kind() == "field_identifier" {
375 return Some(node_text(first, source).to_string());
376 }
377 }
378 None
379}
380
381pub fn extract_all_exports(root: Node, source: &[u8]) -> Option<Vec<String>> {
387 let mut cursor = root.walk();
388
389 for child in root.children(&mut cursor) {
390 if child.kind() == "expression_statement" {
391 if let Some(assignment) = child.child(0) {
392 if assignment.kind() == "assignment" {
393 if let Some(left) = assignment.child_by_field_name("left") {
394 if left.kind() == "identifier" {
395 let name = node_text(left, source);
396 if name == "__all__" {
397 if let Some(right) = assignment.child_by_field_name("right") {
398 return extract_list_strings(right, source);
399 }
400 }
401 }
402 }
403 }
404 }
405 }
406 }
407
408 None
409}
410
411fn extract_list_strings(node: Node, source: &[u8]) -> Option<Vec<String>> {
413 if node.kind() != "list" {
414 return None;
415 }
416
417 let mut exports = Vec::new();
418 let mut cursor = node.walk();
419
420 for child in node.children(&mut cursor) {
421 if child.kind() == "string" {
422 let text = node_text(child, source);
423 let cleaned = text
424 .trim_start_matches(['"', '\''])
425 .trim_end_matches(['"', '\'']);
426 exports.push(cleaned.to_string());
427 }
428 }
429
430 if exports.is_empty() {
431 None
432 } else {
433 Some(exports)
434 }
435}
436
437pub fn extract_function_signature(func_node: Node, source: &[u8], lang: Language) -> String {
446 match lang {
447 Language::Python => extract_python_signature(func_node, source),
448 Language::Rust => extract_rust_signature(func_node, source),
449 Language::Go => extract_go_signature(func_node, source),
450 Language::Java | Language::CSharp => extract_java_like_signature(func_node, source),
451 Language::TypeScript | Language::JavaScript => extract_ts_signature(func_node, source),
452 Language::C | Language::Cpp => extract_c_signature(func_node, source),
453 Language::Ruby => extract_ruby_signature(func_node, source),
454 Language::Php => extract_php_signature(func_node, source),
455 Language::Scala => extract_scala_signature(func_node, source),
456 _ => extract_generic_signature(func_node, source),
457 }
458}
459
460fn extract_python_signature(func_node: Node, source: &[u8]) -> String {
462 let mut params = Vec::new();
463
464 if let Some(params_node) = func_node.child_by_field_name("parameters") {
465 let mut cursor = params_node.walk();
466
467 for child in params_node.children(&mut cursor) {
468 match child.kind() {
469 "identifier" => {
470 params.push(node_text(child, source).to_string());
471 }
472 "typed_parameter" => {
473 params.push(extract_typed_parameter(child, source));
474 }
475 "default_parameter" => {
476 params.push(extract_default_parameter(child, source));
477 }
478 "typed_default_parameter" => {
479 params.push(extract_typed_default_parameter(child, source));
480 }
481 "list_splat_pattern" | "dictionary_splat_pattern" => {
482 params.push(node_text(child, source).to_string());
483 }
484 _ => {}
485 }
486 }
487 }
488
489 let params_str = params.join(", ");
490 let mut signature = format!("({})", params_str);
491
492 if let Some(return_type) = func_node.child_by_field_name("return_type") {
493 let return_text = node_text(return_type, source);
494 signature.push_str(" -> ");
495 signature.push_str(return_text);
496 }
497
498 signature
499}
500
501fn extract_rust_signature(func_node: Node, source: &[u8]) -> String {
503 let mut sig = String::new();
504
505 if let Some(params) = func_node.child_by_field_name("parameters") {
506 sig.push_str(node_text(params, source));
507 }
508
509 if let Some(ret) = func_node.child_by_field_name("return_type") {
510 sig.push_str(" -> ");
511 sig.push_str(node_text(ret, source));
512 }
513
514 sig
515}
516
517fn extract_go_signature(func_node: Node, source: &[u8]) -> String {
519 let mut sig = String::new();
520
521 if let Some(params) = func_node.child_by_field_name("parameters") {
522 sig.push_str(node_text(params, source));
523 }
524
525 if let Some(result) = func_node.child_by_field_name("result") {
526 sig.push(' ');
527 sig.push_str(node_text(result, source));
528 }
529
530 sig
531}
532
533fn extract_java_like_signature(func_node: Node, source: &[u8]) -> String {
535 let mut sig = String::new();
536
537 let params_node = func_node.child_by_field_name("parameters").or_else(|| {
539 let mut cursor = func_node.walk();
541 let found = func_node
542 .children(&mut cursor)
543 .find(|&child| child.kind() == "formal_parameters" || child.kind() == "parameter_list");
544 found
545 });
546
547 if let Some(params) = params_node {
548 sig.push_str(node_text(params, source));
549 }
550
551 if let Some(ret) = func_node.child_by_field_name("type") {
553 let ret_text = node_text(ret, source);
555 sig = format!("{}: {}", sig, ret_text);
556 }
557
558 sig
559}
560
561fn extract_ts_signature(func_node: Node, source: &[u8]) -> String {
563 let mut sig = String::new();
564
565 if let Some(params) = func_node.child_by_field_name("parameters") {
566 sig.push_str(node_text(params, source));
567 }
568
569 if let Some(ret) = func_node.child_by_field_name("return_type") {
570 sig.push_str(": ");
571 sig.push_str(node_text(ret, source));
572 }
573
574 sig
575}
576
577fn extract_c_signature(func_node: Node, source: &[u8]) -> String {
579 let mut sig = String::new();
580
581 if let Some(declarator) = func_node.child_by_field_name("declarator") {
582 if let Some(params) = declarator.child_by_field_name("parameters") {
585 sig.push_str(node_text(params, source));
586 }
587 }
588
589 if let Some(type_node) = func_node.child_by_field_name("type") {
591 let type_text = node_text(type_node, source);
592 if !type_text.is_empty() {
593 sig = format!("{}: {}", sig, type_text);
594 }
595 }
596
597 sig
598}
599
600fn extract_ruby_signature(func_node: Node, source: &[u8]) -> String {
602 if let Some(params) = func_node.child_by_field_name("parameters") {
603 node_text(params, source).to_string()
604 } else {
605 let mut cursor = func_node.walk();
607 for child in func_node.children(&mut cursor) {
608 if child.kind() == "method_parameters" {
609 return node_text(child, source).to_string();
610 }
611 }
612 "()".to_string()
613 }
614}
615
616fn extract_php_signature(func_node: Node, source: &[u8]) -> String {
618 let mut sig = String::new();
619
620 if let Some(params) = func_node.child_by_field_name("parameters") {
621 sig.push_str(node_text(params, source));
622 }
623
624 if let Some(ret) = func_node.child_by_field_name("return_type") {
625 sig.push_str(": ");
626 sig.push_str(node_text(ret, source));
627 }
628
629 sig
630}
631
632fn extract_scala_signature(func_node: Node, source: &[u8]) -> String {
634 let mut sig = String::new();
635
636 if let Some(params) = func_node.child_by_field_name("parameters") {
637 sig.push_str(node_text(params, source));
638 }
639
640 if let Some(ret) = func_node.child_by_field_name("return_type") {
641 sig.push_str(": ");
642 sig.push_str(node_text(ret, source));
643 }
644
645 sig
646}
647
648fn extract_generic_signature(func_node: Node, source: &[u8]) -> String {
650 let mut sig = String::new();
651
652 if let Some(params) = func_node.child_by_field_name("parameters") {
653 sig.push_str(node_text(params, source));
654 }
655
656 sig
657}
658
659fn extract_typed_parameter(node: Node, source: &[u8]) -> String {
661 let name = node
662 .child(0)
663 .filter(|c| c.kind() == "identifier")
664 .map(|n| node_text(n, source))
665 .unwrap_or("");
666 let type_hint = node
667 .child_by_field_name("type")
668 .map(|n| node_text(n, source))
669 .unwrap_or("");
670
671 if type_hint.is_empty() {
672 name.to_string()
673 } else {
674 format!("{}: {}", name, type_hint)
675 }
676}
677
678fn extract_default_parameter(node: Node, source: &[u8]) -> String {
680 let name = node
681 .child_by_field_name("name")
682 .map(|n| node_text(n, source))
683 .unwrap_or("");
684 let value = node
685 .child_by_field_name("value")
686 .map(|n| node_text(n, source))
687 .unwrap_or("");
688
689 format!("{} = {}", name, value)
690}
691
692fn extract_typed_default_parameter(node: Node, source: &[u8]) -> String {
694 let name = node
695 .child_by_field_name("name")
696 .map(|n| node_text(n, source))
697 .unwrap_or("");
698 let type_hint = node
699 .child_by_field_name("type")
700 .map(|n| node_text(n, source))
701 .unwrap_or("");
702 let value = node
703 .child_by_field_name("value")
704 .map(|n| node_text(n, source))
705 .unwrap_or("");
706
707 if type_hint.is_empty() {
708 format!("{} = {}", name, value)
709 } else {
710 format!("{}: {} = {}", name, type_hint, value)
711 }
712}
713
714pub fn extract_function_info(func_node: Node, source: &[u8], lang: Language) -> FunctionInfo {
720 let name = get_node_name(func_node, source, lang).unwrap_or_default();
721 let signature = extract_function_signature(func_node, source, lang);
722 let lineno = func_node.start_position().row as u32 + 1;
723 let is_async = detect_async(func_node, source, lang);
724 let docstring = extract_docstring(func_node, source, lang);
725
726 FunctionInfo {
727 name,
728 signature,
729 docstring,
730 lineno,
731 is_async,
732 }
733}
734
735fn detect_async(func_node: Node, source: &[u8], lang: Language) -> bool {
737 match lang {
738 Language::Python => {
739 let func_text = node_text(func_node, source);
740 func_text.starts_with("async ")
741 }
742 Language::Rust => {
743 for i in 0..func_node.child_count() {
745 if let Some(child) = func_node.child(i) {
746 if node_text(child, source) == "async" {
747 return true;
748 }
749 }
750 }
751 false
752 }
753 Language::TypeScript | Language::JavaScript => {
754 let func_text = node_text(func_node, source);
756 func_text.starts_with("async ")
757 }
758 Language::CSharp => {
759 if let Some(modifiers) = func_node.child_by_field_name("modifiers") {
761 return node_text(modifiers, source).contains("async");
762 }
763 false
764 }
765 Language::Elixir => {
766 false
768 }
769 _ => false,
770 }
771}
772
773fn extract_docstring(node: Node, source: &[u8], lang: Language) -> Option<String> {
779 match lang {
780 Language::Python => extract_python_docstring(node, source),
781 Language::Rust => extract_rust_doc_comment(node, source),
782 Language::Go => extract_go_doc_comment(node, source),
783 Language::Java | Language::CSharp | Language::Scala | Language::Php => {
784 extract_javadoc_comment(node, source)
785 }
786 Language::TypeScript | Language::JavaScript => extract_jsdoc_comment(node, source),
787 Language::Ruby => extract_ruby_comment(node, source),
788 Language::Elixir => extract_elixir_doc(node, source),
789 _ => None,
790 }
791}
792
793fn extract_python_docstring(node: Node, source: &[u8]) -> Option<String> {
795 if let Some(body) = node.child_by_field_name("body") {
796 let mut cursor = body.walk();
797 let first_stmt = body.children(&mut cursor).next();
798 if let Some(child) = first_stmt {
799 if child.kind() == "expression_statement" {
800 if let Some(expr) = child.child(0) {
801 if expr.kind() == "string" {
802 let text = node_text(expr, source);
803 let cleaned = text
804 .trim_start_matches("\"\"\"")
805 .trim_start_matches("'''")
806 .trim_end_matches("\"\"\"")
807 .trim_end_matches("'''")
808 .trim();
809 return Some(cleaned.to_string());
810 }
811 }
812 }
813 }
814 }
815 None
816}
817
818fn extract_rust_doc_comment(node: Node, source: &[u8]) -> Option<String> {
820 let mut comments = Vec::new();
821 let mut prev = node.prev_sibling();
822
823 while let Some(sib) = prev {
824 let kind = sib.kind();
825 if kind == "line_comment" {
826 let text = node_text(sib, source);
827 if text.starts_with("///") || text.starts_with("//!") {
828 let content = text
829 .trim_start_matches("///")
830 .trim_start_matches("//!")
831 .trim();
832 comments.push(content.to_string());
833 } else {
834 break;
835 }
836 } else if kind == "attribute_item" {
837 } else {
839 break;
840 }
841 prev = sib.prev_sibling();
842 }
843
844 if comments.is_empty() {
845 None
846 } else {
847 comments.reverse();
848 Some(comments.join("\n"))
849 }
850}
851
852fn extract_go_doc_comment(node: Node, source: &[u8]) -> Option<String> {
854 let mut comments = Vec::new();
855 let mut prev = node.prev_sibling();
856
857 while let Some(sib) = prev {
858 if sib.kind() == "comment" {
859 let text = node_text(sib, source);
860 let content = text.trim_start_matches("//").trim();
861 comments.push(content.to_string());
862 } else {
863 break;
864 }
865 prev = sib.prev_sibling();
866 }
867
868 if comments.is_empty() {
869 None
870 } else {
871 comments.reverse();
872 Some(comments.join("\n"))
873 }
874}
875
876fn extract_javadoc_comment(node: Node, source: &[u8]) -> Option<String> {
878 let mut prev = node.prev_sibling();
879
880 while let Some(sib) = prev {
881 let kind = sib.kind();
882 if kind == "block_comment" || kind == "comment" || kind == "multiline_comment" {
883 let text = node_text(sib, source);
884 if text.starts_with("/**") {
885 let cleaned = text
886 .trim_start_matches("/**")
887 .trim_end_matches("*/")
888 .lines()
889 .map(|l| l.trim().trim_start_matches('*').trim())
890 .filter(|l| !l.is_empty())
891 .collect::<Vec<_>>()
892 .join("\n");
893 return Some(cleaned);
894 }
895 } else if kind == "annotation" || kind == "marker_annotation" || kind == "attribute_list" {
896 } else {
898 break;
899 }
900 prev = sib.prev_sibling();
901 }
902 None
903}
904
905fn extract_jsdoc_comment(node: Node, source: &[u8]) -> Option<String> {
907 extract_javadoc_comment(node, source)
908}
909
910fn extract_ruby_comment(node: Node, source: &[u8]) -> Option<String> {
912 let mut comments = Vec::new();
913 let mut prev = node.prev_sibling();
914
915 while let Some(sib) = prev {
916 if sib.kind() == "comment" {
917 let text = node_text(sib, source);
918 let content = text.trim_start_matches('#').trim();
919 comments.push(content.to_string());
920 } else {
921 break;
922 }
923 prev = sib.prev_sibling();
924 }
925
926 if comments.is_empty() {
927 None
928 } else {
929 comments.reverse();
930 Some(comments.join("\n"))
931 }
932}
933
934fn extract_elixir_doc(node: Node, source: &[u8]) -> Option<String> {
936 let mut prev = node.prev_sibling();
937
938 while let Some(sib) = prev {
939 if sib.kind() == "unary_operator" || sib.kind() == "call" {
940 let text = node_text(sib, source);
941 if text.starts_with("@doc") || text.starts_with("@moduledoc") {
942 let cleaned = text
944 .trim_start_matches("@moduledoc")
945 .trim_start_matches("@doc")
946 .trim()
947 .trim_start_matches("\"\"\"")
948 .trim_end_matches("\"\"\"")
949 .trim_start_matches('"')
950 .trim_end_matches('"')
951 .trim();
952 if !cleaned.is_empty() {
953 return Some(cleaned.to_string());
954 }
955 }
956 } else if sib.kind() == "comment" {
957 } else {
959 break;
960 }
961 prev = sib.prev_sibling();
962 }
963 None
964}
965
966pub fn extract_class_info(class_node: Node, source: &[u8], lang: Language) -> ClassInfo {
972 let name = get_node_name(class_node, source, lang).unwrap_or_default();
973 let lineno = class_node.start_position().row as u32 + 1;
974
975 let bases = extract_base_classes(class_node, source, lang);
977
978 let mut methods = Vec::new();
980 let mut private_method_count = 0u32;
981
982 let method_kinds = method_node_kinds(lang);
983 let body_node = find_body_node(class_node, lang);
984
985 if let Some(body) = body_node {
986 collect_methods_from_body(
987 body,
988 source,
989 lang,
990 method_kinds,
991 &mut methods,
992 &mut private_method_count,
993 );
994 }
995
996 ClassInfo {
997 name,
998 lineno,
999 bases,
1000 methods,
1001 private_method_count,
1002 }
1003}
1004
1005fn find_body_node<'a>(class_node: Node<'a>, lang: Language) -> Option<Node<'a>> {
1007 if let Some(body) = class_node.child_by_field_name("body") {
1009 return Some(body);
1010 }
1011 if let Some(body) = class_node.child_by_field_name("members") {
1012 return Some(body);
1013 }
1014
1015 match lang {
1016 Language::Rust => {
1017 let mut cursor = class_node.walk();
1019 for child in class_node.children(&mut cursor) {
1020 if child.kind() == "declaration_list" {
1021 return Some(child);
1022 }
1023 }
1024 None
1025 }
1026 Language::Java | Language::CSharp => {
1027 let mut cursor = class_node.walk();
1029 for child in class_node.children(&mut cursor) {
1030 if child.kind() == "class_body"
1031 || child.kind() == "interface_body"
1032 || child.kind() == "enum_body"
1033 || child.kind() == "declaration_list"
1034 {
1035 return Some(child);
1036 }
1037 }
1038 None
1039 }
1040 Language::TypeScript | Language::JavaScript => {
1041 let mut cursor = class_node.walk();
1042 for child in class_node.children(&mut cursor) {
1043 if child.kind() == "class_body" {
1044 return Some(child);
1045 }
1046 }
1047 None
1048 }
1049 Language::Cpp => {
1050 let mut cursor = class_node.walk();
1051 for child in class_node.children(&mut cursor) {
1052 if child.kind() == "field_declaration_list" {
1053 return Some(child);
1054 }
1055 }
1056 None
1057 }
1058 Language::Ruby => {
1059 let mut cursor = class_node.walk();
1061 for child in class_node.children(&mut cursor) {
1062 if child.kind() == "body_statement" {
1063 return Some(child);
1064 }
1065 }
1066 Some(class_node)
1068 }
1069 _ => {
1070 let mut cursor = class_node.walk();
1072 for child in class_node.children(&mut cursor) {
1073 let kind = child.kind();
1074 if kind.contains("body")
1075 || kind.contains("block")
1076 || kind == "declaration_list"
1077 || kind == "template_body"
1078 {
1079 return Some(child);
1080 }
1081 }
1082 None
1083 }
1084 }
1085}
1086
1087fn collect_methods_from_body(
1089 body: Node,
1090 source: &[u8],
1091 lang: Language,
1092 method_kinds: &[&str],
1093 methods: &mut Vec<MethodInfo>,
1094 private_count: &mut u32,
1095) {
1096 let mut cursor = body.walk();
1097 let decorator_kinds = decorator_node_kinds(lang);
1098
1099 for child in body.children(&mut cursor) {
1100 let kind = child.kind();
1101
1102 if method_kinds.contains(&kind) {
1103 let method_name = get_node_name(child, source, lang).unwrap_or_default();
1104 if is_method_public(&method_name, child, source, lang) {
1105 methods.push(extract_method_info(child, source, lang));
1106 } else {
1107 *private_count += 1;
1108 }
1109 } else if decorator_kinds.contains(&kind) {
1110 if let Some(def) = find_definition_in_decorated(child, method_kinds) {
1112 let method_name = get_node_name(def, source, lang).unwrap_or_default();
1113 if is_method_public(&method_name, def, source, lang) {
1114 methods.push(extract_method_info(def, source, lang));
1115 } else {
1116 *private_count += 1;
1117 }
1118 }
1119 }
1120 }
1121}
1122
1123fn is_method_public(name: &str, node: Node, source: &[u8], lang: Language) -> bool {
1125 match lang {
1126 Language::Python | Language::Ruby | Language::Lua | Language::Luau => {
1127 is_public_for_lang(name, lang)
1128 }
1129 Language::Rust => is_rust_pub(node, source),
1130 Language::Go => name.chars().next().is_some_and(|c| c.is_uppercase()),
1131 Language::Java | Language::CSharp => has_public_modifier(node, source),
1132 _ => true,
1133 }
1134}
1135
1136fn extract_base_classes(class_node: Node, source: &[u8], lang: Language) -> Vec<String> {
1138 let mut bases = Vec::new();
1139
1140 match lang {
1141 Language::Python => {
1142 if let Some(superclasses) = class_node.child_by_field_name("superclasses") {
1143 let mut cursor = superclasses.walk();
1144 for child in superclasses.children(&mut cursor) {
1145 if child.kind() == "identifier" || child.kind() == "attribute" {
1146 bases.push(node_text(child, source).to_string());
1147 }
1148 }
1149 }
1150 }
1151 Language::Java | Language::CSharp => {
1152 if let Some(super_node) = class_node.child_by_field_name("superclass") {
1154 bases.push(node_text(super_node, source).to_string());
1155 }
1156 if let Some(interfaces) = class_node.child_by_field_name("interfaces") {
1157 let mut cursor = interfaces.walk();
1158 for child in interfaces.children(&mut cursor) {
1159 if child.kind() == "type_identifier" || child.kind() == "generic_type" {
1160 bases.push(node_text(child, source).to_string());
1161 }
1162 }
1163 }
1164 if let Some(extends) = class_node.child_by_field_name("type_parameters") {
1166 let _ = extends;
1168 }
1169 }
1170 Language::Rust => {
1171 if class_node.kind() == "impl_item" {
1174 if let Some(trait_node) = class_node.child_by_field_name("trait") {
1175 bases.push(node_text(trait_node, source).to_string());
1176 }
1177 }
1178 }
1179 Language::TypeScript | Language::JavaScript => {
1180 let mut cursor = class_node.walk();
1182 for child in class_node.children(&mut cursor) {
1183 if child.kind() == "class_heritage" {
1184 let mut inner_cursor = child.walk();
1185 for clause in child.children(&mut inner_cursor) {
1186 if clause.kind() == "extends_clause" || clause.kind() == "implements_clause"
1187 {
1188 let mut type_cursor = clause.walk();
1189 for type_child in clause.children(&mut type_cursor) {
1190 if type_child.kind() == "identifier"
1191 || type_child.kind() == "type_identifier"
1192 {
1193 bases.push(node_text(type_child, source).to_string());
1194 }
1195 }
1196 }
1197 }
1198 }
1199 }
1200 }
1201 Language::Ruby => {
1202 if let Some(super_node) = class_node.child_by_field_name("superclass") {
1203 bases.push(node_text(super_node, source).to_string());
1204 }
1205 }
1206 Language::Go => {
1207 }
1210 Language::Scala => {
1211 if let Some(extends) = class_node.child_by_field_name("extends") {
1212 bases.push(node_text(extends, source).to_string());
1213 }
1214 }
1215 _ => {}
1216 }
1217
1218 bases
1219}
1220
1221fn find_definition_in_decorated<'a>(node: Node<'a>, target_kinds: &[&str]) -> Option<Node<'a>> {
1223 let mut cursor = node.walk();
1224 let found = node
1225 .children(&mut cursor)
1226 .find(|&child| target_kinds.contains(&child.kind()));
1227 found
1228}
1229
1230fn extract_method_info(func_node: Node, source: &[u8], lang: Language) -> MethodInfo {
1232 let name = get_node_name(func_node, source, lang).unwrap_or_default();
1233 let signature = extract_function_signature(func_node, source, lang);
1234 let is_async = detect_async(func_node, source, lang);
1235
1236 MethodInfo {
1237 name,
1238 signature,
1239 is_async,
1240 }
1241}
1242
1243pub fn extract_interface(path: &Path, source: &str) -> PatternsResult<InterfaceInfo> {
1252 let lang = Language::from_path(path).unwrap_or(Language::Python);
1253 extract_interface_with_lang(path, source, lang)
1254}
1255
1256pub fn extract_interface_with_lang(
1258 path: &Path,
1259 source: &str,
1260 lang: Language,
1261) -> PatternsResult<InterfaceInfo> {
1262 let source_bytes = source.as_bytes();
1263
1264 let pool = ParserPool::new();
1266 let tree = pool
1267 .parse(source, lang)
1268 .map_err(|e| PatternsError::parse_error(path, format!("Failed to parse: {}", e)))?;
1269
1270 let root = tree.root_node();
1271
1272 let all_exports = if lang == Language::Python {
1274 extract_all_exports(root, source_bytes)
1275 } else {
1276 None
1277 };
1278
1279 let func_kinds = function_node_kinds(lang);
1281 let class_kinds = class_node_kinds(lang);
1282 let decorator_kinds = decorator_node_kinds(lang);
1283
1284 let (functions, classes) = collect_top_level_definitions(
1286 root,
1287 source_bytes,
1288 lang,
1289 func_kinds,
1290 class_kinds,
1291 decorator_kinds,
1292 );
1293
1294 Ok(InterfaceInfo {
1295 file: path.display().to_string(),
1296 all_exports,
1297 functions,
1298 classes,
1299 })
1300}
1301
1302fn collect_top_level_definitions(
1304 root: Node,
1305 source: &[u8],
1306 lang: Language,
1307 func_kinds: &[&str],
1308 class_kinds: &[&str],
1309 decorator_kinds: &[&str],
1310) -> (Vec<FunctionInfo>, Vec<ClassInfo>) {
1311 let mut functions = Vec::new();
1312 let mut classes = Vec::new();
1313 let mut cursor = root.walk();
1314
1315 for child in root.children(&mut cursor) {
1316 let kind = child.kind();
1317
1318 if func_kinds.contains(&kind) {
1319 if is_node_public(child, source, lang) {
1320 if lang == Language::Elixir {
1322 if let Some(target) = child.child(0) {
1323 let target_text = node_text(target, source);
1324 if target_text == "defp" {
1325 continue;
1326 }
1327 if target_text != "def" {
1328 continue;
1329 }
1330 }
1331 }
1332 functions.push(extract_function_info(child, source, lang));
1333 }
1334 } else if class_kinds.contains(&kind) {
1335 if is_node_public(child, source, lang) {
1336 if lang == Language::Elixir {
1338 if let Some(target) = child.child(0) {
1339 if node_text(target, source) != "defmodule" {
1340 continue;
1341 }
1342 }
1343 }
1344 classes.push(extract_class_info(child, source, lang));
1345 }
1346 } else if decorator_kinds.contains(&kind) {
1347 if let Some(def) = find_definition_in_decorated(child, func_kinds) {
1349 if is_node_public(def, source, lang) {
1350 functions.push(extract_function_info(def, source, lang));
1351 }
1352 } else if let Some(class_def) = find_definition_in_decorated(child, class_kinds) {
1353 if is_node_public(class_def, source, lang) {
1354 classes.push(extract_class_info(class_def, source, lang));
1355 }
1356 }
1357 } else if lang == Language::Php {
1358 let mut inner_cursor = child.walk();
1364 for inner_child in child.children(&mut inner_cursor) {
1365 let inner_kind = inner_child.kind();
1366 if func_kinds.contains(&inner_kind) {
1367 if is_node_public(inner_child, source, lang) {
1368 functions.push(extract_function_info(inner_child, source, lang));
1369 }
1370 } else if class_kinds.contains(&inner_kind)
1371 && is_node_public(inner_child, source, lang)
1372 {
1373 classes.push(extract_class_info(inner_child, source, lang));
1374 }
1375 }
1376 }
1377 }
1378
1379 (functions, classes)
1380}
1381
1382pub fn format_interface_text(info: &InterfaceInfo) -> String {
1388 let mut lines = Vec::new();
1389
1390 lines.push(format!("File: {}", info.file));
1392 lines.push(String::new());
1393
1394 if let Some(ref exports) = info.all_exports {
1396 lines.push("Exports (__all__):".to_string());
1397 for name in exports {
1398 lines.push(format!(" {}", name));
1399 }
1400 lines.push(String::new());
1401 }
1402
1403 if !info.functions.is_empty() {
1405 lines.push("Functions:".to_string());
1406 for func in &info.functions {
1407 let async_marker = if func.is_async { "async " } else { "" };
1408 lines.push(format!(
1409 " {}def {}{} [line {}]",
1410 async_marker, func.name, func.signature, func.lineno
1411 ));
1412 if let Some(ref doc) = func.docstring {
1413 let doc_preview = if doc.len() > 60 {
1415 format!("{}...", &doc[..57])
1416 } else {
1417 doc.clone()
1418 };
1419 lines.push(format!(" \"{}\"", doc_preview));
1420 }
1421 }
1422 lines.push(String::new());
1423 }
1424
1425 if !info.classes.is_empty() {
1427 lines.push("Classes:".to_string());
1428 for class in &info.classes {
1429 let bases_str = if class.bases.is_empty() {
1430 String::new()
1431 } else {
1432 format!("({})", class.bases.join(", "))
1433 };
1434 lines.push(format!(
1435 " class {}{} [line {}]",
1436 class.name, bases_str, class.lineno
1437 ));
1438
1439 for method in &class.methods {
1440 let async_marker = if method.is_async { "async " } else { "" };
1441 lines.push(format!(
1442 " {}def {}{}",
1443 async_marker, method.name, method.signature
1444 ));
1445 }
1446
1447 if class.private_method_count > 0 {
1448 lines.push(format!(
1449 " ({} private methods)",
1450 class.private_method_count
1451 ));
1452 }
1453 }
1454 lines.push(String::new());
1455 }
1456
1457 let total_methods: u32 = info.classes.iter().map(|c| c.methods.len() as u32).sum();
1459 lines.push(format!(
1460 "Summary: {} functions, {} classes, {} public methods",
1461 info.functions.len(),
1462 info.classes.len(),
1463 total_methods
1464 ));
1465
1466 lines.join("\n")
1467}
1468
1469fn is_supported_source_file(path: &Path) -> bool {
1475 Language::from_path(path).is_some()
1476}
1477
1478pub fn run(args: InterfaceArgs, format: OutputFormat) -> anyhow::Result<()> {
1480 let path = &args.path;
1481
1482 if path.is_dir() {
1483 let canonical_dir = if let Some(ref root) = args.project_root {
1485 super::validation::validate_file_path_in_project(path, root)?
1486 } else {
1487 validate_directory_path(path)?
1488 };
1489
1490 let mut results = Vec::new();
1492 let mut entries: Vec<PathBuf> = walk_project(&canonical_dir)
1493 .filter(|e| e.path().is_file() && is_supported_source_file(e.path()))
1494 .map(|e| e.path().to_path_buf())
1495 .collect();
1496
1497 entries.sort();
1499
1500 for file_path in entries {
1501 let source = read_file_safe(&file_path)?;
1502 match extract_interface(&file_path, &source) {
1503 Ok(info) => results.push(info),
1504 Err(_) => {
1505 continue;
1507 }
1508 }
1509 }
1510
1511 match format {
1513 OutputFormat::Text => {
1514 for info in &results {
1515 println!("{}", format_interface_text(info));
1516 println!();
1517 }
1518 }
1519 OutputFormat::Compact => {
1520 let json = serde_json::to_string(&results)?;
1521 println!("{}", json);
1522 }
1523 _ => {
1524 let json = serde_json::to_string_pretty(&results)?;
1525 println!("{}", json);
1526 }
1527 }
1528 } else {
1529 let canonical_path = if let Some(ref root) = args.project_root {
1531 super::validation::validate_file_path_in_project(path, root)?
1532 } else {
1533 validate_file_path(path)?
1534 };
1535
1536 let source = read_file_safe(&canonical_path)?;
1537 let info = extract_interface(&canonical_path, &source)?;
1538
1539 match format {
1541 OutputFormat::Text => {
1542 println!("{}", format_interface_text(&info));
1543 }
1544 OutputFormat::Compact => {
1545 let json = serde_json::to_string(&info)?;
1546 println!("{}", json);
1547 }
1548 _ => {
1549 let json = serde_json::to_string_pretty(&info)?;
1550 println!("{}", json);
1551 }
1552 }
1553 }
1554
1555 Ok(())
1556}
1557
1558fn node_text<'a>(node: Node, source: &'a [u8]) -> &'a str {
1564 node.utf8_text(source).unwrap_or("")
1565}
1566
1567#[cfg(test)]
1572mod tests {
1573 use super::*;
1574
1575 #[test]
1580 fn test_is_public_name_public() {
1581 assert!(is_public_name("my_function"));
1582 assert!(is_public_name("MyClass"));
1583 assert!(is_public_name("process"));
1584 assert!(is_public_name("x"));
1585 }
1586
1587 #[test]
1588 fn test_is_public_name_private() {
1589 assert!(!is_public_name("_private"));
1590 assert!(!is_public_name("__dunder__"));
1591 assert!(!is_public_name("_PrivateClass"));
1592 assert!(!is_public_name("__init__"));
1593 }
1594
1595 #[test]
1600 fn test_extract_all_exports_present() {
1601 let source = r#"
1602__all__ = ['foo', 'bar', 'Baz']
1603
1604def foo():
1605 pass
1606"#;
1607 let pool = ParserPool::new();
1608 let tree = pool.parse(source, Language::Python).unwrap();
1609 let root = tree.root_node();
1610
1611 let exports = extract_all_exports(root, source.as_bytes());
1612 assert!(exports.is_some());
1613 let exports = exports.unwrap();
1614 assert_eq!(exports.len(), 3);
1615 assert!(exports.contains(&"foo".to_string()));
1616 assert!(exports.contains(&"bar".to_string()));
1617 assert!(exports.contains(&"Baz".to_string()));
1618 }
1619
1620 #[test]
1621 fn test_extract_all_exports_absent() {
1622 let source = r#"
1623def foo():
1624 pass
1625"#;
1626 let pool = ParserPool::new();
1627 let tree = pool.parse(source, Language::Python).unwrap();
1628 let root = tree.root_node();
1629
1630 let exports = extract_all_exports(root, source.as_bytes());
1631 assert!(exports.is_none());
1632 }
1633
1634 #[test]
1639 fn test_extract_function_signature_simple() {
1640 let source = "def foo(x, y): pass";
1641 let pool = ParserPool::new();
1642 let tree = pool.parse(source, Language::Python).unwrap();
1643 let root = tree.root_node();
1644 let func_node = root.child(0).unwrap();
1645
1646 let sig = extract_function_signature(func_node, source.as_bytes(), Language::Python);
1647 assert_eq!(sig, "(x, y)");
1648 }
1649
1650 #[test]
1651 fn test_extract_function_signature_typed() {
1652 let source = "def foo(x: int, y: str) -> bool: pass";
1653 let pool = ParserPool::new();
1654 let tree = pool.parse(source, Language::Python).unwrap();
1655 let root = tree.root_node();
1656 let func_node = root.child(0).unwrap();
1657
1658 let sig = extract_function_signature(func_node, source.as_bytes(), Language::Python);
1659 assert!(sig.contains("x: int"), "sig = {:?}", sig);
1660 assert!(sig.contains("y: str"), "sig = {:?}", sig);
1661 assert!(sig.contains("-> bool"), "sig = {:?}", sig);
1662 }
1663
1664 #[test]
1665 fn test_extract_function_signature_default() {
1666 let source = "def foo(x: int = 10): pass";
1667 let pool = ParserPool::new();
1668 let tree = pool.parse(source, Language::Python).unwrap();
1669 let root = tree.root_node();
1670 let func_node = root.child(0).unwrap();
1671
1672 let sig = extract_function_signature(func_node, source.as_bytes(), Language::Python);
1673 assert!(sig.contains("x: int = 10") || sig.contains("x: int=10"));
1674 }
1675
1676 #[test]
1681 fn test_extract_interface_public_functions() {
1682 let source = r#"
1683def public_func():
1684 """A public function."""
1685 pass
1686
1687def _private_func():
1688 pass
1689"#;
1690 let info = extract_interface(Path::new("test.py"), source).unwrap();
1691
1692 assert_eq!(info.functions.len(), 1);
1693 assert_eq!(info.functions[0].name, "public_func");
1694 }
1695
1696 #[test]
1697 fn test_extract_interface_public_classes() {
1698 let source = r#"
1699class PublicClass:
1700 def public_method(self):
1701 pass
1702
1703 def _private_method(self):
1704 pass
1705
1706class _PrivateClass:
1707 pass
1708"#;
1709 let info = extract_interface(Path::new("test.py"), source).unwrap();
1710
1711 assert_eq!(info.classes.len(), 1);
1712 assert_eq!(info.classes[0].name, "PublicClass");
1713 assert_eq!(info.classes[0].methods.len(), 1);
1714 assert_eq!(info.classes[0].methods[0].name, "public_method");
1715 assert_eq!(info.classes[0].private_method_count, 1);
1716 }
1717
1718 #[test]
1719 fn test_extract_interface_async_function() {
1720 let source = r#"
1721async def async_func():
1722 pass
1723
1724def sync_func():
1725 pass
1726"#;
1727 let info = extract_interface(Path::new("test.py"), source).unwrap();
1728
1729 assert_eq!(info.functions.len(), 2);
1730
1731 let async_fn = info.functions.iter().find(|f| f.name == "async_func");
1732 assert!(async_fn.is_some());
1733 assert!(async_fn.unwrap().is_async);
1734
1735 let sync_fn = info.functions.iter().find(|f| f.name == "sync_func");
1736 assert!(sync_fn.is_some());
1737 assert!(!sync_fn.unwrap().is_async);
1738 }
1739
1740 #[test]
1741 fn test_extract_interface_with_all() {
1742 let source = r#"
1743__all__ = ['foo', 'Bar']
1744
1745def foo():
1746 pass
1747
1748def bar():
1749 pass
1750
1751class Bar:
1752 pass
1753"#;
1754 let info = extract_interface(Path::new("test.py"), source).unwrap();
1755
1756 assert!(info.all_exports.is_some());
1757 let exports = info.all_exports.unwrap();
1758 assert!(exports.contains(&"foo".to_string()));
1759 assert!(exports.contains(&"Bar".to_string()));
1760 }
1761
1762 #[test]
1763 fn test_extract_interface_docstrings() {
1764 let source = r#"
1765def documented():
1766 """This is a docstring."""
1767 pass
1768
1769def undocumented():
1770 pass
1771"#;
1772 let info = extract_interface(Path::new("test.py"), source).unwrap();
1773
1774 let documented = info.functions.iter().find(|f| f.name == "documented");
1775 assert!(documented.is_some());
1776 assert!(documented.unwrap().docstring.is_some());
1777 assert!(documented
1778 .unwrap()
1779 .docstring
1780 .as_ref()
1781 .unwrap()
1782 .contains("docstring"));
1783
1784 let undocumented = info.functions.iter().find(|f| f.name == "undocumented");
1785 assert!(undocumented.is_some());
1786 assert!(undocumented.unwrap().docstring.is_none());
1787 }
1788
1789 #[test]
1790 fn test_extract_interface_class_bases() {
1791 let source = r#"
1792class Child(Parent, Mixin):
1793 pass
1794"#;
1795 let info = extract_interface(Path::new("test.py"), source).unwrap();
1796
1797 assert_eq!(info.classes.len(), 1);
1798 assert_eq!(info.classes[0].bases.len(), 2);
1799 assert!(info.classes[0].bases.contains(&"Parent".to_string()));
1800 assert!(info.classes[0].bases.contains(&"Mixin".to_string()));
1801 }
1802
1803 #[test]
1808 fn test_format_interface_text() {
1809 let info = InterfaceInfo {
1810 file: "test.py".to_string(),
1811 all_exports: Some(vec!["foo".to_string()]),
1812 functions: vec![FunctionInfo {
1813 name: "foo".to_string(),
1814 signature: "(x: int) -> str".to_string(),
1815 docstring: Some("A function.".to_string()),
1816 lineno: 5,
1817 is_async: false,
1818 }],
1819 classes: vec![ClassInfo {
1820 name: "MyClass".to_string(),
1821 lineno: 10,
1822 bases: vec!["Base".to_string()],
1823 methods: vec![MethodInfo {
1824 name: "method".to_string(),
1825 signature: "(self)".to_string(),
1826 is_async: false,
1827 }],
1828 private_method_count: 2,
1829 }],
1830 };
1831
1832 let text = format_interface_text(&info);
1833 assert!(text.contains("File: test.py"));
1834 assert!(text.contains("foo"));
1835 assert!(text.contains("MyClass"));
1836 assert!(text.contains("Base"));
1837 assert!(text.contains("method"));
1838 assert!(text.contains("2 private methods"));
1839 }
1840
1841 #[test]
1850 fn test_extract_interface_rust_pub_functions() {
1851 let source = r#"
1852/// Adds two numbers.
1853pub fn add(a: i32, b: i32) -> i32 {
1854 a + b
1855}
1856
1857fn private_helper() -> bool {
1858 true
1859}
1860
1861pub async fn async_fetch() -> String {
1862 String::new()
1863}
1864"#;
1865 let info = extract_interface(Path::new("test.rs"), source).unwrap();
1866
1867 assert_eq!(
1868 info.functions.len(),
1869 2,
1870 "Should find 2 pub functions, got: {:?}",
1871 info.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
1872 );
1873
1874 let add_fn = info.functions.iter().find(|f| f.name == "add");
1875 assert!(add_fn.is_some(), "Should find 'add' function");
1876 let add_fn = add_fn.unwrap();
1877 assert!(
1878 add_fn.signature.contains("a: i32"),
1879 "sig = {:?}",
1880 add_fn.signature
1881 );
1882 assert!(
1883 add_fn.signature.contains("-> i32"),
1884 "sig = {:?}",
1885 add_fn.signature
1886 );
1887 assert!(add_fn.docstring.is_some(), "Should have doc comment");
1888 assert!(add_fn
1889 .docstring
1890 .as_ref()
1891 .unwrap()
1892 .contains("Adds two numbers"));
1893 assert!(!add_fn.is_async);
1894
1895 let async_fn = info.functions.iter().find(|f| f.name == "async_fetch");
1896 assert!(async_fn.is_some(), "Should find 'async_fetch' function");
1897 assert!(async_fn.unwrap().is_async);
1898 }
1899
1900 #[test]
1901 fn test_extract_interface_rust_struct_impl() {
1902 let source = r#"
1903pub struct Point {
1904 pub x: f64,
1905 pub y: f64,
1906}
1907
1908impl Point {
1909 pub fn new(x: f64, y: f64) -> Self {
1910 Point { x, y }
1911 }
1912
1913 fn internal(&self) {}
1914}
1915"#;
1916 let info = extract_interface(Path::new("test.rs"), source).unwrap();
1917
1918 assert!(
1920 !info.classes.is_empty(),
1921 "Should find at least struct/impl, got: {:?}",
1922 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
1923 );
1924
1925 let point_struct = info.classes.iter().find(|c| c.name == "Point");
1927 assert!(point_struct.is_some(), "Should find Point struct/impl");
1928 }
1929
1930 #[test]
1931 fn test_extract_interface_rust_trait() {
1932 let source = r#"
1933pub trait Drawable {
1934 fn draw(&self);
1935 fn resize(&mut self, factor: f64);
1936}
1937"#;
1938 let info = extract_interface(Path::new("test.rs"), source).unwrap();
1939
1940 let trait_info = info.classes.iter().find(|c| c.name == "Drawable");
1941 assert!(trait_info.is_some(), "Should find Drawable trait");
1942 }
1943
1944 #[test]
1949 fn test_extract_interface_go_exported_functions() {
1950 let source = r#"
1951package main
1952
1953// ProcessData handles data processing.
1954func ProcessData(input string) (string, error) {
1955 return input, nil
1956}
1957
1958func internalHelper() bool {
1959 return true
1960}
1961"#;
1962 let info = extract_interface(Path::new("test.go"), source).unwrap();
1963
1964 assert_eq!(
1966 info.functions.len(),
1967 1,
1968 "Should find 1 exported function, got: {:?}",
1969 info.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
1970 );
1971 assert_eq!(info.functions[0].name, "ProcessData");
1972 assert!(
1973 info.functions[0].docstring.is_some(),
1974 "Should have doc comment"
1975 );
1976 }
1977
1978 #[test]
1983 fn test_extract_interface_typescript_class() {
1984 let source = r#"
1985class UserService {
1986 async fetchUser(id: string): Promise<User> {
1987 return {} as User;
1988 }
1989
1990 private internalMethod(): void {}
1991}
1992
1993function processData(input: string): number {
1994 return input.length;
1995}
1996"#;
1997 let info = extract_interface(Path::new("test.ts"), source).unwrap();
1998
1999 assert!(
2001 !info.functions.is_empty() || !info.classes.is_empty(),
2002 "Should find definitions: functions={:?}, classes={:?}",
2003 info.functions.iter().map(|f| &f.name).collect::<Vec<_>>(),
2004 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2005 );
2006 }
2007
2008 #[test]
2009 fn test_extract_interface_typescript_interface() {
2010 let source = r#"
2011interface User {
2012 id: string;
2013 name: string;
2014 email: string;
2015}
2016
2017type Status = "active" | "inactive";
2018"#;
2019 let info = extract_interface(Path::new("test.ts"), source).unwrap();
2020
2021 assert!(
2022 !info.classes.is_empty(),
2023 "Should find interface/type declarations, got: {:?}",
2024 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2025 );
2026 }
2027
2028 #[test]
2033 fn test_extract_interface_java_class() {
2034 let source = r#"
2035/**
2036 * Service for managing users.
2037 */
2038public class UserService {
2039 public String getUser(String id) {
2040 return id;
2041 }
2042
2043 private void internalCleanup() {}
2044}
2045"#;
2046 let info = extract_interface(Path::new("test.java"), source).unwrap();
2047
2048 assert!(
2049 !info.classes.is_empty(),
2050 "Should find Java class, got: {:?}",
2051 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2052 );
2053
2054 if let Some(cls) = info.classes.iter().find(|c| c.name == "UserService") {
2055 assert!(!cls.methods.is_empty(), "Should find public methods");
2056 }
2057 }
2058
2059 #[test]
2064 fn test_extract_interface_c_functions() {
2065 let source = r#"
2066int add(int a, int b) {
2067 return a + b;
2068}
2069
2070static int internal_helper(void) {
2071 return 42;
2072}
2073"#;
2074 let info = extract_interface(Path::new("test.c"), source).unwrap();
2075
2076 assert_eq!(
2078 info.functions.len(),
2079 1,
2080 "Should find 1 non-static function, got: {:?}",
2081 info.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
2082 );
2083 assert_eq!(info.functions[0].name, "add");
2084 }
2085
2086 #[test]
2091 fn test_extract_interface_ruby_class() {
2092 let source = r#"
2093class UserManager
2094 def find_user(id)
2095 # find user
2096 end
2097
2098 def _private_method
2099 # private
2100 end
2101end
2102"#;
2103 let info = extract_interface(Path::new("test.rb"), source).unwrap();
2104
2105 assert!(
2106 !info.classes.is_empty(),
2107 "Should find Ruby class, got: {:?}",
2108 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2109 );
2110
2111 if let Some(cls) = info.classes.iter().find(|c| c.name == "UserManager") {
2112 assert_eq!(
2113 cls.methods.len(),
2114 1,
2115 "Should find 1 public method, got: {:?}",
2116 cls.methods.iter().map(|m| &m.name).collect::<Vec<_>>()
2117 );
2118 assert_eq!(cls.methods[0].name, "find_user");
2119 assert_eq!(cls.private_method_count, 1);
2120 }
2121 }
2122
2123 #[test]
2128 fn test_is_public_for_go() {
2129 assert!(is_public_for_lang("ProcessData", Language::Go));
2130 assert!(!is_public_for_lang("processData", Language::Go));
2131 }
2132
2133 #[test]
2134 fn test_is_public_for_python() {
2135 assert!(is_public_for_lang("process_data", Language::Python));
2136 assert!(!is_public_for_lang("_private", Language::Python));
2137 }
2138
2139 #[test]
2144 fn test_is_supported_source_file() {
2145 assert!(is_supported_source_file(Path::new("test.py")));
2146 assert!(is_supported_source_file(Path::new("test.rs")));
2147 assert!(is_supported_source_file(Path::new("test.go")));
2148 assert!(is_supported_source_file(Path::new("test.ts")));
2149 assert!(is_supported_source_file(Path::new("test.java")));
2150 assert!(is_supported_source_file(Path::new("test.c")));
2151 assert!(is_supported_source_file(Path::new("test.rb")));
2152 assert!(is_supported_source_file(Path::new("test.cs")));
2153 assert!(!is_supported_source_file(Path::new("test.txt")));
2154 assert!(!is_supported_source_file(Path::new("test.md")));
2155 }
2156}