Skip to main content

tldr_cli/commands/patterns/
interface.rs

1//! Interface command - Public API extraction
2//!
3//! Extracts the public interface (API surface) from source files.
4//! Supports all languages with tree-sitter grammars: Python, Rust, Go,
5//! TypeScript, JavaScript, Java, C, C++, Ruby, C#, Scala, PHP, Lua, Luau,
6//! Elixir, and OCaml.
7//!
8//! # Features
9//!
10//! - Extracts public functions (language-appropriate visibility rules)
11//! - Extracts public classes/structs/traits with their public methods
12//! - Captures export declarations when present (e.g., Python `__all__`)
13//! - Marks async functions/methods
14//! - Includes function signatures with type annotations
15//! - Includes docstrings/doc comments when present
16//!
17//! # Example
18//!
19//! ```bash
20//! tldr interface src/api.py
21//! tldr interface src/lib.rs
22//! tldr interface src/ --format text
23//! ```
24
25use 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// =============================================================================
39// CLI Arguments
40// =============================================================================
41
42/// Arguments for the interface command.
43#[derive(Debug, Clone, Args)]
44pub struct InterfaceArgs {
45    /// File or directory to analyze
46    #[arg(required = true)]
47    pub path: PathBuf,
48
49    /// Project root for path validation
50    #[arg(long)]
51    pub project_root: Option<PathBuf>,
52}
53
54// =============================================================================
55// Language-Aware Node Kind Configuration
56// =============================================================================
57
58/// Node kinds that represent function definitions for a given language.
59fn 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"], // `def` and `defp` are calls in elixir tree-sitter
79        Language::Ocaml => &["let_binding", "value_definition"],
80        _ => &[],
81    }
82}
83
84/// Node kinds that represent class/struct/trait definitions for a given language.
85fn 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 => &[], // Lua doesn't have class syntax
111        Language::Elixir => &["call"],         // defmodule is a call in elixir tree-sitter
112        Language::Ocaml => &["module_definition", "type_definition"],
113        _ => &[],
114    }
115}
116
117/// Node kinds that represent decorated/annotated definitions.
118fn 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
129/// Node kinds for method definitions inside classes/structs.
130fn 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// =============================================================================
151// Public Name Detection (Language-Aware)
152// =============================================================================
153
154/// Check if a name is public based on language conventions.
155///
156/// - Python: names not starting with `_`
157/// - Rust: `pub` keyword (checked at node level, not name level)
158/// - Go: names starting with uppercase
159/// - Ruby: methods not starting with `_` (private is keyword-based)
160/// - Other languages: generally all names are considered public
161///   (visibility modifiers are checked at the node level)
162#[inline]
163pub fn is_public_name(name: &str) -> bool {
164    !name.starts_with('_')
165}
166
167/// Check if a name is public based on language-specific rules.
168fn 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            // Go exports start with an uppercase letter
175            name.chars().next().is_some_and(|c| c.is_uppercase())
176        }
177        // For Rust, Java, TS, C#, etc. - visibility is determined by modifiers,
178        // not naming. We check modifiers at the node level.
179        _ => true,
180    }
181}
182
183/// Check if a Rust node has `pub` visibility.
184fn 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
196/// Check if a Java/C#/TS node has public access modifier.
197fn has_public_modifier(node: Node, source: &[u8]) -> bool {
198    // Check modifiers child
199    if let Some(modifiers) = node.child_by_field_name("modifiers") {
200        let text = node_text(modifiers, source);
201        return text.contains("public");
202    }
203    // Also check for direct modifier children
204    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            // For TypeScript: check for accessibility_modifier
214            if kind == "accessibility_modifier" {
215                let text = node_text(child, source);
216                return text == "public";
217            }
218        }
219    }
220    // In Java, default (package-private) is not public, but for interface extraction
221    // we treat non-private as public for utility
222    true
223}
224
225/// Check if a C/C++ function is `static` (file-local, not public).
226fn is_c_static(node: Node, source: &[u8]) -> bool {
227    // Check for storage_class_specifier "static" before the function
228    if let Some(prev) = node.prev_sibling() {
229        if prev.kind() == "storage_class_specifier" {
230            return node_text(prev, source) == "static";
231        }
232    }
233    // Check declarator specifiers
234    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
244/// Language-aware visibility check for a node.
245fn 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        // For other languages, default to public
258        _ => true,
259    }
260}
261
262// =============================================================================
263// Name Extraction
264// =============================================================================
265
266/// Get the name of a definition node based on language.
267fn get_node_name<'a>(node: Node<'a>, source: &'a [u8], lang: Language) -> Option<String> {
268    // First try the common "name" field
269    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            // C/C++ function_definition has a "declarator" field
276            if let Some(declarator) = node.child_by_field_name("declarator") {
277                return extract_c_declarator_name(declarator, source);
278            }
279        }
280        Language::Go => {
281            // Go type_declaration wraps type_spec which has the name
282            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            // Rust impl_item doesn't always have a "name" field
296            if node.kind() == "impl_item" {
297                // Look for the type being implemented
298                if let Some(type_node) = node.child_by_field_name("type") {
299                    return Some(node_text(type_node, source).to_string());
300                }
301                // Fallback: find type_identifier child
302                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            // In Elixir, `def` and `defmodule` are call nodes
313            // The first argument is the name
314            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                                // For def/defp, the first arg may be a call (name + params)
321                                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            // Try the "name" field first (already done above), then check child nodes
335            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            // Ruby methods: first identifier child after "def"
345            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
359/// Extract name from a C/C++ declarator (which may be nested).
360fn extract_c_declarator_name(declarator: Node, source: &[u8]) -> Option<String> {
361    // The declarator could be a function_declarator wrapping an identifier
362    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    // function_declarator has a "declarator" field that is the name
369    if let Some(inner) = declarator.child_by_field_name("declarator") {
370        return extract_c_declarator_name(inner, source);
371    }
372    // Try first child
373    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
381// =============================================================================
382// __all__ Extraction (Python-specific)
383// =============================================================================
384
385/// Extract the contents of `__all__` if defined in the module (Python only).
386pub 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
411/// Extract string elements from a list node.
412fn 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
437// =============================================================================
438// Signature Extraction (Language-Aware)
439// =============================================================================
440
441/// Extract the function signature from a definition node.
442///
443/// For Python, reconstructs from parameter nodes.
444/// For other languages, extracts the raw text of parameters and return type.
445pub 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
460/// Python signature: reconstruct from parameter nodes.
461fn 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
501/// Rust signature: extract parameters and return type.
502fn 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
517/// Go signature: extract parameters and return type.
518fn 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
533/// Java/C# signature: extract parameters from formal_parameters.
534fn extract_java_like_signature(func_node: Node, source: &[u8]) -> String {
535    let mut sig = String::new();
536
537    // Try "parameters" field first, then look for "formal_parameters" or a parameter_list child
538    let params_node = func_node.child_by_field_name("parameters").or_else(|| {
539        // Search for formal_parameters or parameter_list node among children
540        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    // For Java, check for return type (it's the "type" field)
552    if let Some(ret) = func_node.child_by_field_name("type") {
553        // Prepend return type
554        let ret_text = node_text(ret, source);
555        sig = format!("{}: {}", sig, ret_text);
556    }
557
558    sig
559}
560
561/// TypeScript/JavaScript signature.
562fn 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
577/// C/C++ signature: extract from declarator.
578fn 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        // The declarator includes function name and parameter list
583        // We want just the parameters portion
584        if let Some(params) = declarator.child_by_field_name("parameters") {
585            sig.push_str(node_text(params, source));
586        }
587    }
588
589    // Return type is typically the first child (type specifier)
590    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
600/// Ruby signature.
601fn 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        // Check for method_parameters child
606        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
616/// PHP signature.
617fn 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
632/// Scala signature.
633fn 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
648/// Generic signature: try to find parameters/return type fields.
649fn 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
659/// Extract a typed parameter (name: type) - Python-specific.
660fn 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
678/// Extract a default parameter (name=default) - Python-specific.
679fn 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
692/// Extract a typed default parameter (name: type = default) - Python-specific.
693fn 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
714// =============================================================================
715// Function Info Extraction (Language-Aware)
716// =============================================================================
717
718/// Extract function information from a function definition node.
719pub 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
735/// Detect if a function is async.
736fn 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            // Check for "async" keyword child
744            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            // Check for async keyword
755            let func_text = node_text(func_node, source);
756            func_text.starts_with("async ")
757        }
758        Language::CSharp => {
759            // Check modifiers for "async"
760            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            // Elixir doesn't have async keyword in the traditional sense
767            false
768        }
769        _ => false,
770    }
771}
772
773// =============================================================================
774// Docstring / Doc Comment Extraction (Language-Aware)
775// =============================================================================
776
777/// Extract docstring or doc comment from a function or class node.
778fn 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
793/// Python docstring: first string in function/class body.
794fn 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
818/// Rust doc comments: /// or //! preceding the node.
819fn 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            // Skip attributes between doc comments
838        } 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
852/// Go doc comment: preceding line comment block.
853fn 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
876/// Javadoc-style: /** ... */ preceding the node.
877fn 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            // Skip annotations/attributes
897        } else {
898            break;
899        }
900        prev = sib.prev_sibling();
901    }
902    None
903}
904
905/// JSDoc: /** ... */ preceding the node.
906fn extract_jsdoc_comment(node: Node, source: &[u8]) -> Option<String> {
907    extract_javadoc_comment(node, source)
908}
909
910/// Ruby: # comments preceding the node.
911fn 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
934/// Elixir: @doc or @moduledoc preceding the node.
935fn 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                // Extract the string content
943                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            // skip
958        } else {
959            break;
960        }
961        prev = sib.prev_sibling();
962    }
963    None
964}
965
966// =============================================================================
967// Class Info Extraction (Language-Aware)
968// =============================================================================
969
970/// Extract class/struct/trait information from a definition node.
971pub 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    // Extract base classes / implemented interfaces
976    let bases = extract_base_classes(class_node, source, lang);
977
978    // Extract methods
979    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
1005/// Find the body/block node of a class/struct definition.
1006fn find_body_node<'a>(class_node: Node<'a>, lang: Language) -> Option<Node<'a>> {
1007    // Try common field names
1008    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            // For impl_item, look for declaration_list
1018            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            // class_body or interface_body
1028            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            // Ruby class body is inside a body_statement child
1060            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            // Fallback: use the class node itself
1067            Some(class_node)
1068        }
1069        _ => {
1070            // Default: try common body kinds
1071            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
1087/// Collect methods from a class body node.
1088fn 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            // Handle decorated methods (Python, Java annotations, etc.)
1111            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
1123/// Check if a method is public based on language conventions.
1124fn 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
1136/// Extract base classes / superclasses / implemented interfaces.
1137fn 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            // Check for "superclass" and "interfaces" fields
1153            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            // Also check super_interfaces for Java interface declarations
1165            if let Some(extends) = class_node.child_by_field_name("type_parameters") {
1166                // type parameters are not bases, skip
1167                let _ = extends;
1168            }
1169        }
1170        Language::Rust => {
1171            // For trait_item, look for trait bounds
1172            // For impl_item, look for the trait being implemented
1173            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            // Check for extends_clause or implements_clause
1181            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            // Go type_declaration doesn't have base classes per se
1208            // But embedded structs could be found in struct fields
1209        }
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
1221/// Find a function/class definition inside a decorated_definition node.
1222fn 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
1230/// Extract method information from a function definition node.
1231fn 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
1243// =============================================================================
1244// Interface Extraction (Language-Aware)
1245// =============================================================================
1246
1247/// Extract the public interface from a source file.
1248///
1249/// Detects the language from the file extension and uses the appropriate
1250/// tree-sitter grammar and node kinds.
1251pub 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
1256/// Extract the public interface from a source file with an explicit language.
1257pub 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    // Parse with ParserPool (multi-language)
1265    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    // Extract __all__ exports (Python-specific)
1273    let all_exports = if lang == Language::Python {
1274        extract_all_exports(root, source_bytes)
1275    } else {
1276        None
1277    };
1278
1279    // Determine node kinds for this language
1280    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    // Extract public functions and classes
1285    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
1302/// Collect top-level function and class definitions from the AST root.
1303fn 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                // For Elixir, filter to only `def` (not `defp` which is private)
1321                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                // For Elixir, filter to only `defmodule`
1337                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            // Handle decorated definitions (Python)
1348            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            // For some languages, definitions may be nested in other constructs
1359            // (e.g., Go has top-level declarations that contain the actual definitions)
1360            // Recurse one level for these
1361            // PHP wraps everything in a program > php_tag + declaration list
1362            // Recurse into children.
1363            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
1382// =============================================================================
1383// Text Formatting
1384// =============================================================================
1385
1386/// Format interface info as human-readable text.
1387pub fn format_interface_text(info: &InterfaceInfo) -> String {
1388    let mut lines = Vec::new();
1389
1390    // Header
1391    lines.push(format!("File: {}", info.file));
1392    lines.push(String::new());
1393
1394    // __all__ exports
1395    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    // Functions
1404    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                // Truncate long docstrings
1414                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    // Classes
1426    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    // Summary
1458    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
1469// =============================================================================
1470// Entry Point
1471// =============================================================================
1472
1473/// Check if a file has a supported source code extension.
1474fn is_supported_source_file(path: &Path) -> bool {
1475    Language::from_path(path).is_some()
1476}
1477
1478/// Run the interface command.
1479pub fn run(args: InterfaceArgs, format: OutputFormat) -> anyhow::Result<()> {
1480    let path = &args.path;
1481
1482    if path.is_dir() {
1483        // Validate directory
1484        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        // Collect all supported source files recursively
1491        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        // Sort for deterministic output
1498        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                    // Skip files that fail to parse (unsupported grammars, etc.)
1506                    continue;
1507                }
1508            }
1509        }
1510
1511        // Output
1512        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        // Single file
1530        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        // Output
1540        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
1558// =============================================================================
1559// Utilities
1560// =============================================================================
1561
1562/// Get the text content of a node.
1563fn node_text<'a>(node: Node, source: &'a [u8]) -> &'a str {
1564    node.utf8_text(source).unwrap_or("")
1565}
1566
1567// =============================================================================
1568// Tests
1569// =============================================================================
1570
1571#[cfg(test)]
1572mod tests {
1573    use super::*;
1574
1575    // -------------------------------------------------------------------------
1576    // is_public_name tests (backward-compatible)
1577    // -------------------------------------------------------------------------
1578
1579    #[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    // -------------------------------------------------------------------------
1596    // Python: extract_all_exports tests
1597    // -------------------------------------------------------------------------
1598
1599    #[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    // -------------------------------------------------------------------------
1635    // Python: extract_function_signature tests
1636    // -------------------------------------------------------------------------
1637
1638    #[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    // -------------------------------------------------------------------------
1677    // Python: extract_interface tests
1678    // -------------------------------------------------------------------------
1679
1680    #[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    // -------------------------------------------------------------------------
1804    // format_interface_text tests
1805    // -------------------------------------------------------------------------
1806
1807    #[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    // =========================================================================
1842    // Multi-language tests
1843    // =========================================================================
1844
1845    // -------------------------------------------------------------------------
1846    // Rust
1847    // -------------------------------------------------------------------------
1848
1849    #[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        // Should find struct and impl as classes
1919        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        // Check the struct
1926        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    // -------------------------------------------------------------------------
1945    // Go
1946    // -------------------------------------------------------------------------
1947
1948    #[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        // Go: exported functions start with uppercase
1965        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    // -------------------------------------------------------------------------
1979    // TypeScript
1980    // -------------------------------------------------------------------------
1981
1982    #[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        // Should find both the class and the function
2000        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    // -------------------------------------------------------------------------
2029    // Java
2030    // -------------------------------------------------------------------------
2031
2032    #[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    // -------------------------------------------------------------------------
2060    // C
2061    // -------------------------------------------------------------------------
2062
2063    #[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        // Non-static C functions should be public
2077        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    // -------------------------------------------------------------------------
2087    // Ruby
2088    // -------------------------------------------------------------------------
2089
2090    #[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    // -------------------------------------------------------------------------
2124    // is_public_for_lang tests
2125    // -------------------------------------------------------------------------
2126
2127    #[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    // -------------------------------------------------------------------------
2140    // is_supported_source_file tests
2141    // -------------------------------------------------------------------------
2142
2143    #[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}