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        // real-repo-fixes-v1 (P9.BUG-R6/R7): wire kotlin/swift surface forms
81        // for top-level/standalone function definitions.
82        Language::Kotlin | Language::Swift => &["function_declaration"],
83    }
84}
85
86/// Node kinds that represent class/struct/trait definitions for a given language.
87fn class_node_kinds(lang: Language) -> &'static [&'static str] {
88    match lang {
89        Language::Python => &["class_definition"],
90        Language::Rust => &["struct_item", "impl_item", "trait_item", "enum_item"],
91        Language::Go => &["type_declaration"],
92        Language::Java => &[
93            "class_declaration",
94            "interface_declaration",
95            "enum_declaration",
96        ],
97        Language::TypeScript | Language::JavaScript => &[
98            "class_declaration",
99            "interface_declaration",
100            "type_alias_declaration",
101        ],
102        Language::C => &["struct_specifier"],
103        Language::Cpp => &["struct_specifier", "class_specifier"],
104        Language::Ruby => &["class", "module"],
105        Language::CSharp => &[
106            "class_declaration",
107            "interface_declaration",
108            "struct_declaration",
109        ],
110        Language::Scala => &["class_definition", "object_definition", "trait_definition"],
111        Language::Php => &["class_declaration", "interface_declaration"],
112        Language::Lua | Language::Luau => &[], // Lua doesn't have class syntax
113        Language::Elixir => &["call"],         // defmodule is a call in elixir tree-sitter
114        Language::Ocaml => &["module_definition", "type_definition"],
115        // real-repo-fixes-v1 (P9.BUG-R6): kotlin classes/objects/interfaces.
116        // Kotlin's tree-sitter grammar emits `class_declaration` for
117        // `class`, `interface`, `enum class`, `data class`, etc., and
118        // `object_declaration` for singleton `object` blocks.
119        Language::Kotlin => &["class_declaration", "object_declaration"],
120        // real-repo-fixes-v1 (P9.BUG-R7): swift classes/protocols. The
121        // tree-sitter-swift grammar uses `class_declaration` for
122        // class/struct/enum/actor/extension and `protocol_declaration`
123        // separately. Including all gives a useful interface surface even
124        // when files only contain extensions (e.g.
125        // swift-collections/.../Span+Extras.swift).
126        Language::Swift => &["class_declaration", "protocol_declaration"],
127    }
128}
129
130/// Node kinds that represent decorated/annotated definitions.
131fn decorator_node_kinds(lang: Language) -> &'static [&'static str] {
132    match lang {
133        Language::Python => &["decorated_definition"],
134        Language::Java => &["annotation"],
135        Language::TypeScript | Language::JavaScript => &["decorator"],
136        Language::CSharp => &["attribute_list"],
137        Language::Rust => &["attribute_item"],
138        _ => &[],
139    }
140}
141
142/// Node kinds for method definitions inside classes/structs.
143fn method_node_kinds(lang: Language) -> &'static [&'static str] {
144    match lang {
145        Language::Python => &["function_definition"],
146        Language::Rust => &["function_item"],
147        Language::Go => &["method_declaration"],
148        Language::Java => &["method_declaration", "constructor_declaration"],
149        Language::TypeScript | Language::JavaScript => {
150            &["method_definition", "public_field_definition"]
151        }
152        Language::C | Language::Cpp => &["function_definition"],
153        Language::Ruby => &["method", "singleton_method"],
154        Language::CSharp => &["method_declaration", "constructor_declaration"],
155        Language::Scala => &["function_definition", "def_definition"],
156        Language::Php => &["method_declaration"],
157        Language::Elixir => &["call"],
158        Language::Ocaml => &["let_binding", "value_definition"],
159        _ => &[],
160    }
161}
162
163// =============================================================================
164// Public Name Detection (Language-Aware)
165// =============================================================================
166
167/// Check if a name is public based on language conventions.
168///
169/// - Python: names not starting with `_`
170/// - Rust: `pub` keyword (checked at node level, not name level)
171/// - Go: names starting with uppercase
172/// - Ruby: methods not starting with `_` (private is keyword-based)
173/// - Other languages: generally all names are considered public
174///   (visibility modifiers are checked at the node level)
175#[inline]
176pub fn is_public_name(name: &str) -> bool {
177    !name.starts_with('_')
178}
179
180/// Check if a name is public based on language-specific rules.
181fn is_public_for_lang(name: &str, lang: Language) -> bool {
182    match lang {
183        Language::Python | Language::Ruby | Language::Lua | Language::Luau => {
184            !name.starts_with('_')
185        }
186        Language::Go => {
187            // Go exports start with an uppercase letter
188            name.chars().next().is_some_and(|c| c.is_uppercase())
189        }
190        // For Rust, Java, TS, C#, etc. - visibility is determined by modifiers,
191        // not naming. We check modifiers at the node level.
192        _ => true,
193    }
194}
195
196/// Check if a Rust node has `pub` visibility.
197fn is_rust_pub(node: Node, source: &[u8]) -> bool {
198    for i in 0..node.child_count() {
199        if let Some(child) = node.child(i) {
200            if child.kind() == "visibility_modifier" {
201                let text = node_text(child, source);
202                return text.starts_with("pub");
203            }
204        }
205    }
206    false
207}
208
209/// Check if a Java/C#/TS node has public access modifier.
210fn has_public_modifier(node: Node, source: &[u8]) -> bool {
211    // Check modifiers child
212    if let Some(modifiers) = node.child_by_field_name("modifiers") {
213        let text = node_text(modifiers, source);
214        return text.contains("public");
215    }
216    // Also check for direct modifier children
217    for i in 0..node.child_count() {
218        if let Some(child) = node.child(i) {
219            let kind = child.kind();
220            if kind == "modifiers" || kind == "modifier" || kind == "access_modifier" {
221                let text = node_text(child, source);
222                if text.contains("public") {
223                    return true;
224                }
225            }
226            // For TypeScript: check for accessibility_modifier
227            if kind == "accessibility_modifier" {
228                let text = node_text(child, source);
229                return text == "public";
230            }
231        }
232    }
233    // In Java, default (package-private) is not public, but for interface extraction
234    // we treat non-private as public for utility
235    true
236}
237
238/// Check if a C/C++ function is `static` (file-local, not public).
239fn is_c_static(node: Node, source: &[u8]) -> bool {
240    // Check for storage_class_specifier "static" before the function
241    if let Some(prev) = node.prev_sibling() {
242        if prev.kind() == "storage_class_specifier" {
243            return node_text(prev, source) == "static";
244        }
245    }
246    // Check declarator specifiers
247    for i in 0..node.child_count() {
248        if let Some(child) = node.child(i) {
249            if child.kind() == "storage_class_specifier" && node_text(child, source) == "static" {
250                return true;
251            }
252        }
253    }
254    false
255}
256
257/// Language-aware visibility check for a node.
258fn is_node_public(node: Node, source: &[u8], lang: Language) -> bool {
259    let name = get_node_name(node, source, lang);
260    let name_str = name.as_deref().unwrap_or("");
261
262    match lang {
263        Language::Rust => {
264            // language-specific-bugs-v1 (P14.AGG14-10): `impl_item` blocks
265            // are not declarations; they are method-collecting containers
266            // and never carry their own `pub` modifier. Always treat them
267            // as visible — the methods inside still get their own
268            // `is_method_public` filtering. Without this, every
269            // `impl Foo { pub fn ... }` block was filtered out at the
270            // class layer, so `tldr interface` reported every struct with
271            // `methods: 0` even when the inherent impl exposed dozens of
272            // public methods.
273            if node.kind() == "impl_item" {
274                return true;
275            }
276            is_rust_pub(node, source)
277        }
278        Language::Go => name_str.chars().next().is_some_and(|c| c.is_uppercase()),
279        Language::Python | Language::Ruby | Language::Lua | Language::Luau => {
280            !name_str.starts_with('_')
281        }
282        Language::Java | Language::CSharp => has_public_modifier(node, source),
283        Language::C | Language::Cpp => !is_c_static(node, source),
284        // For other languages, default to public
285        _ => true,
286    }
287}
288
289// =============================================================================
290// Name Extraction
291// =============================================================================
292
293/// Get the name of a definition node based on language.
294fn get_node_name<'a>(node: Node<'a>, source: &'a [u8], lang: Language) -> Option<String> {
295    // First try the common "name" field
296    if let Some(name_node) = node.child_by_field_name("name") {
297        return Some(node_text(name_node, source).to_string());
298    }
299
300    match lang {
301        Language::C | Language::Cpp => {
302            // C/C++ function_definition has a "declarator" field
303            if let Some(declarator) = node.child_by_field_name("declarator") {
304                return extract_c_declarator_name(declarator, source);
305            }
306        }
307        Language::Go => {
308            // Go type_declaration wraps type_spec which has the name
309            if node.kind() == "type_declaration" {
310                for i in 0..node.child_count() {
311                    if let Some(child) = node.child(i) {
312                        if child.kind() == "type_spec" {
313                            if let Some(name_node) = child.child_by_field_name("name") {
314                                return Some(node_text(name_node, source).to_string());
315                            }
316                        }
317                    }
318                }
319            }
320        }
321        Language::Rust => {
322            // Rust impl_item doesn't always have a "name" field
323            if node.kind() == "impl_item" {
324                // Look for the type being implemented
325                if let Some(type_node) = node.child_by_field_name("type") {
326                    return Some(node_text(type_node, source).to_string());
327                }
328                // Fallback: find type_identifier child
329                for i in 0..node.child_count() {
330                    if let Some(child) = node.child(i) {
331                        if child.kind() == "type_identifier" || child.kind() == "generic_type" {
332                            return Some(node_text(child, source).to_string());
333                        }
334                    }
335                }
336            }
337        }
338        Language::Elixir => {
339            // In Elixir, `def` and `defmodule` are call nodes
340            // The first argument is the name
341            if node.kind() == "call" {
342                if let Some(target) = node.child(0) {
343                    let target_text = node_text(target, source);
344                    if target_text == "def"
345                        || target_text == "defp"
346                        || target_text == "defmacro"
347                        || target_text == "defmacrop"
348                        || target_text == "defmodule"
349                    {
350                        // The Elixir tree-sitter grammar exposes the
351                        // arguments either via a named "arguments" field
352                        // or as the second positional child depending on
353                        // grammar version. Try field first, fall back to
354                        // child(1) — and accept either an `arguments`
355                        // wrapper or a bare `call`/`identifier`.
356                        let args_node = node
357                            .child_by_field_name("arguments")
358                            .or_else(|| node.child(1));
359                        if let Some(args) = args_node {
360                            // If args is the `arguments` wrapper, peel one
361                            // level. Otherwise `args` itself is the first
362                            // argument node (call / identifier / alias).
363                            let first_arg = if args.kind() == "arguments" {
364                                args.child(0)
365                            } else {
366                                Some(args)
367                            };
368                            if let Some(first_arg) = first_arg {
369                                // For def/defp, the first arg may be a call (name + params)
370                                if first_arg.kind() == "call" {
371                                    if let Some(fn_name) = first_arg.child(0) {
372                                        return Some(node_text(fn_name, source).to_string());
373                                    }
374                                }
375                                // For def with a guard: `def fn(x) when guard`,
376                                // the first arg is a `binary_operator` whose
377                                // left side is the call we want.
378                                if first_arg.kind() == "binary_operator" {
379                                    let mut bin_cursor = first_arg.walk();
380                                    for bin_child in first_arg.children(&mut bin_cursor) {
381                                        if bin_child.kind() == "call" {
382                                            if let Some(fname) = bin_child.child(0) {
383                                                return Some(
384                                                    node_text(fname, source).to_string(),
385                                                );
386                                            }
387                                        }
388                                    }
389                                }
390                                return Some(node_text(first_arg, source).to_string());
391                            }
392                        }
393                    }
394                }
395            }
396        }
397        Language::Ocaml => {
398            // OCaml `value_definition` wraps one or more `let_binding` children.
399            // The function name lives on `let_binding.pattern` (a `value_name`).
400            // BUG-AGG-8 (P11): the interface extractor was walking
401            // `value_definition` directly and querying `child_by_field_name("name")`
402            // which doesn't exist for OCaml — leaving every name empty.
403            if node.kind() == "value_definition" {
404                let mut cursor = node.walk();
405                for child in node.children(&mut cursor) {
406                    if child.kind() == "let_binding" {
407                        if let Some(pat) = child.child_by_field_name("pattern") {
408                            return Some(node_text(pat, source).to_string());
409                        }
410                    }
411                }
412            }
413            if node.kind() == "let_binding" {
414                if let Some(pat) = node.child_by_field_name("pattern") {
415                    return Some(node_text(pat, source).to_string());
416                }
417            }
418            // language-adapters-completeness-v1 (BUG-AGG12-9): the
419            // synthetic class node for an OCaml file is a
420            // `module_definition` (e.g. `module Make (V) = struct ... end`
421            // in dune's dag.ml). The grammar does not expose a `name`
422            // field on `module_definition`; the name lives on the
423            // first `module_name` child of the inner `module_binding`.
424            // P11's BUG-AGG-8 fix only addressed the function-level
425            // extractor (`value_definition` / `let_binding`); module
426            // wrappers remained nameless, surfacing as empty strings in
427            // every interface report for files that wrap their content
428            // in a functor or named module.
429            if node.kind() == "module_definition" {
430                let mut cursor = node.walk();
431                for child in node.children(&mut cursor) {
432                    if child.kind() == "module_binding" {
433                        let mut bind_cursor = child.walk();
434                        for bind_child in child.children(&mut bind_cursor) {
435                            if bind_child.kind() == "module_name" {
436                                return Some(node_text(bind_child, source).to_string());
437                            }
438                        }
439                    }
440                }
441            }
442            // `type t = { ... }` and similar — the type name is the
443            // first `type_constructor` (or fallback identifier) child.
444            if node.kind() == "type_definition" {
445                let mut cursor = node.walk();
446                for child in node.children(&mut cursor) {
447                    if child.kind() == "type_binding" {
448                        let mut bind_cursor = child.walk();
449                        for bind_child in child.children(&mut bind_cursor) {
450                            if matches!(
451                                bind_child.kind(),
452                                "type_constructor" | "type_constructor_path"
453                            ) {
454                                return Some(node_text(bind_child, source).to_string());
455                            }
456                        }
457                    }
458                    if matches!(child.kind(), "type_constructor" | "type_constructor_path") {
459                        return Some(node_text(child, source).to_string());
460                    }
461                }
462            }
463        }
464        Language::Lua | Language::Luau => {
465            // Try the "name" field first (already done above), then check child nodes
466            for i in 0..node.child_count() {
467                if let Some(child) = node.child(i) {
468                    if child.kind() == "identifier" || child.kind() == "dot_index_expression" {
469                        return Some(node_text(child, source).to_string());
470                    }
471                }
472            }
473        }
474        Language::Ruby => {
475            // Ruby methods: first identifier child after "def"
476            for i in 0..node.child_count() {
477                if let Some(child) = node.child(i) {
478                    if child.kind() == "identifier" || child.kind() == "constant" {
479                        return Some(node_text(child, source).to_string());
480                    }
481                }
482            }
483        }
484        _ => {}
485    }
486
487    None
488}
489
490/// Extract name from a C/C++ declarator (which may be nested).
491fn extract_c_declarator_name(declarator: Node, source: &[u8]) -> Option<String> {
492    // The declarator could be a function_declarator wrapping an identifier
493    if declarator.kind() == "identifier" {
494        return Some(node_text(declarator, source).to_string());
495    }
496    if declarator.kind() == "field_identifier" {
497        return Some(node_text(declarator, source).to_string());
498    }
499    // function_declarator has a "declarator" field that is the name
500    if let Some(inner) = declarator.child_by_field_name("declarator") {
501        return extract_c_declarator_name(inner, source);
502    }
503    // Try first child
504    if let Some(first) = declarator.child(0) {
505        if first.kind() == "identifier" || first.kind() == "field_identifier" {
506            return Some(node_text(first, source).to_string());
507        }
508    }
509    None
510}
511
512// =============================================================================
513// __all__ Extraction (Python-specific)
514// =============================================================================
515
516/// Extract the contents of `__all__` if defined in the module (Python only).
517pub fn extract_all_exports(root: Node, source: &[u8]) -> Option<Vec<String>> {
518    let mut cursor = root.walk();
519
520    for child in root.children(&mut cursor) {
521        if child.kind() == "expression_statement" {
522            if let Some(assignment) = child.child(0) {
523                if assignment.kind() == "assignment" {
524                    if let Some(left) = assignment.child_by_field_name("left") {
525                        if left.kind() == "identifier" {
526                            let name = node_text(left, source);
527                            if name == "__all__" {
528                                if let Some(right) = assignment.child_by_field_name("right") {
529                                    return extract_list_strings(right, source);
530                                }
531                            }
532                        }
533                    }
534                }
535            }
536        }
537    }
538
539    None
540}
541
542/// Extract string elements from a list node.
543fn extract_list_strings(node: Node, source: &[u8]) -> Option<Vec<String>> {
544    if node.kind() != "list" {
545        return None;
546    }
547
548    let mut exports = Vec::new();
549    let mut cursor = node.walk();
550
551    for child in node.children(&mut cursor) {
552        if child.kind() == "string" {
553            let text = node_text(child, source);
554            let cleaned = text
555                .trim_start_matches(['"', '\''])
556                .trim_end_matches(['"', '\'']);
557            exports.push(cleaned.to_string());
558        }
559    }
560
561    if exports.is_empty() {
562        None
563    } else {
564        Some(exports)
565    }
566}
567
568// =============================================================================
569// Signature Extraction (Language-Aware)
570// =============================================================================
571
572/// Extract the function signature from a definition node.
573///
574/// For Python, reconstructs from parameter nodes.
575/// For other languages, extracts the raw text of parameters and return type.
576pub fn extract_function_signature(func_node: Node, source: &[u8], lang: Language) -> String {
577    match lang {
578        Language::Python => extract_python_signature(func_node, source),
579        Language::Rust => extract_rust_signature(func_node, source),
580        Language::Go => extract_go_signature(func_node, source),
581        Language::Java | Language::CSharp => extract_java_like_signature(func_node, source),
582        Language::TypeScript | Language::JavaScript => extract_ts_signature(func_node, source),
583        Language::C | Language::Cpp => extract_c_signature(func_node, source),
584        Language::Ruby => extract_ruby_signature(func_node, source),
585        Language::Php => extract_php_signature(func_node, source),
586        Language::Scala => extract_scala_signature(func_node, source),
587        Language::Ocaml => extract_ocaml_signature(func_node, source),
588        Language::Elixir => extract_elixir_signature(func_node, source),
589        _ => extract_generic_signature(func_node, source),
590    }
591}
592
593/// OCaml signature: walk the `let_binding` parameters and optional return type.
594///
595/// BUG-AGG-8 (P11): without an OCaml-specific signature extractor the
596/// generic fallback (`child_by_field_name("parameters")`) returns nothing,
597/// so signatures are empty strings even when names are present.
598fn extract_ocaml_signature(func_node: Node, source: &[u8]) -> String {
599    // Find the inner let_binding if we were handed a value_definition.
600    let binding_owned;
601    let binding = if func_node.kind() == "value_definition" {
602        let mut found: Option<Node> = None;
603        let mut cursor = func_node.walk();
604        for child in func_node.children(&mut cursor) {
605            if child.kind() == "let_binding" {
606                found = Some(child);
607                break;
608            }
609        }
610        match found {
611            Some(b) => {
612                binding_owned = b;
613                binding_owned
614            }
615            None => return String::new(),
616        }
617    } else {
618        func_node
619    };
620
621    let mut params = Vec::new();
622    let mut cursor = binding.walk();
623    for child in binding.children(&mut cursor) {
624        if child.kind() == "parameter" {
625            // Parameter may have a "pattern" field with value_pattern /
626            // typed_pattern / unit / tuple_pattern, or fall back to the
627            // raw text.
628            if let Some(pattern) = child.child_by_field_name("pattern") {
629                let text = node_text(pattern, source).trim();
630                if !text.is_empty() {
631                    params.push(text.to_string());
632                    continue;
633                }
634            }
635            // Fallback: walk children for value_pattern / value_name.
636            let mut inner_cursor = child.walk();
637            let mut handled = false;
638            for inner in child.children(&mut inner_cursor) {
639                if inner.kind() == "value_pattern" || inner.kind() == "value_name" {
640                    params.push(node_text(inner, source).to_string());
641                    handled = true;
642                    break;
643                }
644            }
645            if !handled {
646                let text = node_text(child, source).trim();
647                if !text.is_empty() {
648                    params.push(text.to_string());
649                }
650            }
651        }
652    }
653
654    let mut sig = format!("({})", params.join(", "));
655
656    // Optional return type: `: type` between the last parameter and `=`.
657    let return_type = extract_ocaml_signature_return_type(binding, source);
658    if let Some(ret) = return_type {
659        sig.push_str(" : ");
660        sig.push_str(&ret);
661    }
662
663    sig
664}
665
666fn extract_ocaml_signature_return_type(binding: Node, source: &[u8]) -> Option<String> {
667    let mut last_was_colon = false;
668    let mut past_all_params = false;
669    let mut cursor = binding.walk();
670    for child in binding.children(&mut cursor) {
671        let kind = child.kind();
672        if kind == "parameter" {
673            past_all_params = false;
674            last_was_colon = false;
675            continue;
676        }
677        if kind != "parameter" && !past_all_params {
678            past_all_params = true;
679        }
680        if past_all_params && kind == ":" {
681            last_was_colon = true;
682            continue;
683        }
684        if last_was_colon && kind == "=" {
685            return None;
686        }
687        if last_was_colon && kind != "=" {
688            let t = node_text(child, source).trim().to_string();
689            if !t.is_empty() {
690                return Some(t);
691            }
692            last_was_colon = false;
693        }
694        if kind == "=" {
695            break;
696        }
697    }
698    None
699}
700
701/// Elixir signature: extract the parameter list of a `def`/`defp`/`defmacro`
702/// call node. BUG-AGG-9 (P11): without an Elixir-specific signature
703/// extractor, `tldr interface` would emit empty signatures for Elixir
704/// modules even after wiring up name extraction.
705fn extract_elixir_signature(func_node: Node, source: &[u8]) -> String {
706    if func_node.kind() != "call" {
707        return String::new();
708    }
709    // Structure: (call (identifier "def") (arguments (call (identifier "name") (arguments ...))))
710    let args_node = match func_node
711        .child_by_field_name("arguments")
712        .or_else(|| func_node.child(1))
713    {
714        Some(a) => a,
715        None => return String::new(),
716    };
717    let first_arg = if args_node.kind() == "arguments" {
718        match args_node.child(0) {
719            Some(a) => a,
720            None => return String::new(),
721        }
722    } else {
723        args_node
724    };
725
726    // Identifier-only def (no params): `def foo do ... end`
727    if first_arg.kind() == "identifier" {
728        return "()".to_string();
729    }
730
731    // call form: `def foo(a, b)` -> first_arg is a call(name, arguments)
732    let inner_call = match first_arg.kind() {
733        "call" => first_arg,
734        "binary_operator" => {
735            // `def foo(...) when guard` — find the inner call.
736            let mut found: Option<Node> = None;
737            let mut cursor = first_arg.walk();
738            for c in first_arg.children(&mut cursor) {
739                if c.kind() == "call" {
740                    found = Some(c);
741                    break;
742                }
743            }
744            match found {
745                Some(c) => c,
746                None => return String::new(),
747            }
748        }
749        _ => return String::new(),
750    };
751
752    // The call's second child is its `arguments` block. The arguments
753    // text is the raw source slice — for `def foo(a, b)` that's `(a, b)`,
754    // so just emit it verbatim. When it's a bareword call (no parens) the
755    // text won't have surrounding parens; wrap it in that case.
756    if let Some(call_args) = inner_call.child(1) {
757        if call_args.kind() == "arguments" {
758            let raw = node_text(call_args, source).trim();
759            if raw.starts_with('(') && raw.ends_with(')') {
760                return raw.to_string();
761            }
762            return format!("({})", raw);
763        }
764    }
765    "()".to_string()
766}
767
768/// Python signature: reconstruct from parameter nodes.
769fn extract_python_signature(func_node: Node, source: &[u8]) -> String {
770    let mut params = Vec::new();
771
772    if let Some(params_node) = func_node.child_by_field_name("parameters") {
773        let mut cursor = params_node.walk();
774
775        for child in params_node.children(&mut cursor) {
776            match child.kind() {
777                "identifier" => {
778                    params.push(node_text(child, source).to_string());
779                }
780                "typed_parameter" => {
781                    params.push(extract_typed_parameter(child, source));
782                }
783                "default_parameter" => {
784                    params.push(extract_default_parameter(child, source));
785                }
786                "typed_default_parameter" => {
787                    params.push(extract_typed_default_parameter(child, source));
788                }
789                "list_splat_pattern" | "dictionary_splat_pattern" => {
790                    params.push(node_text(child, source).to_string());
791                }
792                _ => {}
793            }
794        }
795    }
796
797    let params_str = params.join(", ");
798    let mut signature = format!("({})", params_str);
799
800    if let Some(return_type) = func_node.child_by_field_name("return_type") {
801        let return_text = node_text(return_type, source);
802        signature.push_str(" -> ");
803        signature.push_str(return_text);
804    }
805
806    signature
807}
808
809/// Rust signature: extract parameters and return type.
810fn extract_rust_signature(func_node: Node, source: &[u8]) -> String {
811    let mut sig = String::new();
812
813    if let Some(params) = func_node.child_by_field_name("parameters") {
814        sig.push_str(node_text(params, source));
815    }
816
817    if let Some(ret) = func_node.child_by_field_name("return_type") {
818        sig.push_str(" -> ");
819        sig.push_str(node_text(ret, source));
820    }
821
822    sig
823}
824
825/// Go signature: extract parameters and return type.
826fn extract_go_signature(func_node: Node, source: &[u8]) -> String {
827    let mut sig = String::new();
828
829    if let Some(params) = func_node.child_by_field_name("parameters") {
830        sig.push_str(node_text(params, source));
831    }
832
833    if let Some(result) = func_node.child_by_field_name("result") {
834        sig.push(' ');
835        sig.push_str(node_text(result, source));
836    }
837
838    sig
839}
840
841/// Java/C# signature: extract parameters from formal_parameters.
842fn extract_java_like_signature(func_node: Node, source: &[u8]) -> String {
843    let mut sig = String::new();
844
845    // Try "parameters" field first, then look for "formal_parameters" or a parameter_list child
846    let params_node = func_node.child_by_field_name("parameters").or_else(|| {
847        // Search for formal_parameters or parameter_list node among children
848        let mut cursor = func_node.walk();
849        let found = func_node
850            .children(&mut cursor)
851            .find(|&child| child.kind() == "formal_parameters" || child.kind() == "parameter_list");
852        found
853    });
854
855    if let Some(params) = params_node {
856        sig.push_str(node_text(params, source));
857    }
858
859    // For Java, check for return type (it's the "type" field)
860    if let Some(ret) = func_node.child_by_field_name("type") {
861        // Prepend return type
862        let ret_text = node_text(ret, source);
863        sig = format!("{}: {}", sig, ret_text);
864    }
865
866    sig
867}
868
869/// TypeScript/JavaScript signature.
870fn extract_ts_signature(func_node: Node, source: &[u8]) -> String {
871    let mut sig = String::new();
872
873    if let Some(params) = func_node.child_by_field_name("parameters") {
874        sig.push_str(node_text(params, source));
875    }
876
877    if let Some(ret) = func_node.child_by_field_name("return_type") {
878        sig.push_str(": ");
879        sig.push_str(node_text(ret, source));
880    }
881
882    sig
883}
884
885/// C/C++ signature: extract from declarator.
886fn extract_c_signature(func_node: Node, source: &[u8]) -> String {
887    let mut sig = String::new();
888
889    if let Some(declarator) = func_node.child_by_field_name("declarator") {
890        // The declarator includes function name and parameter list
891        // We want just the parameters portion
892        if let Some(params) = declarator.child_by_field_name("parameters") {
893            sig.push_str(node_text(params, source));
894        }
895    }
896
897    // Return type is typically the first child (type specifier)
898    if let Some(type_node) = func_node.child_by_field_name("type") {
899        let type_text = node_text(type_node, source);
900        if !type_text.is_empty() {
901            sig = format!("{}: {}", sig, type_text);
902        }
903    }
904
905    sig
906}
907
908/// Ruby signature.
909fn extract_ruby_signature(func_node: Node, source: &[u8]) -> String {
910    if let Some(params) = func_node.child_by_field_name("parameters") {
911        node_text(params, source).to_string()
912    } else {
913        // Check for method_parameters child
914        let mut cursor = func_node.walk();
915        for child in func_node.children(&mut cursor) {
916            if child.kind() == "method_parameters" {
917                return node_text(child, source).to_string();
918            }
919        }
920        "()".to_string()
921    }
922}
923
924/// PHP signature.
925fn extract_php_signature(func_node: Node, source: &[u8]) -> String {
926    let mut sig = String::new();
927
928    if let Some(params) = func_node.child_by_field_name("parameters") {
929        sig.push_str(node_text(params, source));
930    }
931
932    if let Some(ret) = func_node.child_by_field_name("return_type") {
933        sig.push_str(": ");
934        sig.push_str(node_text(ret, source));
935    }
936
937    sig
938}
939
940/// Scala signature.
941fn extract_scala_signature(func_node: Node, source: &[u8]) -> String {
942    let mut sig = String::new();
943
944    if let Some(params) = func_node.child_by_field_name("parameters") {
945        sig.push_str(node_text(params, source));
946    }
947
948    if let Some(ret) = func_node.child_by_field_name("return_type") {
949        sig.push_str(": ");
950        sig.push_str(node_text(ret, source));
951    }
952
953    sig
954}
955
956/// Generic signature: try to find parameters/return type fields.
957fn extract_generic_signature(func_node: Node, source: &[u8]) -> String {
958    let mut sig = String::new();
959
960    if let Some(params) = func_node.child_by_field_name("parameters") {
961        sig.push_str(node_text(params, source));
962    }
963
964    sig
965}
966
967/// Extract a typed parameter (name: type) - Python-specific.
968fn extract_typed_parameter(node: Node, source: &[u8]) -> String {
969    let name = node
970        .child(0)
971        .filter(|c| c.kind() == "identifier")
972        .map(|n| node_text(n, source))
973        .unwrap_or("");
974    let type_hint = node
975        .child_by_field_name("type")
976        .map(|n| node_text(n, source))
977        .unwrap_or("");
978
979    if type_hint.is_empty() {
980        name.to_string()
981    } else {
982        format!("{}: {}", name, type_hint)
983    }
984}
985
986/// Extract a default parameter (name=default) - Python-specific.
987fn extract_default_parameter(node: Node, source: &[u8]) -> String {
988    let name = node
989        .child_by_field_name("name")
990        .map(|n| node_text(n, source))
991        .unwrap_or("");
992    let value = node
993        .child_by_field_name("value")
994        .map(|n| node_text(n, source))
995        .unwrap_or("");
996
997    format!("{} = {}", name, value)
998}
999
1000/// Extract a typed default parameter (name: type = default) - Python-specific.
1001fn extract_typed_default_parameter(node: Node, source: &[u8]) -> String {
1002    let name = node
1003        .child_by_field_name("name")
1004        .map(|n| node_text(n, source))
1005        .unwrap_or("");
1006    let type_hint = node
1007        .child_by_field_name("type")
1008        .map(|n| node_text(n, source))
1009        .unwrap_or("");
1010    let value = node
1011        .child_by_field_name("value")
1012        .map(|n| node_text(n, source))
1013        .unwrap_or("");
1014
1015    if type_hint.is_empty() {
1016        format!("{} = {}", name, value)
1017    } else {
1018        format!("{}: {} = {}", name, type_hint, value)
1019    }
1020}
1021
1022// =============================================================================
1023// Function Info Extraction (Language-Aware)
1024// =============================================================================
1025
1026/// Extract function information from a function definition node.
1027pub fn extract_function_info(func_node: Node, source: &[u8], lang: Language) -> FunctionInfo {
1028    let name = get_node_name(func_node, source, lang).unwrap_or_default();
1029    let signature = extract_function_signature(func_node, source, lang);
1030    let lineno = func_node.start_position().row as u32 + 1;
1031    let is_async = detect_async(func_node, source, lang);
1032    let docstring = extract_docstring(func_node, source, lang);
1033
1034    FunctionInfo {
1035        name,
1036        signature,
1037        docstring,
1038        lineno,
1039        is_async,
1040    }
1041}
1042
1043/// Detect if a function is async.
1044fn detect_async(func_node: Node, source: &[u8], lang: Language) -> bool {
1045    match lang {
1046        Language::Python => {
1047            let func_text = node_text(func_node, source);
1048            func_text.starts_with("async ")
1049        }
1050        Language::Rust => {
1051            // Check for "async" keyword child
1052            for i in 0..func_node.child_count() {
1053                if let Some(child) = func_node.child(i) {
1054                    if node_text(child, source) == "async" {
1055                        return true;
1056                    }
1057                }
1058            }
1059            false
1060        }
1061        Language::TypeScript | Language::JavaScript => {
1062            // Check for async keyword
1063            let func_text = node_text(func_node, source);
1064            func_text.starts_with("async ")
1065        }
1066        Language::CSharp => {
1067            // Check modifiers for "async"
1068            if let Some(modifiers) = func_node.child_by_field_name("modifiers") {
1069                return node_text(modifiers, source).contains("async");
1070            }
1071            false
1072        }
1073        Language::Elixir => {
1074            // Elixir doesn't have async keyword in the traditional sense
1075            false
1076        }
1077        _ => false,
1078    }
1079}
1080
1081// =============================================================================
1082// Docstring / Doc Comment Extraction (Language-Aware)
1083// =============================================================================
1084
1085/// Extract docstring or doc comment from a function or class node.
1086fn extract_docstring(node: Node, source: &[u8], lang: Language) -> Option<String> {
1087    match lang {
1088        Language::Python => extract_python_docstring(node, source),
1089        Language::Rust => extract_rust_doc_comment(node, source),
1090        Language::Go => extract_go_doc_comment(node, source),
1091        Language::Java | Language::CSharp | Language::Scala | Language::Php => {
1092            extract_javadoc_comment(node, source)
1093        }
1094        Language::TypeScript | Language::JavaScript => extract_jsdoc_comment(node, source),
1095        Language::Ruby => extract_ruby_comment(node, source),
1096        Language::Elixir => extract_elixir_doc(node, source),
1097        _ => None,
1098    }
1099}
1100
1101/// Python docstring: first string in function/class body.
1102fn extract_python_docstring(node: Node, source: &[u8]) -> Option<String> {
1103    if let Some(body) = node.child_by_field_name("body") {
1104        let mut cursor = body.walk();
1105        let first_stmt = body.children(&mut cursor).next();
1106        if let Some(child) = first_stmt {
1107            if child.kind() == "expression_statement" {
1108                if let Some(expr) = child.child(0) {
1109                    if expr.kind() == "string" {
1110                        let text = node_text(expr, source);
1111                        let cleaned = text
1112                            .trim_start_matches("\"\"\"")
1113                            .trim_start_matches("'''")
1114                            .trim_end_matches("\"\"\"")
1115                            .trim_end_matches("'''")
1116                            .trim();
1117                        return Some(cleaned.to_string());
1118                    }
1119                }
1120            }
1121        }
1122    }
1123    None
1124}
1125
1126/// Rust doc comments: /// or //! preceding the node.
1127fn extract_rust_doc_comment(node: Node, source: &[u8]) -> Option<String> {
1128    let mut comments = Vec::new();
1129    let mut prev = node.prev_sibling();
1130
1131    while let Some(sib) = prev {
1132        let kind = sib.kind();
1133        if kind == "line_comment" {
1134            let text = node_text(sib, source);
1135            if text.starts_with("///") || text.starts_with("//!") {
1136                let content = text
1137                    .trim_start_matches("///")
1138                    .trim_start_matches("//!")
1139                    .trim();
1140                comments.push(content.to_string());
1141            } else {
1142                break;
1143            }
1144        } else if kind == "attribute_item" {
1145            // Skip attributes between doc comments
1146        } else {
1147            break;
1148        }
1149        prev = sib.prev_sibling();
1150    }
1151
1152    if comments.is_empty() {
1153        None
1154    } else {
1155        comments.reverse();
1156        Some(comments.join("\n"))
1157    }
1158}
1159
1160/// Go doc comment: preceding line comment block.
1161fn extract_go_doc_comment(node: Node, source: &[u8]) -> Option<String> {
1162    let mut comments = Vec::new();
1163    let mut prev = node.prev_sibling();
1164
1165    while let Some(sib) = prev {
1166        if sib.kind() == "comment" {
1167            let text = node_text(sib, source);
1168            let content = text.trim_start_matches("//").trim();
1169            comments.push(content.to_string());
1170        } else {
1171            break;
1172        }
1173        prev = sib.prev_sibling();
1174    }
1175
1176    if comments.is_empty() {
1177        None
1178    } else {
1179        comments.reverse();
1180        Some(comments.join("\n"))
1181    }
1182}
1183
1184/// Javadoc-style: /** ... */ preceding the node.
1185fn extract_javadoc_comment(node: Node, source: &[u8]) -> Option<String> {
1186    let mut prev = node.prev_sibling();
1187
1188    while let Some(sib) = prev {
1189        let kind = sib.kind();
1190        if kind == "block_comment" || kind == "comment" || kind == "multiline_comment" {
1191            let text = node_text(sib, source);
1192            if text.starts_with("/**") {
1193                let cleaned = text
1194                    .trim_start_matches("/**")
1195                    .trim_end_matches("*/")
1196                    .lines()
1197                    .map(|l| l.trim().trim_start_matches('*').trim())
1198                    .filter(|l| !l.is_empty())
1199                    .collect::<Vec<_>>()
1200                    .join("\n");
1201                return Some(cleaned);
1202            }
1203        } else if kind == "annotation" || kind == "marker_annotation" || kind == "attribute_list" {
1204            // Skip annotations/attributes
1205        } else {
1206            break;
1207        }
1208        prev = sib.prev_sibling();
1209    }
1210    None
1211}
1212
1213/// JSDoc: /** ... */ preceding the node.
1214fn extract_jsdoc_comment(node: Node, source: &[u8]) -> Option<String> {
1215    extract_javadoc_comment(node, source)
1216}
1217
1218/// Ruby: # comments preceding the node.
1219fn extract_ruby_comment(node: Node, source: &[u8]) -> Option<String> {
1220    let mut comments = Vec::new();
1221    let mut prev = node.prev_sibling();
1222
1223    while let Some(sib) = prev {
1224        if sib.kind() == "comment" {
1225            let text = node_text(sib, source);
1226            let content = text.trim_start_matches('#').trim();
1227            comments.push(content.to_string());
1228        } else {
1229            break;
1230        }
1231        prev = sib.prev_sibling();
1232    }
1233
1234    if comments.is_empty() {
1235        None
1236    } else {
1237        comments.reverse();
1238        Some(comments.join("\n"))
1239    }
1240}
1241
1242/// Elixir: @doc or @moduledoc preceding the node.
1243fn extract_elixir_doc(node: Node, source: &[u8]) -> Option<String> {
1244    let mut prev = node.prev_sibling();
1245
1246    while let Some(sib) = prev {
1247        if sib.kind() == "unary_operator" || sib.kind() == "call" {
1248            let text = node_text(sib, source);
1249            if text.starts_with("@doc") || text.starts_with("@moduledoc") {
1250                // Extract the string content
1251                let cleaned = text
1252                    .trim_start_matches("@moduledoc")
1253                    .trim_start_matches("@doc")
1254                    .trim()
1255                    .trim_start_matches("\"\"\"")
1256                    .trim_end_matches("\"\"\"")
1257                    .trim_start_matches('"')
1258                    .trim_end_matches('"')
1259                    .trim();
1260                if !cleaned.is_empty() {
1261                    return Some(cleaned.to_string());
1262                }
1263            }
1264        } else if sib.kind() == "comment" {
1265            // skip
1266        } else {
1267            break;
1268        }
1269        prev = sib.prev_sibling();
1270    }
1271    None
1272}
1273
1274// =============================================================================
1275// Class Info Extraction (Language-Aware)
1276// =============================================================================
1277
1278/// Extract class/struct/trait information from a definition node.
1279pub fn extract_class_info(class_node: Node, source: &[u8], lang: Language) -> ClassInfo {
1280    let name = get_node_name(class_node, source, lang).unwrap_or_default();
1281    let lineno = class_node.start_position().row as u32 + 1;
1282
1283    // Extract base classes / implemented interfaces
1284    let bases = extract_base_classes(class_node, source, lang);
1285
1286    // Extract methods
1287    let mut methods = Vec::new();
1288    let mut private_method_count = 0u32;
1289
1290    let method_kinds = method_node_kinds(lang);
1291    let body_node = find_body_node(class_node, lang);
1292
1293    if let Some(body) = body_node {
1294        collect_methods_from_body(
1295            body,
1296            source,
1297            lang,
1298            method_kinds,
1299            &mut methods,
1300            &mut private_method_count,
1301        );
1302    }
1303
1304    ClassInfo {
1305        name,
1306        lineno,
1307        bases,
1308        methods,
1309        private_method_count,
1310    }
1311}
1312
1313/// Find the body/block node of a class/struct definition.
1314fn find_body_node<'a>(class_node: Node<'a>, lang: Language) -> Option<Node<'a>> {
1315    // Try common field names
1316    if let Some(body) = class_node.child_by_field_name("body") {
1317        return Some(body);
1318    }
1319    if let Some(body) = class_node.child_by_field_name("members") {
1320        return Some(body);
1321    }
1322
1323    match lang {
1324        Language::Rust => {
1325            // For impl_item, look for declaration_list
1326            let mut cursor = class_node.walk();
1327            for child in class_node.children(&mut cursor) {
1328                if child.kind() == "declaration_list" {
1329                    return Some(child);
1330                }
1331            }
1332            None
1333        }
1334        Language::Java | Language::CSharp => {
1335            // class_body or interface_body
1336            let mut cursor = class_node.walk();
1337            for child in class_node.children(&mut cursor) {
1338                if child.kind() == "class_body"
1339                    || child.kind() == "interface_body"
1340                    || child.kind() == "enum_body"
1341                    || child.kind() == "declaration_list"
1342                {
1343                    return Some(child);
1344                }
1345            }
1346            None
1347        }
1348        Language::TypeScript | Language::JavaScript => {
1349            let mut cursor = class_node.walk();
1350            for child in class_node.children(&mut cursor) {
1351                if child.kind() == "class_body" {
1352                    return Some(child);
1353                }
1354            }
1355            None
1356        }
1357        Language::Cpp => {
1358            let mut cursor = class_node.walk();
1359            for child in class_node.children(&mut cursor) {
1360                if child.kind() == "field_declaration_list" {
1361                    return Some(child);
1362                }
1363            }
1364            None
1365        }
1366        Language::Ruby => {
1367            // Ruby class body is inside a body_statement child
1368            let mut cursor = class_node.walk();
1369            for child in class_node.children(&mut cursor) {
1370                if child.kind() == "body_statement" {
1371                    return Some(child);
1372                }
1373            }
1374            // Fallback: use the class node itself
1375            Some(class_node)
1376        }
1377        _ => {
1378            // Default: try common body kinds
1379            let mut cursor = class_node.walk();
1380            for child in class_node.children(&mut cursor) {
1381                let kind = child.kind();
1382                if kind.contains("body")
1383                    || kind.contains("block")
1384                    || kind == "declaration_list"
1385                    || kind == "template_body"
1386                {
1387                    return Some(child);
1388                }
1389            }
1390            None
1391        }
1392    }
1393}
1394
1395/// Collect methods from a class body node.
1396fn collect_methods_from_body(
1397    body: Node,
1398    source: &[u8],
1399    lang: Language,
1400    method_kinds: &[&str],
1401    methods: &mut Vec<MethodInfo>,
1402    private_count: &mut u32,
1403) {
1404    let mut cursor = body.walk();
1405    let decorator_kinds = decorator_node_kinds(lang);
1406
1407    for child in body.children(&mut cursor) {
1408        let kind = child.kind();
1409
1410        if method_kinds.contains(&kind) {
1411            let method_name = get_node_name(child, source, lang).unwrap_or_default();
1412            if is_method_public(&method_name, child, source, lang) {
1413                methods.push(extract_method_info(child, source, lang));
1414            } else {
1415                *private_count += 1;
1416            }
1417        } else if decorator_kinds.contains(&kind) {
1418            // Handle decorated methods (Python, Java annotations, etc.)
1419            if let Some(def) = find_definition_in_decorated(child, method_kinds) {
1420                let method_name = get_node_name(def, source, lang).unwrap_or_default();
1421                if is_method_public(&method_name, def, source, lang) {
1422                    methods.push(extract_method_info(def, source, lang));
1423                } else {
1424                    *private_count += 1;
1425                }
1426            }
1427        }
1428    }
1429}
1430
1431/// Check if a method is public based on language conventions.
1432fn is_method_public(name: &str, node: Node, source: &[u8], lang: Language) -> bool {
1433    match lang {
1434        Language::Python | Language::Ruby | Language::Lua | Language::Luau => {
1435            is_public_for_lang(name, lang)
1436        }
1437        Language::Rust => is_rust_pub(node, source),
1438        Language::Go => name.chars().next().is_some_and(|c| c.is_uppercase()),
1439        Language::Java | Language::CSharp => has_public_modifier(node, source),
1440        _ => true,
1441    }
1442}
1443
1444/// Extract base classes / superclasses / implemented interfaces.
1445fn extract_base_classes(class_node: Node, source: &[u8], lang: Language) -> Vec<String> {
1446    let mut bases = Vec::new();
1447
1448    match lang {
1449        Language::Python => {
1450            if let Some(superclasses) = class_node.child_by_field_name("superclasses") {
1451                let mut cursor = superclasses.walk();
1452                for child in superclasses.children(&mut cursor) {
1453                    if child.kind() == "identifier" || child.kind() == "attribute" {
1454                        bases.push(node_text(child, source).to_string());
1455                    }
1456                }
1457            }
1458        }
1459        Language::Java | Language::CSharp => {
1460            // Check for "superclass" and "interfaces" fields
1461            if let Some(super_node) = class_node.child_by_field_name("superclass") {
1462                bases.push(node_text(super_node, source).to_string());
1463            }
1464            if let Some(interfaces) = class_node.child_by_field_name("interfaces") {
1465                let mut cursor = interfaces.walk();
1466                for child in interfaces.children(&mut cursor) {
1467                    if child.kind() == "type_identifier" || child.kind() == "generic_type" {
1468                        bases.push(node_text(child, source).to_string());
1469                    }
1470                }
1471            }
1472            // Also check super_interfaces for Java interface declarations
1473            if let Some(extends) = class_node.child_by_field_name("type_parameters") {
1474                // type parameters are not bases, skip
1475                let _ = extends;
1476            }
1477        }
1478        Language::Rust => {
1479            // For trait_item, look for trait bounds
1480            // For impl_item, look for the trait being implemented
1481            if class_node.kind() == "impl_item" {
1482                if let Some(trait_node) = class_node.child_by_field_name("trait") {
1483                    bases.push(node_text(trait_node, source).to_string());
1484                }
1485            }
1486        }
1487        Language::TypeScript | Language::JavaScript => {
1488            // Check for extends_clause or implements_clause
1489            let mut cursor = class_node.walk();
1490            for child in class_node.children(&mut cursor) {
1491                if child.kind() == "class_heritage" {
1492                    let mut inner_cursor = child.walk();
1493                    for clause in child.children(&mut inner_cursor) {
1494                        if clause.kind() == "extends_clause" || clause.kind() == "implements_clause"
1495                        {
1496                            let mut type_cursor = clause.walk();
1497                            for type_child in clause.children(&mut type_cursor) {
1498                                if type_child.kind() == "identifier"
1499                                    || type_child.kind() == "type_identifier"
1500                                {
1501                                    bases.push(node_text(type_child, source).to_string());
1502                                }
1503                            }
1504                        }
1505                    }
1506                }
1507            }
1508        }
1509        Language::Ruby => {
1510            if let Some(super_node) = class_node.child_by_field_name("superclass") {
1511                bases.push(node_text(super_node, source).to_string());
1512            }
1513        }
1514        Language::Go => {
1515            // Go type_declaration doesn't have base classes per se
1516            // But embedded structs could be found in struct fields
1517        }
1518        Language::Scala => {
1519            if let Some(extends) = class_node.child_by_field_name("extends") {
1520                bases.push(node_text(extends, source).to_string());
1521            }
1522        }
1523        _ => {}
1524    }
1525
1526    bases
1527}
1528
1529/// Find a function/class definition inside a decorated_definition node.
1530fn find_definition_in_decorated<'a>(node: Node<'a>, target_kinds: &[&str]) -> Option<Node<'a>> {
1531    let mut cursor = node.walk();
1532    let found = node
1533        .children(&mut cursor)
1534        .find(|&child| target_kinds.contains(&child.kind()));
1535    found
1536}
1537
1538/// Extract method information from a function definition node.
1539fn extract_method_info(func_node: Node, source: &[u8], lang: Language) -> MethodInfo {
1540    let name = get_node_name(func_node, source, lang).unwrap_or_default();
1541    let signature = extract_function_signature(func_node, source, lang);
1542    let is_async = detect_async(func_node, source, lang);
1543
1544    MethodInfo {
1545        name,
1546        signature,
1547        is_async,
1548    }
1549}
1550
1551// =============================================================================
1552// Interface Extraction (Language-Aware)
1553// =============================================================================
1554
1555/// Extract the public interface from a source file.
1556///
1557/// Detects the language from the file extension and uses the appropriate
1558/// tree-sitter grammar and node kinds. Uses sibling-aware detection so a
1559/// `.h` header next to `.cpp` translation units is parsed with the C++
1560/// grammar — without this, `tldr interface tinyxml2.h` parses as C and
1561/// returns zero classes (real-repo-fixes-v1 P9.BUG-R2).
1562pub fn extract_interface(path: &Path, source: &str) -> PatternsResult<InterfaceInfo> {
1563    let lang = Language::from_path_with_siblings(path).unwrap_or(Language::Python);
1564    extract_interface_with_lang(path, source, lang)
1565}
1566
1567/// Extract the public interface from a source file with an explicit language.
1568pub fn extract_interface_with_lang(
1569    path: &Path,
1570    source: &str,
1571    lang: Language,
1572) -> PatternsResult<InterfaceInfo> {
1573    let source_bytes = source.as_bytes();
1574
1575    // Parse with ParserPool (multi-language)
1576    let pool = ParserPool::new();
1577    let tree = pool
1578        .parse(source, lang)
1579        .map_err(|e| PatternsError::parse_error(path, format!("Failed to parse: {}", e)))?;
1580
1581    let root = tree.root_node();
1582
1583    // Extract __all__ exports (Python-specific)
1584    let explicit_all_exports = if lang == Language::Python {
1585        extract_all_exports(root, source_bytes)
1586    } else {
1587        None
1588    };
1589
1590    // Determine node kinds for this language
1591    let func_kinds = function_node_kinds(lang);
1592    let class_kinds = class_node_kinds(lang);
1593    let decorator_kinds = decorator_node_kinds(lang);
1594
1595    // Extract public functions and classes
1596    let (functions, classes) = collect_top_level_definitions(
1597        root,
1598        source_bytes,
1599        lang,
1600        func_kinds,
1601        class_kinds,
1602        decorator_kinds,
1603    );
1604
1605    // schema-cleanup-v1 BUG-22: populate `all_exports` as a non-null
1606    // array. Prefer the explicit `__all__` (Python only); otherwise
1607    // fall back to the union of public function and class names —
1608    // mirroring "import *" semantics. Empty modules → `[]`.
1609    let all_exports = if let Some(explicit) = explicit_all_exports {
1610        explicit
1611    } else {
1612        let mut names: Vec<String> = functions
1613            .iter()
1614            .map(|f| f.name.clone())
1615            .chain(classes.iter().map(|c| c.name.clone()))
1616            .collect();
1617        names.sort();
1618        names.dedup();
1619        names
1620    };
1621
1622    Ok(InterfaceInfo {
1623        file: path.display().to_string(),
1624        all_exports,
1625        functions,
1626        classes,
1627    })
1628}
1629
1630/// Container node kinds whose children should be treated as top-level for
1631/// the purpose of public-interface extraction.
1632///
1633/// Real-world repos commonly wrap top-level classes/functions in:
1634/// * C++: `namespace foo { ... }`, `extern "C" { ... }`, `#if/#elif` preproc
1635/// * C#: `namespace Foo { ... }` and `namespace Foo;` (file-scoped)
1636/// * C/C++: preproc conditional branches gating typedefs and inline functions
1637///
1638/// Without recursion, `tldr interface` reported zero classes for cpp/csharp
1639/// even though `tldr extract` listed them — real-repo-fixes-v1 (P9.BUG-R2/R5).
1640fn is_interface_container(kind: &str) -> bool {
1641    matches!(
1642        kind,
1643        "namespace_definition"
1644            | "namespace_declaration"
1645            | "file_scoped_namespace_declaration"
1646            | "linkage_specification"
1647            | "preproc_if"
1648            | "preproc_ifdef"
1649            | "preproc_else"
1650            | "preproc_elif"
1651            | "preproc_elifdef"
1652            | "declaration_list"
1653            // tree-sitter-cpp commonly produces ERROR / function_definition
1654            // wrappers in real-world headers (e.g. tinyxml2.h) when macro
1655            // names like `TINYXML2_LIB` confuse the parser. Recurse into
1656            // these so embedded class_specifier nodes still surface.
1657            | "ERROR"
1658            | "compound_statement"
1659            // C# wraps the whole file content under various namespace forms
1660            // and global_statement/file_scoped_namespace bodies.
1661            | "global_statement"
1662    )
1663}
1664
1665/// Languages where misparses are common enough that we should walk the full
1666/// AST looking for class/function nodes, not just direct children of root.
1667///
1668/// For these languages `tldr extract` already does a deep walk; matching that
1669/// behaviour for `tldr interface` keeps the two commands consistent.
1670/// real-repo-fixes-v1 (P9.BUG-R2/R5/R6/R7).
1671fn needs_deep_walk(lang: Language) -> bool {
1672    matches!(
1673        lang,
1674        Language::Cpp
1675            | Language::C
1676            | Language::CSharp
1677            | Language::Kotlin
1678            | Language::Swift
1679    )
1680}
1681
1682/// Collect top-level function and class definitions from the AST root.
1683///
1684/// Recurses one level into language-appropriate container nodes (PHP
1685/// declaration list; C++/C# namespaces; cpp preprocessor branches) so that
1686/// public types defined inside `namespace { ... }` or `#if ... #endif`
1687/// blocks are surfaced — without this, real cpp/csharp codebases report zero
1688/// classes (P9.BUG-R2/R5).
1689fn collect_top_level_definitions(
1690    root: Node,
1691    source: &[u8],
1692    lang: Language,
1693    func_kinds: &[&str],
1694    class_kinds: &[&str],
1695    decorator_kinds: &[&str],
1696) -> (Vec<FunctionInfo>, Vec<ClassInfo>) {
1697    let mut functions = Vec::new();
1698    let mut classes = Vec::new();
1699    if needs_deep_walk(lang) {
1700        // Walk the whole AST, collecting top-level (non-method) functions
1701        // and class-like nodes wherever they appear. Mirrors `tldr extract`'s
1702        // behaviour for languages where misparses or namespace wrapping are
1703        // common in real-world code.
1704        deep_collect(
1705            root,
1706            source,
1707            lang,
1708            func_kinds,
1709            class_kinds,
1710            &mut functions,
1711            &mut classes,
1712            0,
1713        );
1714    } else {
1715        visit_top_level(
1716            root,
1717            source,
1718            lang,
1719            func_kinds,
1720            class_kinds,
1721            decorator_kinds,
1722            &mut functions,
1723            &mut classes,
1724            0,
1725        );
1726    }
1727
1728    // language-specific-bugs-v1 (P14.AGG14-10): post-process Rust class
1729    // entries to merge `impl Foo { ... }` blocks into the corresponding
1730    // `struct Foo` / `enum Foo` / `trait Foo` entry. Without this, the
1731    // output contained both a `struct GlobSet` (methods=[]) AND an
1732    // `impl GlobSet` (whose methods were the actual API surface) — and
1733    // the user saw `methods: 0` on the struct.
1734    if matches!(lang, Language::Rust) {
1735        merge_rust_impl_entries(&mut classes);
1736    }
1737
1738    // language-specific-bugs-v1 (P14.AGG14-17): for Java (and the same
1739    // class-only languages where every public function lives inside a
1740    // class and the top-level `functions[]` would otherwise always be
1741    // empty), copy each public method into the top-level
1742    // `functions[]` array as a flat entry. Method entries stay inside
1743    // the class entry so consumers that index by class still work; the
1744    // flat `functions[]` array now matches the convention python /
1745    // typescript already follow (every callable a downstream consumer
1746    // could call is reachable without dereferencing a `classes[]`
1747    // entry first).
1748    if matches!(lang, Language::Java | Language::Kotlin) {
1749        flatten_class_methods_to_functions(&classes, &mut functions);
1750    }
1751
1752    (functions, classes)
1753}
1754
1755/// language-specific-bugs-v1 (P14.AGG14-17): flatten every public method
1756/// from `classes` into `functions` as a top-level entry, deduplicated by
1757/// `(name, lineno)`. Used for class-only languages (Java, Kotlin) so that
1758/// `tldr interface SomeController.java | jq '.functions | length'` is
1759/// non-zero whenever the file declares a class with public methods —
1760/// matching the contract Python / TypeScript already satisfy at the
1761/// schema level (every public callable is enumerable from `functions[]`
1762/// without dereferencing `classes[]`).
1763fn flatten_class_methods_to_functions(
1764    classes: &[ClassInfo],
1765    functions: &mut Vec<FunctionInfo>,
1766) {
1767    use std::collections::HashSet;
1768    let mut seen: HashSet<(String, u32)> = HashSet::new();
1769    for f in functions.iter() {
1770        seen.insert((f.name.clone(), f.lineno));
1771    }
1772    for class in classes {
1773        for method in &class.methods {
1774            // Method doesn't carry an own line in this schema (see
1775            // `types.rs::MethodInfo`); use `(name, class_line)` as the
1776            // dedup key, matching the lineno we'll attach below. Two
1777            // methods with the same name on the same class would
1778            // produce a key collision (overload/companion), but Java
1779            // forbids that and Kotlin permits it only when signatures
1780            // differ — picking one is the convention `tldr structure`
1781            // already follows.
1782            let key = (method.name.clone(), class.lineno);
1783            if seen.contains(&key) {
1784                continue;
1785            }
1786            seen.insert(key);
1787            // MethodInfo doesn't carry a `lineno` of its own — it
1788            // inherits visibility/positioning from the enclosing class
1789            // entry. For the flat `functions[]` view, attach the class's
1790            // line number as a stable proxy so callers can navigate to
1791            // the class declaration. Same convention `tldr extract` uses
1792            // for class methods exposed at the file level.
1793            functions.push(FunctionInfo {
1794                name: method.name.clone(),
1795                signature: method.signature.clone(),
1796                docstring: None,
1797                lineno: class.lineno,
1798                is_async: method.is_async,
1799            });
1800        }
1801    }
1802}
1803
1804/// language-specific-bugs-v1 (P14.AGG14-10): coalesce duplicate Rust class
1805/// entries. After the walker has gathered `struct`/`enum`/`trait` entries
1806/// AND every `impl <Type>` block as separate `ClassInfo`s (because both
1807/// node kinds are listed in `class_node_kinds(Language::Rust)`), this pass
1808/// finds each impl whose `name` matches an existing struct/enum/trait
1809/// entry and folds the impl's methods into the matching entry. impl
1810/// blocks with no struct/enum/trait counterpart in the same file (e.g.
1811/// `impl SomeTrait for ExternalType { ... }` where `ExternalType` lives
1812/// elsewhere) are dropped entirely — we cannot attach them to anything in
1813/// this file's interface and surfacing them with the trait/type name as a
1814/// "class" was misleading.
1815fn merge_rust_impl_entries(classes: &mut Vec<ClassInfo>) {
1816    use std::collections::HashSet;
1817
1818    // Step 1: index the lineno of every non-impl class entry so we keep
1819    // their stable ordering when re-inserting methods.
1820    let mut struct_like_indices: HashSet<String> = HashSet::new();
1821    for c in classes.iter() {
1822        // We treat any entry whose name doesn't carry generic / for-clause
1823        // syntax as struct-like. impl entries carry the impl'd type name
1824        // verbatim (which may include generics like `Foo<T>`), so we
1825        // strip generics on lookup keys.
1826        let key = strip_generics(&c.name);
1827        struct_like_indices.insert(key);
1828    }
1829    let _ = struct_like_indices; // (only used implicitly via the merge)
1830
1831    // Step 2: separate impl entries from struct/enum/trait entries by
1832    // lineno - we don't have a `kind` discriminator, so we re-scan: any
1833    // entry whose name appears more than once is an impl-block duplicate.
1834    let mut name_counts: std::collections::HashMap<String, usize> =
1835        std::collections::HashMap::new();
1836    for c in classes.iter() {
1837        *name_counts.entry(strip_generics(&c.name)).or_insert(0) += 1;
1838    }
1839
1840    // Step 3: walk classes in order. For each entry whose name is a
1841    // duplicate, fold its methods into the FIRST entry with the same
1842    // name (the canonical struct/enum/trait location). Mark folded
1843    // entries for removal.
1844    let mut canonical_index: std::collections::HashMap<String, usize> =
1845        std::collections::HashMap::new();
1846    let mut to_remove: Vec<usize> = Vec::new();
1847    for (i, c) in classes.iter().enumerate() {
1848        let key = strip_generics(&c.name);
1849        canonical_index.entry(key).or_insert(i);
1850    }
1851
1852    for i in 0..classes.len() {
1853        let key = strip_generics(&classes[i].name);
1854        let canonical = match canonical_index.get(&key) {
1855            Some(&idx) => idx,
1856            None => continue,
1857        };
1858        if i == canonical {
1859            continue;
1860        }
1861        // Fold methods (and bases) into canonical.
1862        let methods = std::mem::take(&mut classes[i].methods);
1863        let bases = std::mem::take(&mut classes[i].bases);
1864        let private_count = classes[i].private_method_count;
1865
1866        let canonical_entry = &mut classes[canonical];
1867        for m in methods {
1868            let already = canonical_entry.methods.iter().any(|existing| {
1869                existing.name == m.name && existing.signature == m.signature
1870            });
1871            if !already {
1872                canonical_entry.methods.push(m);
1873            }
1874        }
1875        for b in bases {
1876            if !canonical_entry.bases.contains(&b) {
1877                canonical_entry.bases.push(b);
1878            }
1879        }
1880        canonical_entry.private_method_count =
1881            canonical_entry.private_method_count.saturating_add(private_count);
1882        to_remove.push(i);
1883    }
1884
1885    // Remove duplicates in reverse order so indices remain valid.
1886    for idx in to_remove.into_iter().rev() {
1887        classes.remove(idx);
1888    }
1889
1890    // Step 4: drop any remaining entries whose name count was originally
1891    // > 1 but which are now empty placeholders (this happens for
1892    // `impl Trait for ExternalType` where ExternalType has no
1893    // struct/enum/trait declaration in the same file — the impl entry
1894    // was folded into the canonical, leaving the canonical entry as a
1895    // duplicate-of-self; nothing to drop in that case). Reserved for
1896    // future expansion.
1897    let _ = name_counts;
1898}
1899
1900/// Strip generic / lifetime parameters from a Rust type name.
1901/// `Vec<T>` -> `Vec`, `Foo<'a>` -> `Foo`, `Bar` -> `Bar`.
1902fn strip_generics(name: &str) -> String {
1903    if let Some(idx) = name.find('<') {
1904        name[..idx].trim().to_string()
1905    } else {
1906        name.trim().to_string()
1907    }
1908}
1909
1910/// Walk the entire AST of a file, collecting class-like nodes and any
1911/// function definitions that are NOT methods inside a class.
1912///
1913/// Used for cpp/c/csharp/kotlin/swift where:
1914/// * cpp headers often have macro-prefixed `class TINYXML2_LIB Foo` that
1915///   confuse tree-sitter into emitting ERROR / function_definition wrappers
1916///   around the namespace body, so plain root-children iteration misses them.
1917/// * csharp wraps everything under one or more `namespace_declaration` /
1918///   `file_scoped_namespace_declaration` nodes.
1919/// * kotlin/swift normally have classes at the file root, but extension-only
1920///   files (Span+Extras.swift) and nested object_declaration trees benefit
1921///   from a full walk.
1922#[allow(clippy::too_many_arguments)]
1923fn deep_collect(
1924    node: Node,
1925    source: &[u8],
1926    lang: Language,
1927    func_kinds: &[&str],
1928    class_kinds: &[&str],
1929    functions: &mut Vec<FunctionInfo>,
1930    classes: &mut Vec<ClassInfo>,
1931    depth: usize,
1932) {
1933    // review-followup-v1 (Concern 4): defense-in-depth bound matching
1934    // `visit_top_level`'s `MAX_CONTAINER_DEPTH = 8`. Tree-sitter limits
1935    // real-code nesting in practice, but a corrupt or adversarial AST
1936    // could still produce deep recursion; cap it here for consistency.
1937    const MAX_DEEP_WALK_DEPTH: usize = 8;
1938    if depth > MAX_DEEP_WALK_DEPTH {
1939        return;
1940    }
1941    let mut cursor = node.walk();
1942    for child in node.children(&mut cursor) {
1943        let kind = child.kind();
1944        if class_kinds.contains(&kind) {
1945            // Avoid double-counting nested classes when an enclosing class
1946            // already collected its inner methods/types via extract_class_info.
1947            // Top-level rule: a class node is "top-level" iff it isn't itself
1948            // contained in another class-kind ancestor.
1949            if !is_inside_class_ancestor(child, class_kinds)
1950                && is_node_public(child, source, lang)
1951            {
1952                let info = extract_class_info(child, source, lang);
1953                // Skip empty/anonymous misparses where extract returned no name.
1954                if !info.name.is_empty() {
1955                    classes.push(info);
1956                }
1957            }
1958            // Still recurse into the body — nested classes that are themselves
1959            // public should also surface (mirrors tree-walk behaviour of
1960            // `tldr extract` for cpp / csharp).
1961            deep_collect(
1962                child,
1963                source,
1964                lang,
1965                func_kinds,
1966                class_kinds,
1967                functions,
1968                classes,
1969                depth + 1,
1970            );
1971            continue;
1972        }
1973        if func_kinds.contains(&kind)
1974            && !is_inside_class_ancestor(child, class_kinds)
1975            && is_node_public(child, source, lang)
1976        {
1977            functions.push(extract_function_info(child, source, lang));
1978        }
1979        deep_collect(
1980            child,
1981            source,
1982            lang,
1983            func_kinds,
1984            class_kinds,
1985            functions,
1986            classes,
1987            depth + 1,
1988        );
1989    }
1990}
1991
1992/// Check whether a node is contained within a class/struct/interface ancestor.
1993/// Used to distinguish top-level functions from methods.
1994fn is_inside_class_ancestor(node: Node, class_kinds: &[&str]) -> bool {
1995    let mut current = node.parent();
1996    while let Some(parent) = current {
1997        if class_kinds.contains(&parent.kind()) {
1998            return true;
1999        }
2000        current = parent.parent();
2001    }
2002    false
2003}
2004
2005#[allow(clippy::too_many_arguments)]
2006fn visit_top_level(
2007    node: Node,
2008    source: &[u8],
2009    lang: Language,
2010    func_kinds: &[&str],
2011    class_kinds: &[&str],
2012    decorator_kinds: &[&str],
2013    functions: &mut Vec<FunctionInfo>,
2014    classes: &mut Vec<ClassInfo>,
2015    depth: usize,
2016) {
2017    // Bound recursion conservatively — we only ever need to descend through
2018    // a handful of namespace/preproc levels in real-world code.
2019    const MAX_CONTAINER_DEPTH: usize = 8;
2020    if depth > MAX_CONTAINER_DEPTH {
2021        return;
2022    }
2023
2024    let mut cursor = node.walk();
2025
2026    for child in node.children(&mut cursor) {
2027        let kind = child.kind();
2028
2029        // Elixir-specific dispatch: `call` nodes match BOTH func_kinds and
2030        // class_kinds, so the original logic always took the function
2031        // branch and `defmodule` calls were dropped. BUG-AGG-9 (P11):
2032        // restructure so we route on the call target name.
2033        //
2034        // - `def` / `defmacro` -> public function
2035        // - `defp` / `defmacrop` -> private, skip
2036        // - `defmodule` -> recurse into its `do_block` so nested public
2037        //   `def`s surface as top-level exports (matches `tldr extract`'s
2038        //   walk; mirrors how the Plug.Conn module exposes its public
2039        //   API even though every function lives one level deep).
2040        if lang == Language::Elixir && kind == "call" {
2041            let target_text = child.child(0).map(|t| node_text(t, source)).unwrap_or("");
2042            match target_text {
2043                "def" | "defmacro" => {
2044                    functions.push(extract_function_info(child, source, lang));
2045                }
2046                "defp" | "defmacrop" => {
2047                    // private, skip
2048                }
2049                "defmodule" => {
2050                    // Recurse into the module body. Module body is a `do_block`
2051                    // child of the call node.
2052                    let mut mod_cursor = child.walk();
2053                    for mod_child in child.children(&mut mod_cursor) {
2054                        if mod_child.kind() == "do_block" {
2055                            visit_top_level(
2056                                mod_child,
2057                                source,
2058                                lang,
2059                                func_kinds,
2060                                class_kinds,
2061                                decorator_kinds,
2062                                functions,
2063                                classes,
2064                                depth + 1,
2065                            );
2066                        }
2067                    }
2068                }
2069                _ => {}
2070            }
2071            continue;
2072        }
2073
2074        if func_kinds.contains(&kind) {
2075            if is_node_public(child, source, lang) {
2076                functions.push(extract_function_info(child, source, lang));
2077            }
2078        } else if class_kinds.contains(&kind) {
2079            if is_node_public(child, source, lang) {
2080                classes.push(extract_class_info(child, source, lang));
2081            }
2082        } else if decorator_kinds.contains(&kind) {
2083            // Handle decorated definitions (Python)
2084            if let Some(def) = find_definition_in_decorated(child, func_kinds) {
2085                if is_node_public(def, source, lang) {
2086                    functions.push(extract_function_info(def, source, lang));
2087                }
2088            } else if let Some(class_def) = find_definition_in_decorated(child, class_kinds) {
2089                if is_node_public(class_def, source, lang) {
2090                    classes.push(extract_class_info(class_def, source, lang));
2091                }
2092            }
2093        } else if is_interface_container(kind) {
2094            // Recurse into namespace / preproc / linkage containers so that
2095            // classes defined inside `namespace foo { ... }` (cpp/csharp) or
2096            // gated by `#if ... #endif` (cpp) surface as top-level exports.
2097            visit_top_level(
2098                child,
2099                source,
2100                lang,
2101                func_kinds,
2102                class_kinds,
2103                decorator_kinds,
2104                functions,
2105                classes,
2106                depth + 1,
2107            );
2108        } else if lang == Language::Php {
2109            // PHP wraps everything in a program > php_tag + declaration list.
2110            // Recurse one level for these.
2111            let mut inner_cursor = child.walk();
2112            for inner_child in child.children(&mut inner_cursor) {
2113                let inner_kind = inner_child.kind();
2114                if func_kinds.contains(&inner_kind) {
2115                    if is_node_public(inner_child, source, lang) {
2116                        functions.push(extract_function_info(inner_child, source, lang));
2117                    }
2118                } else if class_kinds.contains(&inner_kind)
2119                    && is_node_public(inner_child, source, lang)
2120                {
2121                    classes.push(extract_class_info(inner_child, source, lang));
2122                }
2123            }
2124        }
2125    }
2126}
2127
2128// =============================================================================
2129// Text Formatting
2130// =============================================================================
2131
2132/// Format interface info as human-readable text.
2133pub fn format_interface_text(info: &InterfaceInfo) -> String {
2134    let mut lines = Vec::new();
2135
2136    // Header
2137    lines.push(format!("File: {}", info.file));
2138    lines.push(String::new());
2139
2140    // Public exports (from __all__ if present, else inferred from
2141    // public function/class names — see InterfaceInfo::all_exports).
2142    if !info.all_exports.is_empty() {
2143        lines.push("Exports:".to_string());
2144        for name in &info.all_exports {
2145            lines.push(format!("  {}", name));
2146        }
2147        lines.push(String::new());
2148    }
2149
2150    // Functions
2151    if !info.functions.is_empty() {
2152        lines.push("Functions:".to_string());
2153        for func in &info.functions {
2154            let async_marker = if func.is_async { "async " } else { "" };
2155            lines.push(format!(
2156                "  {}def {}{}  [line {}]",
2157                async_marker, func.name, func.signature, func.lineno
2158            ));
2159            if let Some(ref doc) = func.docstring {
2160                // Truncate long docstrings
2161                let doc_preview = if doc.len() > 60 {
2162                    format!("{}...", &doc[..57])
2163                } else {
2164                    doc.clone()
2165                };
2166                lines.push(format!("      \"{}\"", doc_preview));
2167            }
2168        }
2169        lines.push(String::new());
2170    }
2171
2172    // Classes
2173    if !info.classes.is_empty() {
2174        lines.push("Classes:".to_string());
2175        for class in &info.classes {
2176            let bases_str = if class.bases.is_empty() {
2177                String::new()
2178            } else {
2179                format!("({})", class.bases.join(", "))
2180            };
2181            lines.push(format!(
2182                "  class {}{}  [line {}]",
2183                class.name, bases_str, class.lineno
2184            ));
2185
2186            for method in &class.methods {
2187                let async_marker = if method.is_async { "async " } else { "" };
2188                lines.push(format!(
2189                    "    {}def {}{}",
2190                    async_marker, method.name, method.signature
2191                ));
2192            }
2193
2194            if class.private_method_count > 0 {
2195                lines.push(format!(
2196                    "    ({} private methods)",
2197                    class.private_method_count
2198                ));
2199            }
2200        }
2201        lines.push(String::new());
2202    }
2203
2204    // Summary
2205    let total_methods: u32 = info.classes.iter().map(|c| c.methods.len() as u32).sum();
2206    lines.push(format!(
2207        "Summary: {} functions, {} classes, {} public methods",
2208        info.functions.len(),
2209        info.classes.len(),
2210        total_methods
2211    ));
2212
2213    lines.join("\n")
2214}
2215
2216// =============================================================================
2217// Entry Point
2218// =============================================================================
2219
2220/// Check if a file has a supported source code extension.
2221fn is_supported_source_file(path: &Path) -> bool {
2222    Language::from_path(path).is_some()
2223}
2224
2225/// Run the interface command.
2226pub fn run(args: InterfaceArgs, format: OutputFormat) -> anyhow::Result<()> {
2227    let path = &args.path;
2228
2229    if path.is_dir() {
2230        // Validate directory
2231        let canonical_dir = if let Some(ref root) = args.project_root {
2232            super::validation::validate_file_path_in_project(path, root)?
2233        } else {
2234            validate_directory_path(path)?
2235        };
2236
2237        // Collect all supported source files recursively
2238        let mut results = Vec::new();
2239        let mut entries: Vec<PathBuf> = walk_project(&canonical_dir)
2240            .filter(|e| e.path().is_file() && is_supported_source_file(e.path()))
2241            .map(|e| e.path().to_path_buf())
2242            .collect();
2243
2244        // Sort for deterministic output
2245        entries.sort();
2246
2247        for file_path in entries {
2248            let source = read_file_safe(&file_path)?;
2249            match extract_interface(&file_path, &source) {
2250                Ok(info) => results.push(info),
2251                Err(_) => {
2252                    // Skip files that fail to parse (unsupported grammars, etc.)
2253                    continue;
2254                }
2255            }
2256        }
2257
2258        // Output
2259        match format {
2260            OutputFormat::Text => {
2261                for info in &results {
2262                    println!("{}", format_interface_text(info));
2263                    println!();
2264                }
2265            }
2266            OutputFormat::Compact => {
2267                let json = serde_json::to_string(&results)?;
2268                println!("{}", json);
2269            }
2270            _ => {
2271                let json = serde_json::to_string_pretty(&results)?;
2272                println!("{}", json);
2273            }
2274        }
2275    } else {
2276        // Single file
2277        let canonical_path = if let Some(ref root) = args.project_root {
2278            super::validation::validate_file_path_in_project(path, root)?
2279        } else {
2280            validate_file_path(path)?
2281        };
2282
2283        let source = read_file_safe(&canonical_path)?;
2284        let mut info = extract_interface(&canonical_path, &source)?;
2285
2286        // (path-and-schema-cleanup-v3 P3.BUG-N2) Echo the user-supplied
2287        // path in the JSON `file` field. The canonical path is used for
2288        // the actual read, but the emit path mirrors the input verbatim
2289        // so macOS does not rewrite `/tmp/...` to `/private/tmp/...`.
2290        info.file = path.display().to_string();
2291
2292        // Output
2293        match format {
2294            OutputFormat::Text => {
2295                println!("{}", format_interface_text(&info));
2296            }
2297            OutputFormat::Compact => {
2298                let json = serde_json::to_string(&info)?;
2299                println!("{}", json);
2300            }
2301            _ => {
2302                let json = serde_json::to_string_pretty(&info)?;
2303                println!("{}", json);
2304            }
2305        }
2306    }
2307
2308    Ok(())
2309}
2310
2311// =============================================================================
2312// Utilities
2313// =============================================================================
2314
2315/// Get the text content of a node.
2316fn node_text<'a>(node: Node, source: &'a [u8]) -> &'a str {
2317    node.utf8_text(source).unwrap_or("")
2318}
2319
2320// =============================================================================
2321// Tests
2322// =============================================================================
2323
2324#[cfg(test)]
2325mod tests {
2326    use super::*;
2327
2328    // -------------------------------------------------------------------------
2329    // is_public_name tests (backward-compatible)
2330    // -------------------------------------------------------------------------
2331
2332    #[test]
2333    fn test_is_public_name_public() {
2334        assert!(is_public_name("my_function"));
2335        assert!(is_public_name("MyClass"));
2336        assert!(is_public_name("process"));
2337        assert!(is_public_name("x"));
2338    }
2339
2340    #[test]
2341    fn test_is_public_name_private() {
2342        assert!(!is_public_name("_private"));
2343        assert!(!is_public_name("__dunder__"));
2344        assert!(!is_public_name("_PrivateClass"));
2345        assert!(!is_public_name("__init__"));
2346    }
2347
2348    // -------------------------------------------------------------------------
2349    // Python: extract_all_exports tests
2350    // -------------------------------------------------------------------------
2351
2352    #[test]
2353    fn test_extract_all_exports_present() {
2354        let source = r#"
2355__all__ = ['foo', 'bar', 'Baz']
2356
2357def foo():
2358    pass
2359"#;
2360        let pool = ParserPool::new();
2361        let tree = pool.parse(source, Language::Python).unwrap();
2362        let root = tree.root_node();
2363
2364        let exports = extract_all_exports(root, source.as_bytes());
2365        assert!(exports.is_some());
2366        let exports = exports.unwrap();
2367        assert_eq!(exports.len(), 3);
2368        assert!(exports.contains(&"foo".to_string()));
2369        assert!(exports.contains(&"bar".to_string()));
2370        assert!(exports.contains(&"Baz".to_string()));
2371    }
2372
2373    #[test]
2374    fn test_extract_all_exports_absent() {
2375        let source = r#"
2376def foo():
2377    pass
2378"#;
2379        let pool = ParserPool::new();
2380        let tree = pool.parse(source, Language::Python).unwrap();
2381        let root = tree.root_node();
2382
2383        let exports = extract_all_exports(root, source.as_bytes());
2384        assert!(exports.is_none());
2385    }
2386
2387    // -------------------------------------------------------------------------
2388    // Python: extract_function_signature tests
2389    // -------------------------------------------------------------------------
2390
2391    #[test]
2392    fn test_extract_function_signature_simple() {
2393        let source = "def foo(x, y): pass";
2394        let pool = ParserPool::new();
2395        let tree = pool.parse(source, Language::Python).unwrap();
2396        let root = tree.root_node();
2397        let func_node = root.child(0).unwrap();
2398
2399        let sig = extract_function_signature(func_node, source.as_bytes(), Language::Python);
2400        assert_eq!(sig, "(x, y)");
2401    }
2402
2403    #[test]
2404    fn test_extract_function_signature_typed() {
2405        let source = "def foo(x: int, y: str) -> bool: pass";
2406        let pool = ParserPool::new();
2407        let tree = pool.parse(source, Language::Python).unwrap();
2408        let root = tree.root_node();
2409        let func_node = root.child(0).unwrap();
2410
2411        let sig = extract_function_signature(func_node, source.as_bytes(), Language::Python);
2412        assert!(sig.contains("x: int"), "sig = {:?}", sig);
2413        assert!(sig.contains("y: str"), "sig = {:?}", sig);
2414        assert!(sig.contains("-> bool"), "sig = {:?}", sig);
2415    }
2416
2417    #[test]
2418    fn test_extract_function_signature_default() {
2419        let source = "def foo(x: int = 10): pass";
2420        let pool = ParserPool::new();
2421        let tree = pool.parse(source, Language::Python).unwrap();
2422        let root = tree.root_node();
2423        let func_node = root.child(0).unwrap();
2424
2425        let sig = extract_function_signature(func_node, source.as_bytes(), Language::Python);
2426        assert!(sig.contains("x: int = 10") || sig.contains("x: int=10"));
2427    }
2428
2429    // -------------------------------------------------------------------------
2430    // Python: extract_interface tests
2431    // -------------------------------------------------------------------------
2432
2433    #[test]
2434    fn test_extract_interface_public_functions() {
2435        let source = r#"
2436def public_func():
2437    """A public function."""
2438    pass
2439
2440def _private_func():
2441    pass
2442"#;
2443        let info = extract_interface(Path::new("test.py"), source).unwrap();
2444
2445        assert_eq!(info.functions.len(), 1);
2446        assert_eq!(info.functions[0].name, "public_func");
2447    }
2448
2449    #[test]
2450    fn test_extract_interface_public_classes() {
2451        let source = r#"
2452class PublicClass:
2453    def public_method(self):
2454        pass
2455
2456    def _private_method(self):
2457        pass
2458
2459class _PrivateClass:
2460    pass
2461"#;
2462        let info = extract_interface(Path::new("test.py"), source).unwrap();
2463
2464        assert_eq!(info.classes.len(), 1);
2465        assert_eq!(info.classes[0].name, "PublicClass");
2466        assert_eq!(info.classes[0].methods.len(), 1);
2467        assert_eq!(info.classes[0].methods[0].name, "public_method");
2468        assert_eq!(info.classes[0].private_method_count, 1);
2469    }
2470
2471    #[test]
2472    fn test_extract_interface_async_function() {
2473        let source = r#"
2474async def async_func():
2475    pass
2476
2477def sync_func():
2478    pass
2479"#;
2480        let info = extract_interface(Path::new("test.py"), source).unwrap();
2481
2482        assert_eq!(info.functions.len(), 2);
2483
2484        let async_fn = info.functions.iter().find(|f| f.name == "async_func");
2485        assert!(async_fn.is_some());
2486        assert!(async_fn.unwrap().is_async);
2487
2488        let sync_fn = info.functions.iter().find(|f| f.name == "sync_func");
2489        assert!(sync_fn.is_some());
2490        assert!(!sync_fn.unwrap().is_async);
2491    }
2492
2493    #[test]
2494    fn test_extract_interface_with_all() {
2495        let source = r#"
2496__all__ = ['foo', 'Bar']
2497
2498def foo():
2499    pass
2500
2501def bar():
2502    pass
2503
2504class Bar:
2505    pass
2506"#;
2507        let info = extract_interface(Path::new("test.py"), source).unwrap();
2508
2509        // schema-cleanup-v1 BUG-22: all_exports is now Vec<String>
2510        // (never null). When `__all__` is present, it carries those.
2511        assert!(!info.all_exports.is_empty());
2512        assert!(info.all_exports.contains(&"foo".to_string()));
2513        assert!(info.all_exports.contains(&"Bar".to_string()));
2514    }
2515
2516    #[test]
2517    fn test_extract_interface_docstrings() {
2518        let source = r#"
2519def documented():
2520    """This is a docstring."""
2521    pass
2522
2523def undocumented():
2524    pass
2525"#;
2526        let info = extract_interface(Path::new("test.py"), source).unwrap();
2527
2528        let documented = info.functions.iter().find(|f| f.name == "documented");
2529        assert!(documented.is_some());
2530        assert!(documented.unwrap().docstring.is_some());
2531        assert!(documented
2532            .unwrap()
2533            .docstring
2534            .as_ref()
2535            .unwrap()
2536            .contains("docstring"));
2537
2538        let undocumented = info.functions.iter().find(|f| f.name == "undocumented");
2539        assert!(undocumented.is_some());
2540        assert!(undocumented.unwrap().docstring.is_none());
2541    }
2542
2543    #[test]
2544    fn test_extract_interface_class_bases() {
2545        let source = r#"
2546class Child(Parent, Mixin):
2547    pass
2548"#;
2549        let info = extract_interface(Path::new("test.py"), source).unwrap();
2550
2551        assert_eq!(info.classes.len(), 1);
2552        assert_eq!(info.classes[0].bases.len(), 2);
2553        assert!(info.classes[0].bases.contains(&"Parent".to_string()));
2554        assert!(info.classes[0].bases.contains(&"Mixin".to_string()));
2555    }
2556
2557    // -------------------------------------------------------------------------
2558    // format_interface_text tests
2559    // -------------------------------------------------------------------------
2560
2561    #[test]
2562    fn test_format_interface_text() {
2563        let info = InterfaceInfo {
2564            file: "test.py".to_string(),
2565            all_exports: vec!["foo".to_string()],
2566            functions: vec![FunctionInfo {
2567                name: "foo".to_string(),
2568                signature: "(x: int) -> str".to_string(),
2569                docstring: Some("A function.".to_string()),
2570                lineno: 5,
2571                is_async: false,
2572            }],
2573            classes: vec![ClassInfo {
2574                name: "MyClass".to_string(),
2575                lineno: 10,
2576                bases: vec!["Base".to_string()],
2577                methods: vec![MethodInfo {
2578                    name: "method".to_string(),
2579                    signature: "(self)".to_string(),
2580                    is_async: false,
2581                }],
2582                private_method_count: 2,
2583            }],
2584        };
2585
2586        let text = format_interface_text(&info);
2587        assert!(text.contains("File: test.py"));
2588        assert!(text.contains("foo"));
2589        assert!(text.contains("MyClass"));
2590        assert!(text.contains("Base"));
2591        assert!(text.contains("method"));
2592        assert!(text.contains("2 private methods"));
2593    }
2594
2595    // =========================================================================
2596    // Multi-language tests
2597    // =========================================================================
2598
2599    // -------------------------------------------------------------------------
2600    // Rust
2601    // -------------------------------------------------------------------------
2602
2603    #[test]
2604    fn test_extract_interface_rust_pub_functions() {
2605        let source = r#"
2606/// Adds two numbers.
2607pub fn add(a: i32, b: i32) -> i32 {
2608    a + b
2609}
2610
2611fn private_helper() -> bool {
2612    true
2613}
2614
2615pub async fn async_fetch() -> String {
2616    String::new()
2617}
2618"#;
2619        let info = extract_interface(Path::new("test.rs"), source).unwrap();
2620
2621        assert_eq!(
2622            info.functions.len(),
2623            2,
2624            "Should find 2 pub functions, got: {:?}",
2625            info.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
2626        );
2627
2628        let add_fn = info.functions.iter().find(|f| f.name == "add");
2629        assert!(add_fn.is_some(), "Should find 'add' function");
2630        let add_fn = add_fn.unwrap();
2631        assert!(
2632            add_fn.signature.contains("a: i32"),
2633            "sig = {:?}",
2634            add_fn.signature
2635        );
2636        assert!(
2637            add_fn.signature.contains("-> i32"),
2638            "sig = {:?}",
2639            add_fn.signature
2640        );
2641        assert!(add_fn.docstring.is_some(), "Should have doc comment");
2642        assert!(add_fn
2643            .docstring
2644            .as_ref()
2645            .unwrap()
2646            .contains("Adds two numbers"));
2647        assert!(!add_fn.is_async);
2648
2649        let async_fn = info.functions.iter().find(|f| f.name == "async_fetch");
2650        assert!(async_fn.is_some(), "Should find 'async_fetch' function");
2651        assert!(async_fn.unwrap().is_async);
2652    }
2653
2654    #[test]
2655    fn test_extract_interface_rust_struct_impl() {
2656        let source = r#"
2657pub struct Point {
2658    pub x: f64,
2659    pub y: f64,
2660}
2661
2662impl Point {
2663    pub fn new(x: f64, y: f64) -> Self {
2664        Point { x, y }
2665    }
2666
2667    fn internal(&self) {}
2668}
2669"#;
2670        let info = extract_interface(Path::new("test.rs"), source).unwrap();
2671
2672        // Should find struct and impl as classes
2673        assert!(
2674            !info.classes.is_empty(),
2675            "Should find at least struct/impl, got: {:?}",
2676            info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2677        );
2678
2679        // Check the struct
2680        let point_struct = info.classes.iter().find(|c| c.name == "Point");
2681        assert!(point_struct.is_some(), "Should find Point struct/impl");
2682    }
2683
2684    #[test]
2685    fn test_extract_interface_rust_trait() {
2686        let source = r#"
2687pub trait Drawable {
2688    fn draw(&self);
2689    fn resize(&mut self, factor: f64);
2690}
2691"#;
2692        let info = extract_interface(Path::new("test.rs"), source).unwrap();
2693
2694        let trait_info = info.classes.iter().find(|c| c.name == "Drawable");
2695        assert!(trait_info.is_some(), "Should find Drawable trait");
2696    }
2697
2698    // -------------------------------------------------------------------------
2699    // Go
2700    // -------------------------------------------------------------------------
2701
2702    #[test]
2703    fn test_extract_interface_go_exported_functions() {
2704        let source = r#"
2705package main
2706
2707// ProcessData handles data processing.
2708func ProcessData(input string) (string, error) {
2709    return input, nil
2710}
2711
2712func internalHelper() bool {
2713    return true
2714}
2715"#;
2716        let info = extract_interface(Path::new("test.go"), source).unwrap();
2717
2718        // Go: exported functions start with uppercase
2719        assert_eq!(
2720            info.functions.len(),
2721            1,
2722            "Should find 1 exported function, got: {:?}",
2723            info.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
2724        );
2725        assert_eq!(info.functions[0].name, "ProcessData");
2726        assert!(
2727            info.functions[0].docstring.is_some(),
2728            "Should have doc comment"
2729        );
2730    }
2731
2732    // -------------------------------------------------------------------------
2733    // TypeScript
2734    // -------------------------------------------------------------------------
2735
2736    #[test]
2737    fn test_extract_interface_typescript_class() {
2738        let source = r#"
2739class UserService {
2740    async fetchUser(id: string): Promise<User> {
2741        return {} as User;
2742    }
2743
2744    private internalMethod(): void {}
2745}
2746
2747function processData(input: string): number {
2748    return input.length;
2749}
2750"#;
2751        let info = extract_interface(Path::new("test.ts"), source).unwrap();
2752
2753        // Should find both the class and the function
2754        assert!(
2755            !info.functions.is_empty() || !info.classes.is_empty(),
2756            "Should find definitions: functions={:?}, classes={:?}",
2757            info.functions.iter().map(|f| &f.name).collect::<Vec<_>>(),
2758            info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2759        );
2760    }
2761
2762    #[test]
2763    fn test_extract_interface_typescript_interface() {
2764        let source = r#"
2765interface User {
2766    id: string;
2767    name: string;
2768    email: string;
2769}
2770
2771type Status = "active" | "inactive";
2772"#;
2773        let info = extract_interface(Path::new("test.ts"), source).unwrap();
2774
2775        assert!(
2776            !info.classes.is_empty(),
2777            "Should find interface/type declarations, got: {:?}",
2778            info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2779        );
2780    }
2781
2782    // -------------------------------------------------------------------------
2783    // Java
2784    // -------------------------------------------------------------------------
2785
2786    #[test]
2787    fn test_extract_interface_java_class() {
2788        let source = r#"
2789/**
2790 * Service for managing users.
2791 */
2792public class UserService {
2793    public String getUser(String id) {
2794        return id;
2795    }
2796
2797    private void internalCleanup() {}
2798}
2799"#;
2800        let info = extract_interface(Path::new("test.java"), source).unwrap();
2801
2802        assert!(
2803            !info.classes.is_empty(),
2804            "Should find Java class, got: {:?}",
2805            info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2806        );
2807
2808        if let Some(cls) = info.classes.iter().find(|c| c.name == "UserService") {
2809            assert!(!cls.methods.is_empty(), "Should find public methods");
2810        }
2811    }
2812
2813    // -------------------------------------------------------------------------
2814    // C
2815    // -------------------------------------------------------------------------
2816
2817    #[test]
2818    fn test_extract_interface_c_functions() {
2819        let source = r#"
2820int add(int a, int b) {
2821    return a + b;
2822}
2823
2824static int internal_helper(void) {
2825    return 42;
2826}
2827"#;
2828        let info = extract_interface(Path::new("test.c"), source).unwrap();
2829
2830        // Non-static C functions should be public
2831        assert_eq!(
2832            info.functions.len(),
2833            1,
2834            "Should find 1 non-static function, got: {:?}",
2835            info.functions.iter().map(|f| &f.name).collect::<Vec<_>>()
2836        );
2837        assert_eq!(info.functions[0].name, "add");
2838    }
2839
2840    // -------------------------------------------------------------------------
2841    // Ruby
2842    // -------------------------------------------------------------------------
2843
2844    #[test]
2845    fn test_extract_interface_ruby_class() {
2846        let source = r#"
2847class UserManager
2848  def find_user(id)
2849    # find user
2850  end
2851
2852  def _private_method
2853    # private
2854  end
2855end
2856"#;
2857        let info = extract_interface(Path::new("test.rb"), source).unwrap();
2858
2859        assert!(
2860            !info.classes.is_empty(),
2861            "Should find Ruby class, got: {:?}",
2862            info.classes.iter().map(|c| &c.name).collect::<Vec<_>>()
2863        );
2864
2865        if let Some(cls) = info.classes.iter().find(|c| c.name == "UserManager") {
2866            assert_eq!(
2867                cls.methods.len(),
2868                1,
2869                "Should find 1 public method, got: {:?}",
2870                cls.methods.iter().map(|m| &m.name).collect::<Vec<_>>()
2871            );
2872            assert_eq!(cls.methods[0].name, "find_user");
2873            assert_eq!(cls.private_method_count, 1);
2874        }
2875    }
2876
2877    // -------------------------------------------------------------------------
2878    // is_public_for_lang tests
2879    // -------------------------------------------------------------------------
2880
2881    #[test]
2882    fn test_is_public_for_go() {
2883        assert!(is_public_for_lang("ProcessData", Language::Go));
2884        assert!(!is_public_for_lang("processData", Language::Go));
2885    }
2886
2887    #[test]
2888    fn test_is_public_for_python() {
2889        assert!(is_public_for_lang("process_data", Language::Python));
2890        assert!(!is_public_for_lang("_private", Language::Python));
2891    }
2892
2893    // -------------------------------------------------------------------------
2894    // is_supported_source_file tests
2895    // -------------------------------------------------------------------------
2896
2897    #[test]
2898    fn test_is_supported_source_file() {
2899        assert!(is_supported_source_file(Path::new("test.py")));
2900        assert!(is_supported_source_file(Path::new("test.rs")));
2901        assert!(is_supported_source_file(Path::new("test.go")));
2902        assert!(is_supported_source_file(Path::new("test.ts")));
2903        assert!(is_supported_source_file(Path::new("test.java")));
2904        assert!(is_supported_source_file(Path::new("test.c")));
2905        assert!(is_supported_source_file(Path::new("test.rb")));
2906        assert!(is_supported_source_file(Path::new("test.cs")));
2907        assert!(!is_supported_source_file(Path::new("test.txt")));
2908        assert!(!is_supported_source_file(Path::new("test.md")));
2909    }
2910}