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 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// =============================================================================
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)
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                // For Elixir, filter to only `def` (not `defp` which is private)
1323                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                // For Elixir, filter to only `defmodule`
1339                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            // Handle decorated definitions (Python)
1350            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            // For some languages, definitions may be nested in other constructs
1361            // (e.g., Go has top-level declarations that contain the actual definitions)
1362            // Recurse one level for these
1363            // PHP wraps everything in a program > php_tag + declaration list
1364            // Recurse into children.
1365            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
1384// =============================================================================
1385// Text Formatting
1386// =============================================================================
1387
1388/// Format interface info as human-readable text.
1389pub fn format_interface_text(info: &InterfaceInfo) -> String {
1390    let mut lines = Vec::new();
1391
1392    // Header
1393    lines.push(format!("File: {}", info.file));
1394    lines.push(String::new());
1395
1396    // __all__ exports
1397    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    // Functions
1406    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                // Truncate long docstrings
1416                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    // Classes
1428    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    // Summary
1460    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
1471// =============================================================================
1472// Entry Point
1473// =============================================================================
1474
1475/// Check if a file has a supported source code extension.
1476fn is_supported_source_file(path: &Path) -> bool {
1477    Language::from_path(path).is_some()
1478}
1479
1480/// Run the interface command.
1481pub fn run(args: InterfaceArgs, format: OutputFormat) -> anyhow::Result<()> {
1482    let path = &args.path;
1483
1484    if path.is_dir() {
1485        // Validate directory
1486        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        // Collect all supported source files recursively
1493        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        // Sort for deterministic output
1509        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                    // Skip files that fail to parse (unsupported grammars, etc.)
1517                    continue;
1518                }
1519            }
1520        }
1521
1522        // Output
1523        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        // Single file
1541        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        // Output
1551        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
1569// =============================================================================
1570// Utilities
1571// =============================================================================
1572
1573/// Get the text content of a node.
1574fn node_text<'a>(node: Node, source: &'a [u8]) -> &'a str {
1575    node.utf8_text(source).unwrap_or("")
1576}
1577
1578// =============================================================================
1579// Tests
1580// =============================================================================
1581
1582#[cfg(test)]
1583mod tests {
1584    use super::*;
1585
1586    // -------------------------------------------------------------------------
1587    // is_public_name tests (backward-compatible)
1588    // -------------------------------------------------------------------------
1589
1590    #[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    // -------------------------------------------------------------------------
1607    // Python: extract_all_exports tests
1608    // -------------------------------------------------------------------------
1609
1610    #[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    // -------------------------------------------------------------------------
1646    // Python: extract_function_signature tests
1647    // -------------------------------------------------------------------------
1648
1649    #[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    // -------------------------------------------------------------------------
1688    // Python: extract_interface tests
1689    // -------------------------------------------------------------------------
1690
1691    #[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    // -------------------------------------------------------------------------
1815    // format_interface_text tests
1816    // -------------------------------------------------------------------------
1817
1818    #[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    // =========================================================================
1853    // Multi-language tests
1854    // =========================================================================
1855
1856    // -------------------------------------------------------------------------
1857    // Rust
1858    // -------------------------------------------------------------------------
1859
1860    #[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        // Should find struct and impl as classes
1930        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        // Check the struct
1937        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    // -------------------------------------------------------------------------
1956    // Go
1957    // -------------------------------------------------------------------------
1958
1959    #[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        // Go: exported functions start with uppercase
1976        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    // -------------------------------------------------------------------------
1990    // TypeScript
1991    // -------------------------------------------------------------------------
1992
1993    #[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        // Should find both the class and the function
2011        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    // -------------------------------------------------------------------------
2040    // Java
2041    // -------------------------------------------------------------------------
2042
2043    #[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    // -------------------------------------------------------------------------
2071    // C
2072    // -------------------------------------------------------------------------
2073
2074    #[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        // Non-static C functions should be public
2088        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    // -------------------------------------------------------------------------
2098    // Ruby
2099    // -------------------------------------------------------------------------
2100
2101    #[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    // -------------------------------------------------------------------------
2135    // is_public_for_lang tests
2136    // -------------------------------------------------------------------------
2137
2138    #[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    // -------------------------------------------------------------------------
2151    // is_supported_source_file tests
2152    // -------------------------------------------------------------------------
2153
2154    #[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}