1use std::path::{Path, PathBuf};
26
27use clap::Args;
28use tree_sitter::Node;
29use walkdir::WalkDir;
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)
1311 -> (Vec<FunctionInfo>, Vec<ClassInfo>)
1312{
1313 let mut functions = Vec::new();
1314 let mut classes = Vec::new();
1315 let mut cursor = root.walk();
1316
1317 for child in root.children(&mut cursor) {
1318 let kind = child.kind();
1319
1320 if func_kinds.contains(&kind) {
1321 if is_node_public(child, source, lang) {
1322 if lang == Language::Elixir {
1324 if let Some(target) = child.child(0) {
1325 let target_text = node_text(target, source);
1326 if target_text == "defp" {
1327 continue;
1328 }
1329 if target_text != "def" {
1330 continue;
1331 }
1332 }
1333 }
1334 functions.push(extract_function_info(child, source, lang));
1335 }
1336 } else if class_kinds.contains(&kind) {
1337 if is_node_public(child, source, lang) {
1338 if lang == Language::Elixir {
1340 if let Some(target) = child.child(0) {
1341 if node_text(target, source) != "defmodule" {
1342 continue;
1343 }
1344 }
1345 }
1346 classes.push(extract_class_info(child, source, lang));
1347 }
1348 } else if decorator_kinds.contains(&kind) {
1349 if let Some(def) = find_definition_in_decorated(child, func_kinds) {
1351 if is_node_public(def, source, lang) {
1352 functions.push(extract_function_info(def, source, lang));
1353 }
1354 } else if let Some(class_def) = find_definition_in_decorated(child, class_kinds) {
1355 if is_node_public(class_def, source, lang) {
1356 classes.push(extract_class_info(class_def, source, lang));
1357 }
1358 }
1359 } else if lang == Language::Php {
1360 let mut inner_cursor = child.walk();
1366 for inner_child in child.children(&mut inner_cursor) {
1367 let inner_kind = inner_child.kind();
1368 if func_kinds.contains(&inner_kind) {
1369 if is_node_public(inner_child, source, lang) {
1370 functions.push(extract_function_info(inner_child, source, lang));
1371 }
1372 } else if class_kinds.contains(&inner_kind)
1373 && is_node_public(inner_child, source, lang)
1374 {
1375 classes.push(extract_class_info(inner_child, source, lang));
1376 }
1377 }
1378 }
1379 }
1380
1381 (functions, classes)
1382}
1383
1384pub fn format_interface_text(info: &InterfaceInfo) -> String {
1390 let mut lines = Vec::new();
1391
1392 lines.push(format!("File: {}", info.file));
1394 lines.push(String::new());
1395
1396 if let Some(ref exports) = info.all_exports {
1398 lines.push("Exports (__all__):".to_string());
1399 for name in exports {
1400 lines.push(format!(" {}", name));
1401 }
1402 lines.push(String::new());
1403 }
1404
1405 if !info.functions.is_empty() {
1407 lines.push("Functions:".to_string());
1408 for func in &info.functions {
1409 let async_marker = if func.is_async { "async " } else { "" };
1410 lines.push(format!(
1411 " {}def {}{} [line {}]",
1412 async_marker, func.name, func.signature, func.lineno
1413 ));
1414 if let Some(ref doc) = func.docstring {
1415 let doc_preview = if doc.len() > 60 {
1417 format!("{}...", &doc[..57])
1418 } else {
1419 doc.clone()
1420 };
1421 lines.push(format!(" \"{}\"", doc_preview));
1422 }
1423 }
1424 lines.push(String::new());
1425 }
1426
1427 if !info.classes.is_empty() {
1429 lines.push("Classes:".to_string());
1430 for class in &info.classes {
1431 let bases_str = if class.bases.is_empty() {
1432 String::new()
1433 } else {
1434 format!("({})", class.bases.join(", "))
1435 };
1436 lines.push(format!(
1437 " class {}{} [line {}]",
1438 class.name, bases_str, class.lineno
1439 ));
1440
1441 for method in &class.methods {
1442 let async_marker = if method.is_async { "async " } else { "" };
1443 lines.push(format!(
1444 " {}def {}{}",
1445 async_marker, method.name, method.signature
1446 ));
1447 }
1448
1449 if class.private_method_count > 0 {
1450 lines.push(format!(
1451 " ({} private methods)",
1452 class.private_method_count
1453 ));
1454 }
1455 }
1456 lines.push(String::new());
1457 }
1458
1459 let total_methods: u32 = info.classes.iter().map(|c| c.methods.len() as u32).sum();
1461 lines.push(format!(
1462 "Summary: {} functions, {} classes, {} public methods",
1463 info.functions.len(),
1464 info.classes.len(),
1465 total_methods
1466 ));
1467
1468 lines.join("\n")
1469}
1470
1471fn is_supported_source_file(path: &Path) -> bool {
1477 Language::from_path(path).is_some()
1478}
1479
1480pub fn run(args: InterfaceArgs, format: OutputFormat) -> anyhow::Result<()> {
1482 let path = &args.path;
1483
1484 if path.is_dir() {
1485 let canonical_dir = if let Some(ref root) = args.project_root {
1487 super::validation::validate_file_path_in_project(path, root)?
1488 } else {
1489 validate_directory_path(path)?
1490 };
1491
1492 let mut results = Vec::new();
1494 let mut entries: Vec<PathBuf> = WalkDir::new(&canonical_dir)
1495 .follow_links(false)
1496 .into_iter()
1497 .filter_entry(|e| {
1498 e.file_name()
1499 .to_str()
1500 .map(|s| !s.starts_with('.'))
1501 .unwrap_or(true)
1502 })
1503 .filter_map(|e| e.ok())
1504 .filter(|e| e.path().is_file() && is_supported_source_file(e.path()))
1505 .map(|e| e.path().to_path_buf())
1506 .collect();
1507
1508 entries.sort();
1510
1511 for file_path in entries {
1512 let source = read_file_safe(&file_path)?;
1513 match extract_interface(&file_path, &source) {
1514 Ok(info) => results.push(info),
1515 Err(_) => {
1516 continue;
1518 }
1519 }
1520 }
1521
1522 match format {
1524 OutputFormat::Text => {
1525 for info in &results {
1526 println!("{}", format_interface_text(info));
1527 println!();
1528 }
1529 }
1530 OutputFormat::Compact => {
1531 let json = serde_json::to_string(&results)?;
1532 println!("{}", json);
1533 }
1534 _ => {
1535 let json = serde_json::to_string_pretty(&results)?;
1536 println!("{}", json);
1537 }
1538 }
1539 } else {
1540 let canonical_path = if let Some(ref root) = args.project_root {
1542 super::validation::validate_file_path_in_project(path, root)?
1543 } else {
1544 validate_file_path(path)?
1545 };
1546
1547 let source = read_file_safe(&canonical_path)?;
1548 let info = extract_interface(&canonical_path, &source)?;
1549
1550 match format {
1552 OutputFormat::Text => {
1553 println!("{}", format_interface_text(&info));
1554 }
1555 OutputFormat::Compact => {
1556 let json = serde_json::to_string(&info)?;
1557 println!("{}", json);
1558 }
1559 _ => {
1560 let json = serde_json::to_string_pretty(&info)?;
1561 println!("{}", json);
1562 }
1563 }
1564 }
1565
1566 Ok(())
1567}
1568
1569fn node_text<'a>(node: Node, source: &'a [u8]) -> &'a str {
1575 node.utf8_text(source).unwrap_or("")
1576}
1577
1578#[cfg(test)]
1583mod tests {
1584 use super::*;
1585
1586 #[test]
1591 fn test_is_public_name_public() {
1592 assert!(is_public_name("my_function"));
1593 assert!(is_public_name("MyClass"));
1594 assert!(is_public_name("process"));
1595 assert!(is_public_name("x"));
1596 }
1597
1598 #[test]
1599 fn test_is_public_name_private() {
1600 assert!(!is_public_name("_private"));
1601 assert!(!is_public_name("__dunder__"));
1602 assert!(!is_public_name("_PrivateClass"));
1603 assert!(!is_public_name("__init__"));
1604 }
1605
1606 #[test]
1611 fn test_extract_all_exports_present() {
1612 let source = r#"
1613__all__ = ['foo', 'bar', 'Baz']
1614
1615def foo():
1616 pass
1617"#;
1618 let pool = ParserPool::new();
1619 let tree = pool.parse(source, Language::Python).unwrap();
1620 let root = tree.root_node();
1621
1622 let exports = extract_all_exports(root, source.as_bytes());
1623 assert!(exports.is_some());
1624 let exports = exports.unwrap();
1625 assert_eq!(exports.len(), 3);
1626 assert!(exports.contains(&"foo".to_string()));
1627 assert!(exports.contains(&"bar".to_string()));
1628 assert!(exports.contains(&"Baz".to_string()));
1629 }
1630
1631 #[test]
1632 fn test_extract_all_exports_absent() {
1633 let source = r#"
1634def foo():
1635 pass
1636"#;
1637 let pool = ParserPool::new();
1638 let tree = pool.parse(source, Language::Python).unwrap();
1639 let root = tree.root_node();
1640
1641 let exports = extract_all_exports(root, source.as_bytes());
1642 assert!(exports.is_none());
1643 }
1644
1645 #[test]
1650 fn test_extract_function_signature_simple() {
1651 let source = "def foo(x, y): pass";
1652 let pool = ParserPool::new();
1653 let tree = pool.parse(source, Language::Python).unwrap();
1654 let root = tree.root_node();
1655 let func_node = root.child(0).unwrap();
1656
1657 let sig = extract_function_signature(func_node, source.as_bytes(), Language::Python);
1658 assert_eq!(sig, "(x, y)");
1659 }
1660
1661 #[test]
1662 fn test_extract_function_signature_typed() {
1663 let source = "def foo(x: int, y: str) -> bool: pass";
1664 let pool = ParserPool::new();
1665 let tree = pool.parse(source, Language::Python).unwrap();
1666 let root = tree.root_node();
1667 let func_node = root.child(0).unwrap();
1668
1669 let sig = extract_function_signature(func_node, source.as_bytes(), Language::Python);
1670 assert!(sig.contains("x: int"), "sig = {:?}", sig);
1671 assert!(sig.contains("y: str"), "sig = {:?}", sig);
1672 assert!(sig.contains("-> bool"), "sig = {:?}", sig);
1673 }
1674
1675 #[test]
1676 fn test_extract_function_signature_default() {
1677 let source = "def foo(x: int = 10): pass";
1678 let pool = ParserPool::new();
1679 let tree = pool.parse(source, Language::Python).unwrap();
1680 let root = tree.root_node();
1681 let func_node = root.child(0).unwrap();
1682
1683 let sig = extract_function_signature(func_node, source.as_bytes(), Language::Python);
1684 assert!(sig.contains("x: int = 10") || sig.contains("x: int=10"));
1685 }
1686
1687 #[test]
1692 fn test_extract_interface_public_functions() {
1693 let source = r#"
1694def public_func():
1695 """A public function."""
1696 pass
1697
1698def _private_func():
1699 pass
1700"#;
1701 let info = extract_interface(Path::new("test.py"), source).unwrap();
1702
1703 assert_eq!(info.functions.len(), 1);
1704 assert_eq!(info.functions[0].name, "public_func");
1705 }
1706
1707 #[test]
1708 fn test_extract_interface_public_classes() {
1709 let source = r#"
1710class PublicClass:
1711 def public_method(self):
1712 pass
1713
1714 def _private_method(self):
1715 pass
1716
1717class _PrivateClass:
1718 pass
1719"#;
1720 let info = extract_interface(Path::new("test.py"), source).unwrap();
1721
1722 assert_eq!(info.classes.len(), 1);
1723 assert_eq!(info.classes[0].name, "PublicClass");
1724 assert_eq!(info.classes[0].methods.len(), 1);
1725 assert_eq!(info.classes[0].methods[0].name, "public_method");
1726 assert_eq!(info.classes[0].private_method_count, 1);
1727 }
1728
1729 #[test]
1730 fn test_extract_interface_async_function() {
1731 let source = r#"
1732async def async_func():
1733 pass
1734
1735def sync_func():
1736 pass
1737"#;
1738 let info = extract_interface(Path::new("test.py"), source).unwrap();
1739
1740 assert_eq!(info.functions.len(), 2);
1741
1742 let async_fn = info.functions.iter().find(|f| f.name == "async_func");
1743 assert!(async_fn.is_some());
1744 assert!(async_fn.unwrap().is_async);
1745
1746 let sync_fn = info.functions.iter().find(|f| f.name == "sync_func");
1747 assert!(sync_fn.is_some());
1748 assert!(!sync_fn.unwrap().is_async);
1749 }
1750
1751 #[test]
1752 fn test_extract_interface_with_all() {
1753 let source = r#"
1754__all__ = ['foo', 'Bar']
1755
1756def foo():
1757 pass
1758
1759def bar():
1760 pass
1761
1762class Bar:
1763 pass
1764"#;
1765 let info = extract_interface(Path::new("test.py"), source).unwrap();
1766
1767 assert!(info.all_exports.is_some());
1768 let exports = info.all_exports.unwrap();
1769 assert!(exports.contains(&"foo".to_string()));
1770 assert!(exports.contains(&"Bar".to_string()));
1771 }
1772
1773 #[test]
1774 fn test_extract_interface_docstrings() {
1775 let source = r#"
1776def documented():
1777 """This is a docstring."""
1778 pass
1779
1780def undocumented():
1781 pass
1782"#;
1783 let info = extract_interface(Path::new("test.py"), source).unwrap();
1784
1785 let documented = info.functions.iter().find(|f| f.name == "documented");
1786 assert!(documented.is_some());
1787 assert!(documented.unwrap().docstring.is_some());
1788 assert!(documented
1789 .unwrap()
1790 .docstring
1791 .as_ref()
1792 .unwrap()
1793 .contains("docstring"));
1794
1795 let undocumented = info.functions.iter().find(|f| f.name == "undocumented");
1796 assert!(undocumented.is_some());
1797 assert!(undocumented.unwrap().docstring.is_none());
1798 }
1799
1800 #[test]
1801 fn test_extract_interface_class_bases() {
1802 let source = r#"
1803class Child(Parent, Mixin):
1804 pass
1805"#;
1806 let info = extract_interface(Path::new("test.py"), source).unwrap();
1807
1808 assert_eq!(info.classes.len(), 1);
1809 assert_eq!(info.classes[0].bases.len(), 2);
1810 assert!(info.classes[0].bases.contains(&"Parent".to_string()));
1811 assert!(info.classes[0].bases.contains(&"Mixin".to_string()));
1812 }
1813
1814 #[test]
1819 fn test_format_interface_text() {
1820 let info = InterfaceInfo {
1821 file: "test.py".to_string(),
1822 all_exports: Some(vec!["foo".to_string()]),
1823 functions: vec![FunctionInfo {
1824 name: "foo".to_string(),
1825 signature: "(x: int) -> str".to_string(),
1826 docstring: Some("A function.".to_string()),
1827 lineno: 5,
1828 is_async: false,
1829 }],
1830 classes: vec![ClassInfo {
1831 name: "MyClass".to_string(),
1832 lineno: 10,
1833 bases: vec!["Base".to_string()],
1834 methods: vec![MethodInfo {
1835 name: "method".to_string(),
1836 signature: "(self)".to_string(),
1837 is_async: false,
1838 }],
1839 private_method_count: 2,
1840 }],
1841 };
1842
1843 let text = format_interface_text(&info);
1844 assert!(text.contains("File: test.py"));
1845 assert!(text.contains("foo"));
1846 assert!(text.contains("MyClass"));
1847 assert!(text.contains("Base"));
1848 assert!(text.contains("method"));
1849 assert!(text.contains("2 private methods"));
1850 }
1851
1852 #[test]
1861 fn test_extract_interface_rust_pub_functions() {
1862 let source = r#"
1863/// Adds two numbers.
1864pub fn add(a: i32, b: i32) -> i32 {
1865 a + b
1866}
1867
1868fn private_helper() -> bool {
1869 true
1870}
1871
1872pub async fn async_fetch() -> String {
1873 String::new()
1874}
1875"#;
1876 let info = extract_interface(Path::new("test.rs"), source).unwrap();
1877
1878 assert_eq!(
1879 info.functions.len(),
1880 2,
1881 "Should find 2 pub functions, got: {:?}",
1882 info.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
1883 );
1884
1885 let add_fn = info.functions.iter().find(|f| f.name == "add");
1886 assert!(add_fn.is_some(), "Should find 'add' function");
1887 let add_fn = add_fn.unwrap();
1888 assert!(
1889 add_fn.signature.contains("a: i32"),
1890 "sig = {:?}",
1891 add_fn.signature
1892 );
1893 assert!(
1894 add_fn.signature.contains("-> i32"),
1895 "sig = {:?}",
1896 add_fn.signature
1897 );
1898 assert!(add_fn.docstring.is_some(), "Should have doc comment");
1899 assert!(add_fn
1900 .docstring
1901 .as_ref()
1902 .unwrap()
1903 .contains("Adds two numbers"));
1904 assert!(!add_fn.is_async);
1905
1906 let async_fn = info.functions.iter().find(|f| f.name == "async_fetch");
1907 assert!(async_fn.is_some(), "Should find 'async_fetch' function");
1908 assert!(async_fn.unwrap().is_async);
1909 }
1910
1911 #[test]
1912 fn test_extract_interface_rust_struct_impl() {
1913 let source = r#"
1914pub struct Point {
1915 pub x: f64,
1916 pub y: f64,
1917}
1918
1919impl Point {
1920 pub fn new(x: f64, y: f64) -> Self {
1921 Point { x, y }
1922 }
1923
1924 fn internal(&self) {}
1925}
1926"#;
1927 let info = extract_interface(Path::new("test.rs"), source).unwrap();
1928
1929 assert!(
1931 !info.classes.is_empty(),
1932 "Should find at least struct/impl, got: {:?}",
1933 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
1934 );
1935
1936 let point_struct = info.classes.iter().find(|c| c.name == "Point");
1938 assert!(point_struct.is_some(), "Should find Point struct/impl");
1939 }
1940
1941 #[test]
1942 fn test_extract_interface_rust_trait() {
1943 let source = r#"
1944pub trait Drawable {
1945 fn draw(&self);
1946 fn resize(&mut self, factor: f64);
1947}
1948"#;
1949 let info = extract_interface(Path::new("test.rs"), source).unwrap();
1950
1951 let trait_info = info.classes.iter().find(|c| c.name == "Drawable");
1952 assert!(trait_info.is_some(), "Should find Drawable trait");
1953 }
1954
1955 #[test]
1960 fn test_extract_interface_go_exported_functions() {
1961 let source = r#"
1962package main
1963
1964// ProcessData handles data processing.
1965func ProcessData(input string) (string, error) {
1966 return input, nil
1967}
1968
1969func internalHelper() bool {
1970 return true
1971}
1972"#;
1973 let info = extract_interface(Path::new("test.go"), source).unwrap();
1974
1975 assert_eq!(
1977 info.functions.len(),
1978 1,
1979 "Should find 1 exported function, got: {:?}",
1980 info.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
1981 );
1982 assert_eq!(info.functions[0].name, "ProcessData");
1983 assert!(
1984 info.functions[0].docstring.is_some(),
1985 "Should have doc comment"
1986 );
1987 }
1988
1989 #[test]
1994 fn test_extract_interface_typescript_class() {
1995 let source = r#"
1996class UserService {
1997 async fetchUser(id: string): Promise<User> {
1998 return {} as User;
1999 }
2000
2001 private internalMethod(): void {}
2002}
2003
2004function processData(input: string): number {
2005 return input.length;
2006}
2007"#;
2008 let info = extract_interface(Path::new("test.ts"), source).unwrap();
2009
2010 assert!(
2012 !info.functions.is_empty() || !info.classes.is_empty(),
2013 "Should find definitions: functions={:?}, classes={:?}",
2014 info.functions.iter().map(|f| &f.name).collect::<Vec<_>>(),
2015 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2016 );
2017 }
2018
2019 #[test]
2020 fn test_extract_interface_typescript_interface() {
2021 let source = r#"
2022interface User {
2023 id: string;
2024 name: string;
2025 email: string;
2026}
2027
2028type Status = "active" | "inactive";
2029"#;
2030 let info = extract_interface(Path::new("test.ts"), source).unwrap();
2031
2032 assert!(
2033 !info.classes.is_empty(),
2034 "Should find interface/type declarations, got: {:?}",
2035 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2036 );
2037 }
2038
2039 #[test]
2044 fn test_extract_interface_java_class() {
2045 let source = r#"
2046/**
2047 * Service for managing users.
2048 */
2049public class UserService {
2050 public String getUser(String id) {
2051 return id;
2052 }
2053
2054 private void internalCleanup() {}
2055}
2056"#;
2057 let info = extract_interface(Path::new("test.java"), source).unwrap();
2058
2059 assert!(
2060 !info.classes.is_empty(),
2061 "Should find Java class, got: {:?}",
2062 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2063 );
2064
2065 if let Some(cls) = info.classes.iter().find(|c| c.name == "UserService") {
2066 assert!(!cls.methods.is_empty(), "Should find public methods");
2067 }
2068 }
2069
2070 #[test]
2075 fn test_extract_interface_c_functions() {
2076 let source = r#"
2077int add(int a, int b) {
2078 return a + b;
2079}
2080
2081static int internal_helper(void) {
2082 return 42;
2083}
2084"#;
2085 let info = extract_interface(Path::new("test.c"), source).unwrap();
2086
2087 assert_eq!(
2089 info.functions.len(),
2090 1,
2091 "Should find 1 non-static function, got: {:?}",
2092 info.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
2093 );
2094 assert_eq!(info.functions[0].name, "add");
2095 }
2096
2097 #[test]
2102 fn test_extract_interface_ruby_class() {
2103 let source = r#"
2104class UserManager
2105 def find_user(id)
2106 # find user
2107 end
2108
2109 def _private_method
2110 # private
2111 end
2112end
2113"#;
2114 let info = extract_interface(Path::new("test.rb"), source).unwrap();
2115
2116 assert!(
2117 !info.classes.is_empty(),
2118 "Should find Ruby class, got: {:?}",
2119 info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2120 );
2121
2122 if let Some(cls) = info.classes.iter().find(|c| c.name == "UserManager") {
2123 assert_eq!(
2124 cls.methods.len(),
2125 1,
2126 "Should find 1 public method, got: {:?}",
2127 cls.methods.iter().map(|m| &m.name).collect::<Vec<_>>()
2128 );
2129 assert_eq!(cls.methods[0].name, "find_user");
2130 assert_eq!(cls.private_method_count, 1);
2131 }
2132 }
2133
2134 #[test]
2139 fn test_is_public_for_go() {
2140 assert!(is_public_for_lang("ProcessData", Language::Go));
2141 assert!(!is_public_for_lang("processData", Language::Go));
2142 }
2143
2144 #[test]
2145 fn test_is_public_for_python() {
2146 assert!(is_public_for_lang("process_data", Language::Python));
2147 assert!(!is_public_for_lang("_private", Language::Python));
2148 }
2149
2150 #[test]
2155 fn test_is_supported_source_file() {
2156 assert!(is_supported_source_file(Path::new("test.py")));
2157 assert!(is_supported_source_file(Path::new("test.rs")));
2158 assert!(is_supported_source_file(Path::new("test.go")));
2159 assert!(is_supported_source_file(Path::new("test.ts")));
2160 assert!(is_supported_source_file(Path::new("test.java")));
2161 assert!(is_supported_source_file(Path::new("test.c")));
2162 assert!(is_supported_source_file(Path::new("test.rb")));
2163 assert!(is_supported_source_file(Path::new("test.cs")));
2164 assert!(!is_supported_source_file(Path::new("test.txt")));
2165 assert!(!is_supported_source_file(Path::new("test.md")));
2166 }
2167}