codebank/parser/lang/
ts.rs

1use crate::{
2    DeclareKind, DeclareStatements, Error, FieldUnit, FileUnit, FunctionUnit, LanguageParser,
3    Result, StructUnit, TypeScriptParser, Visibility,
4};
5use std::{
6    fs,
7    ops::{Deref, DerefMut},
8    path::Path,
9};
10use tree_sitter::{Node, Parser};
11
12impl TypeScriptParser {
13    pub fn try_new() -> Result<Self> {
14        let mut parser = Parser::new();
15        let language = tree_sitter_typescript::LANGUAGE_TYPESCRIPT;
16        parser
17            .set_language(&language.into())
18            .map_err(|e| Error::TreeSitter(e.to_string()))?;
19        Ok(Self { parser })
20    }
21
22    // Helper method to process export statements
23    fn process_export(&self, file_unit: &mut FileUnit, node: Node, source: &[u8]) {
24        // Check if this is a standalone export or contains a declaration
25        if let Some(decl_node) = node.child_by_field_name("declaration") {
26            match decl_node.kind() {
27                "function_declaration" => {
28                    self.process_function(file_unit, decl_node, true, source);
29                }
30                "lexical_declaration" => {
31                    for j in 0..decl_node.child_count() {
32                        if let Some(var_node) = decl_node.child(j) {
33                            if var_node.kind() == "variable_declarator" {
34                                for k in 0..var_node.child_count() {
35                                    if let Some(value_node) = var_node.child(k) {
36                                        if value_node.kind() == "arrow_function"
37                                            || value_node.kind() == "function_expression"
38                                        {
39                                            self.process_function_variable(
40                                                file_unit, decl_node, var_node, true, source,
41                                            );
42                                            break;
43                                        }
44                                    }
45                                }
46                            }
47                        }
48                    }
49                }
50                "class_declaration" => {
51                    self.process_class(file_unit, decl_node, true, source);
52                }
53                "interface_declaration" => {
54                    self.process_interface(file_unit, decl_node, true, source);
55                }
56                "type_alias_declaration" => {
57                    self.process_type_alias(file_unit, decl_node, true, source);
58                }
59                "enum_declaration" => {
60                    self.process_enum(file_unit, decl_node, true, source);
61                }
62                _ => {}
63            }
64        } else {
65            // Standalone export
66            let source_text = node.utf8_text(source).unwrap_or("").to_string();
67            file_unit.declares.push(DeclareStatements {
68                source: source_text,
69                kind: DeclareKind::Other("export".to_string()),
70            });
71        }
72    }
73
74    // Process a function declaration
75    fn process_function(
76        &self,
77        file_unit: &mut FileUnit,
78        node: Node,
79        is_exported: bool,
80        source: &[u8],
81    ) {
82        if let Some(name_node) = node.child_by_field_name("name") {
83            let name = name_node.utf8_text(source).unwrap_or("").to_string();
84            let func_source = node.utf8_text(source).unwrap_or("").to_string();
85            let visibility = if is_exported {
86                Visibility::Public
87            } else {
88                Visibility::Private
89            };
90
91            // Check for documentation in previous sibling
92            let documentation = find_documentation_for_node(node, source);
93
94            // Extract function signature
95            let mut signature = String::from("function ");
96            signature.push_str(&name);
97
98            // Add parameters
99            if let Some(params_node) = node.child_by_field_name("parameters") {
100                signature.push_str(params_node.utf8_text(source).unwrap_or("").trim());
101            }
102
103            // Add return type if present
104            if let Some(return_type) = node.child_by_field_name("return_type") {
105                signature.push_str(return_type.utf8_text(source).unwrap_or(""));
106            }
107
108            file_unit.functions.push(FunctionUnit {
109                name,
110                source: Some(func_source),
111                visibility,
112                doc: documentation,
113                signature: Some(signature),
114                body: None,
115                attributes: vec![],
116            });
117        }
118    }
119
120    // Process a variable that contains a function
121    fn process_function_variable(
122        &self,
123        file_unit: &mut FileUnit,
124        decl_node: Node,
125        var_node: Node,
126        is_exported: bool,
127        source: &[u8],
128    ) {
129        if let Some(name_node) = var_node.child_by_field_name("name") {
130            let name = name_node.utf8_text(source).unwrap_or("").to_string();
131            let func_source = decl_node.utf8_text(source).unwrap_or("").to_string();
132            let visibility = if is_exported {
133                Visibility::Public
134            } else {
135                Visibility::Private
136            };
137
138            // Check for documentation
139            let documentation = find_documentation_for_node(decl_node, source);
140
141            // Find the function value (arrow function or function expression)
142            let mut signature = None;
143
144            if let Some(value_node) = var_node.child_by_field_name("value") {
145                if value_node.kind() == "arrow_function"
146                    || value_node.kind() == "function_expression"
147                {
148                    let mut sig = String::new();
149
150                    // For arrow functions, use the variable name and add parameters
151                    if value_node.kind() == "arrow_function" {
152                        sig.push_str(&name);
153
154                        // Add parameters
155                        if let Some(params_node) = value_node.child_by_field_name("parameters") {
156                            sig.push_str(params_node.utf8_text(source).unwrap_or("").trim());
157                        }
158
159                        // Add return type if present
160                        if let Some(return_type) = value_node.child_by_field_name("return_type") {
161                            sig.push_str(return_type.utf8_text(source).unwrap_or(""));
162                        }
163
164                        // Don't add the arrow operator to the signature
165                    } else {
166                        // For function expressions, format as "function name(params)"
167                        sig.push_str("function ");
168                        sig.push_str(&name);
169
170                        // Add parameters
171                        if let Some(params_node) = value_node.child_by_field_name("parameters") {
172                            sig.push_str(params_node.utf8_text(source).unwrap_or("").trim());
173                        }
174
175                        // Add return type if present
176                        if let Some(return_type) = value_node.child_by_field_name("return_type") {
177                            sig.push_str(return_type.utf8_text(source).unwrap_or(""));
178                        }
179                    }
180
181                    signature = Some(sig);
182                }
183            }
184
185            file_unit.functions.push(FunctionUnit {
186                name,
187                source: Some(func_source),
188                visibility,
189                doc: documentation,
190                signature,
191                body: None,
192                attributes: vec![],
193            });
194        }
195    }
196
197    // Process a class declaration
198    fn process_class(
199        &self,
200        file_unit: &mut FileUnit,
201        node: Node,
202        is_exported: bool,
203        source: &[u8],
204    ) {
205        if let Some(name_node) = node.child_by_field_name("name") {
206            let name = name_node.utf8_text(source).unwrap_or("").to_string();
207            let class_source = node.utf8_text(source).unwrap_or("").to_string();
208            let visibility = if is_exported {
209                Visibility::Public
210            } else {
211                Visibility::Private
212            };
213
214            // Check for documentation
215            let documentation = find_documentation_for_node(node, source);
216
217            // Extract methods from the class body
218            let mut fields = Vec::new();
219            let mut methods = Vec::new();
220
221            // Look for the class body
222            if let Some(body_node) = node.child_by_field_name("body") {
223                // Iterate through children to find method definitions
224                for i in 0..body_node.child_count() {
225                    if let Some(method_node) = body_node.child(i) {
226                        // Check for method_definition or constructor_definition
227                        if method_node.kind() == "method_definition"
228                            || method_node.kind() == "constructor_definition"
229                        {
230                            if let Some(method_name_node) = method_node.child_by_field_name("name")
231                            {
232                                let method_name =
233                                    method_name_node.utf8_text(source).unwrap_or("").to_string();
234                                let method_source =
235                                    method_node.utf8_text(source).unwrap_or("").to_string();
236
237                                // Extract method signature
238                                let mut signature = String::new();
239
240                                // Default visibility for methods is public unless marked private
241                                let mut method_visibility = Visibility::Public;
242
243                                // Check if it's a constructor
244                                if method_node.kind() == "constructor_definition" {
245                                    signature.push_str("constructor");
246                                } else {
247                                    // Get modifiers if any (public, private, etc.)
248                                    for j in 0..method_node.child_count() {
249                                        if let Some(modifier) = method_node.child(j) {
250                                            if modifier.kind() == "accessibility_modifier" {
251                                                let modifier_text =
252                                                    modifier.utf8_text(source).unwrap_or("").trim();
253                                                signature.push_str(modifier_text);
254                                                signature.push(' ');
255
256                                                // Set visibility based on the modifier
257                                                if modifier_text == "private" {
258                                                    method_visibility = Visibility::Private;
259                                                }
260                                                break;
261                                            }
262                                        }
263                                    }
264
265                                    // Add method name
266                                    signature.push_str(&method_name);
267                                }
268
269                                // Add parameters
270                                if let Some(params_node) =
271                                    method_node.child_by_field_name("parameters")
272                                {
273                                    signature.push_str(
274                                        params_node.utf8_text(source).unwrap_or("").trim(),
275                                    );
276                                }
277
278                                // Add return type if present
279                                if let Some(return_type) =
280                                    method_node.child_by_field_name("return_type")
281                                {
282                                    signature.push_str(return_type.utf8_text(source).unwrap_or(""));
283                                }
284
285                                // Add to methods list
286                                methods.push(FunctionUnit {
287                                    name: method_name,
288                                    source: Some(method_source),
289                                    visibility: method_visibility,
290                                    doc: None, // Could extract doc comments for methods too
291                                    signature: Some(signature),
292                                    body: None,
293                                    attributes: vec![],
294                                });
295                            }
296                        }
297                        // Check for field definition
298                        else if method_node.kind() == "public_field_definition" {
299                            if let Some(field_name_node) = method_node.child_by_field_name("name") {
300                                let field_name =
301                                    field_name_node.utf8_text(source).unwrap_or("").to_string();
302                                let field_source =
303                                    method_node.utf8_text(source).unwrap_or("").to_string();
304                                let field_doc = find_documentation_for_node(method_node, source);
305
306                                // TODO: Extract field attributes/decorators if needed
307                                fields.push(FieldUnit {
308                                    name: field_name,
309                                    source: Some(field_source),
310                                    doc: field_doc,
311                                    attributes: vec![],
312                                });
313                            }
314                        }
315                    }
316                }
317            }
318
319            file_unit.structs.push(StructUnit {
320                name: name.clone(),
321                source: Some(class_source),
322                head: format!("class {}", name),
323                visibility,
324                doc: documentation,
325                fields,
326                methods,
327                attributes: vec![],
328            });
329        }
330    }
331
332    // Process an interface declaration
333    fn process_interface(
334        &self,
335        file_unit: &mut FileUnit,
336        node: Node,
337        is_exported: bool,
338        source: &[u8],
339    ) {
340        if let Some(name_node) = node.child_by_field_name("name") {
341            let name = name_node.utf8_text(source).unwrap_or("").to_string();
342            let interface_source = node.utf8_text(source).unwrap_or("").to_string();
343            let visibility = if is_exported {
344                Visibility::Public
345            } else {
346                Visibility::Private
347            };
348
349            // Check for documentation
350            let documentation = find_documentation_for_node(node, source);
351
352            // Extract method declarations from the interface body
353            let mut fields = Vec::new();
354            let mut methods = Vec::new();
355
356            // Look for the interface body
357            if let Some(body_node) = node.child_by_field_name("body") {
358                // Iterate through children to find method declarations
359                for i in 0..body_node.child_count() {
360                    if let Some(method_node) = body_node.child(i) {
361                        // Check for method_signature or property_signature with function type
362                        if method_node.kind() == "method_signature" {
363                            if let Some(method_name_node) = method_node.child_by_field_name("name")
364                            {
365                                let method_name =
366                                    method_name_node.utf8_text(source).unwrap_or("").to_string();
367                                let method_source =
368                                    method_node.utf8_text(source).unwrap_or("").to_string();
369
370                                // Extract method signature
371                                let mut signature = String::new();
372
373                                // Interface methods are public by default in TypeScript
374                                signature.push_str(&method_name);
375
376                                // Add parameters
377                                if let Some(params_node) =
378                                    method_node.child_by_field_name("parameters")
379                                {
380                                    signature.push_str(
381                                        params_node.utf8_text(source).unwrap_or("").trim(),
382                                    );
383                                }
384
385                                // Add return type if present
386                                if let Some(return_type) =
387                                    method_node.child_by_field_name("return_type")
388                                {
389                                    signature.push_str(return_type.utf8_text(source).unwrap_or(""));
390                                }
391
392                                // Add to methods list (interface methods are always public)
393                                methods.push(FunctionUnit {
394                                    name: method_name,
395                                    source: Some(method_source),
396                                    visibility: Visibility::Public,
397                                    doc: None,
398                                    signature: Some(signature),
399                                    body: None,
400                                    attributes: vec![],
401                                });
402                            }
403                        } else if method_node.kind() == "property_signature" {
404                            if let Some(field_name_node) = method_node.child_by_field_name("name") {
405                                let field_name =
406                                    field_name_node.utf8_text(source).unwrap_or("").to_string();
407                                let field_source =
408                                    method_node.utf8_text(source).unwrap_or("").to_string();
409                                let field_doc = find_documentation_for_node(method_node, source);
410
411                                fields.push(FieldUnit {
412                                    name: field_name,
413                                    source: Some(field_source),
414                                    doc: field_doc,
415                                    attributes: vec![],
416                                });
417                            }
418                        }
419                    }
420                }
421            }
422
423            file_unit.structs.push(StructUnit {
424                name: name.clone(),
425                source: Some(interface_source),
426                head: format!("interface {}", name),
427                visibility,
428                doc: documentation,
429                fields,
430                methods,
431                attributes: vec![],
432            });
433        }
434    }
435
436    // Process a type alias declaration
437    fn process_type_alias(
438        &self,
439        file_unit: &mut FileUnit,
440        node: Node,
441        is_exported: bool,
442        source: &[u8],
443    ) {
444        if let Some(name_node) = node.child_by_field_name("name") {
445            let name = name_node.utf8_text(source).unwrap_or("").to_string();
446            let type_source = node.utf8_text(source).unwrap_or("").to_string();
447            let visibility = if is_exported {
448                Visibility::Public
449            } else {
450                Visibility::Private
451            };
452
453            // Check for documentation
454            let documentation = find_documentation_for_node(node, source);
455
456            file_unit.structs.push(StructUnit {
457                name: name.clone(),
458                source: Some(type_source),
459                head: format!("type {}", name),
460                visibility,
461                doc: documentation,
462                methods: vec![],
463                fields: Vec::new(),
464                attributes: vec![],
465            });
466        }
467    }
468
469    // Process an enum declaration
470    fn process_enum(&self, file_unit: &mut FileUnit, node: Node, is_exported: bool, source: &[u8]) {
471        if let Some(name_node) = node.child_by_field_name("name") {
472            let name = name_node.utf8_text(source).unwrap_or("").to_string();
473            let enum_source = node.utf8_text(source).unwrap_or("").to_string();
474            let visibility = if is_exported {
475                Visibility::Public
476            } else {
477                Visibility::Private
478            };
479
480            // Check for documentation
481            let documentation = find_documentation_for_node(node, source);
482
483            file_unit.structs.push(StructUnit {
484                name: name.clone(),
485                source: Some(enum_source),
486                head: format!("enum {}", name),
487                visibility,
488                doc: documentation,
489                methods: vec![],
490                fields: Vec::new(),
491                attributes: vec![],
492            });
493        }
494    }
495}
496
497// --- Helper Functions ---
498
499// Helper to find documentation for a node
500fn find_documentation_for_node(node: Node, source: &[u8]) -> Option<String> {
501    let mut current_node = node;
502
503    // Only look for preceding JSDoc block comments
504    while let Some(prev) = current_node.prev_sibling() {
505        // Check for comment node kind
506        if prev.kind() == "comment" {
507            let text = prev.utf8_text(source).ok()?;
508            // Only consider JSDoc block comments
509            if text.starts_with("/**") {
510                // Check if it's adjacent (no other significant nodes in between)
511                // This logic tries to ensure the comment is directly related.
512                if prev.end_byte() == current_node.start_byte() - 1 || // Directly adjacent
513                    (prev.end_byte() < current_node.start_byte() && // Or only whitespace/newlines between
514                     source[prev.end_byte()..current_node.start_byte()].iter().all(|&b| b.is_ascii_whitespace()))
515                {
516                    return extract_doc_comment(prev, source); // Use the dedicated JSDoc extractor
517                }
518                // If it's a JSDoc but not adjacent, stop looking further back
519                // to avoid associating distant comments.
520                break;
521            }
522        }
523        // Stop if we hit a non-comment, non-extra node
524        else if !prev.is_extra() {
525            break;
526        }
527
528        // Move to the previous sibling to continue searching backwards
529        current_node = prev;
530    }
531
532    // If not found immediately preceding, check if parent is export statement
533    // and look before that (recursive call might be cleaner, but let's try this)
534    if let Some(parent) = node.parent() {
535        if parent.kind() == "export_statement" {
536            current_node = parent;
537            while let Some(prev) = current_node.prev_sibling() {
538                if prev.kind() == "comment" {
539                    let text = prev.utf8_text(source).ok()?;
540                    if text.starts_with("/**") {
541                        if prev.end_byte() == current_node.start_byte() - 1
542                            || (prev.end_byte() < current_node.start_byte()
543                                && source[prev.end_byte()..current_node.start_byte()]
544                                    .iter()
545                                    .all(|&b| b.is_ascii_whitespace()))
546                        {
547                            return extract_doc_comment(prev, source);
548                        }
549                        break; // Found JSDoc but not adjacent
550                    }
551                } else if !prev.is_extra() {
552                    break; // Found non-comment
553                }
554                current_node = prev;
555            }
556        }
557    }
558
559    None // No appropriate comment found
560}
561
562/// Extracts documentation from a JSDoc comment node.
563fn extract_doc_comment(node: Node, source: &[u8]) -> Option<String> {
564    if node.kind() == "comment" {
565        let text = node.utf8_text(source).ok()?;
566        if text.starts_with("/**") {
567            let cleaned = text
568                .trim_start_matches("/**")
569                .trim_end_matches("*/")
570                .lines()
571                .map(|line| {
572                    let trimmed = line.trim_start();
573                    if trimmed.starts_with('*') {
574                        // Handle `*` or `* ` prefix
575                        trimmed.trim_start_matches('*').trim_start()
576                    } else {
577                        trimmed
578                    }
579                })
580                .collect::<Vec<&str>>()
581                .join("\n")
582                .trim()
583                .to_string();
584
585            if cleaned.is_empty() {
586                None
587            } else {
588                Some(cleaned)
589            }
590        } else {
591            None
592        }
593    } else {
594        None
595    }
596}
597
598/// Finds the next non-comment, non-extra sibling node.
599#[allow(dead_code)]
600fn find_next_sibling_node(node: Node) -> Option<Node> {
601    let mut current_node = node;
602    while let Some(sibling) = current_node.next_sibling() {
603        if sibling.kind() != "comment" && !sibling.is_extra() {
604            return Some(sibling);
605        }
606        current_node = sibling;
607    }
608    None
609}
610
611/// Look for documentation in previous sibling comment node
612#[allow(dead_code)]
613fn find_doc_in_previous_comment(node: Node, source: &[u8]) -> Option<String> {
614    let mut current = node;
615    while let Some(prev) = current.prev_sibling() {
616        if prev.kind() == "comment" {
617            return extract_doc_comment(prev, source);
618        }
619        // Skip extra nodes like whitespace
620        if !prev.is_extra() {
621            // Stop if we find a non-comment, non-extra node
622            break;
623        }
624        current = prev;
625    }
626
627    // If we didn't find documentation and this node is inside an export statement,
628    // look for documentation before the export statement
629    if let Some(parent) = node.parent() {
630        if parent.kind() == "export_statement" {
631            return find_doc_in_previous_comment(parent, source);
632        }
633    }
634
635    None
636}
637
638impl Deref for TypeScriptParser {
639    type Target = Parser;
640
641    fn deref(&self) -> &Self::Target {
642        &self.parser
643    }
644}
645
646impl DerefMut for TypeScriptParser {
647    fn deref_mut(&mut self) -> &mut Self::Target {
648        &mut self.parser
649    }
650}
651
652impl LanguageParser for TypeScriptParser {
653    fn parse_file(&mut self, file_path: &Path) -> Result<FileUnit> {
654        let source_code = fs::read_to_string(file_path).map_err(Error::Io)?;
655        let source_bytes = source_code.as_bytes();
656
657        let tree = self.parser.parse(&source_code, None).ok_or_else(|| {
658            Error::Parse(format!(
659                "Tree-sitter failed to parse the file: {}",
660                file_path.display()
661            ))
662        })?;
663
664        let mut file_unit = FileUnit {
665            path: file_path.to_path_buf(),
666            source: Some(source_code.clone()),
667            ..Default::default()
668        };
669
670        let root_node = tree.root_node();
671
672        // First, check for file-level documentation
673        if let Some(child) = root_node.child(0) {
674            if child.kind() == "comment" {
675                if let Some(doc) = extract_doc_comment(child, source_bytes) {
676                    file_unit.doc = Some(doc);
677                }
678            }
679        }
680
681        // First pass: collect all export statements to track exported names
682        let mut exported_names = Vec::new();
683        let mut default_export_name = None;
684
685        for i in 0..root_node.child_count() {
686            if let Some(node) = root_node.child(i) {
687                if node.kind() == "export_statement" {
688                    // Direct exports should already be handled by parent check later, so focus on export blocks
689                    let node_text = node.utf8_text(source_bytes).unwrap_or("");
690
691                    // Handle named exports format: export { Name1, Name2 }
692                    if node_text.contains("{") && node_text.contains("}") {
693                        // Basic parsing of export statement text to extract names
694                        // For more complex cases, a proper structured parsing approach would be better
695                        if let Some(content) = node_text.split('{').nth(1) {
696                            if let Some(items) = content.split('}').next() {
697                                for item in items.split(',') {
698                                    let name = item.trim();
699                                    if !name.is_empty() {
700                                        exported_names.push(name.to_string());
701                                    }
702                                }
703                            }
704                        }
705                    }
706
707                    // Handle export default Name
708                    if node_text.starts_with("export default") {
709                        let parts: Vec<&str> = node_text.split_whitespace().collect();
710                        if parts.len() >= 3 {
711                            let default_name = parts[2].trim_end_matches(';').to_string();
712                            default_export_name = Some(default_name);
713                        }
714                    }
715                }
716            }
717        }
718
719        // Now traverse the tree to find declarations
720        for i in 0..root_node.child_count() {
721            if let Some(node) = root_node.child(i) {
722                match node.kind() {
723                    "function_declaration" => {
724                        // Check if this function is explicitly exported or referenced in an export statement
725                        let is_exported = node
726                            .parent()
727                            .is_some_and(|p| p.kind() == "export_statement")
728                            || if let Some(name_node) = node.child_by_field_name("name") {
729                                let name =
730                                    name_node.utf8_text(source_bytes).unwrap_or("").to_string();
731                                exported_names.contains(&name)
732                                    || default_export_name.as_ref() == Some(&name)
733                            } else {
734                                false
735                            };
736
737                        self.process_function(&mut file_unit, node, is_exported, source_bytes);
738                    }
739                    "lexical_declaration" => {
740                        for j in 0..node.child_count() {
741                            if let Some(var_node) = node.child(j) {
742                                if var_node.kind() == "variable_declarator" {
743                                    // Check if this variable is a function and if it's exported
744                                    let is_exported = if let Some(name_node) =
745                                        var_node.child_by_field_name("name")
746                                    {
747                                        let name = name_node
748                                            .utf8_text(source_bytes)
749                                            .unwrap_or("")
750                                            .to_string();
751                                        exported_names.contains(&name)
752                                            || default_export_name.as_ref() == Some(&name)
753                                    } else {
754                                        false
755                                    };
756
757                                    // Check if it's a function variable
758                                    for k in 0..var_node.child_count() {
759                                        if let Some(value_node) = var_node.child(k) {
760                                            if value_node.kind() == "arrow_function"
761                                                || value_node.kind() == "function_expression"
762                                            {
763                                                self.process_function_variable(
764                                                    &mut file_unit,
765                                                    node,
766                                                    var_node,
767                                                    is_exported,
768                                                    source_bytes,
769                                                );
770                                                break;
771                                            }
772                                        }
773                                    }
774                                }
775                            }
776                        }
777                    }
778                    "class_declaration" => {
779                        // Check if this class is explicitly exported or referenced in an export statement
780                        let is_exported = node
781                            .parent()
782                            .is_some_and(|p| p.kind() == "export_statement")
783                            || if let Some(name_node) = node.child_by_field_name("name") {
784                                let name =
785                                    name_node.utf8_text(source_bytes).unwrap_or("").to_string();
786                                exported_names.contains(&name)
787                                    || default_export_name.as_ref() == Some(&name)
788                            } else {
789                                false
790                            };
791
792                        self.process_class(&mut file_unit, node, is_exported, source_bytes);
793                    }
794                    "interface_declaration" => {
795                        // Check if this interface is explicitly exported or referenced in an export statement
796                        let is_exported = node
797                            .parent()
798                            .is_some_and(|p| p.kind() == "export_statement")
799                            || if let Some(name_node) = node.child_by_field_name("name") {
800                                let name =
801                                    name_node.utf8_text(source_bytes).unwrap_or("").to_string();
802                                exported_names.contains(&name)
803                                    || default_export_name.as_ref() == Some(&name)
804                            } else {
805                                false
806                            };
807
808                        self.process_interface(&mut file_unit, node, is_exported, source_bytes);
809                    }
810                    "type_alias_declaration" => {
811                        // Check if this type is explicitly exported or referenced in an export statement
812                        let is_exported = node
813                            .parent()
814                            .is_some_and(|p| p.kind() == "export_statement")
815                            || if let Some(name_node) = node.child_by_field_name("name") {
816                                let name =
817                                    name_node.utf8_text(source_bytes).unwrap_or("").to_string();
818                                exported_names.contains(&name)
819                                    || default_export_name.as_ref() == Some(&name)
820                            } else {
821                                false
822                            };
823
824                        self.process_type_alias(&mut file_unit, node, is_exported, source_bytes);
825                    }
826                    "enum_declaration" => {
827                        // Check if this enum is explicitly exported or referenced in an export statement
828                        let is_exported = node
829                            .parent()
830                            .is_some_and(|p| p.kind() == "export_statement")
831                            || if let Some(name_node) = node.child_by_field_name("name") {
832                                let name =
833                                    name_node.utf8_text(source_bytes).unwrap_or("").to_string();
834                                exported_names.contains(&name)
835                                    || default_export_name.as_ref() == Some(&name)
836                            } else {
837                                false
838                            };
839
840                        self.process_enum(&mut file_unit, node, is_exported, source_bytes);
841                    }
842                    "export_statement" => {
843                        self.process_export(&mut file_unit, node, source_bytes);
844                    }
845                    "import_statement" => {
846                        let source = node.utf8_text(source_bytes).unwrap_or("").to_string();
847                        file_unit.declares.push(DeclareStatements {
848                            source,
849                            kind: DeclareKind::Import,
850                        });
851                    }
852                    _ => {}
853                }
854            }
855        }
856
857        Ok(file_unit)
858    }
859}
860
861#[cfg(test)]
862mod tests {
863    use super::*;
864    use std::io::Write;
865    use tempfile::NamedTempFile;
866
867    fn parse_ts_str(ts_code: &str) -> Result<FileUnit> {
868        // Create a temporary file with the TypeScript code
869        let mut temp_file = NamedTempFile::new().unwrap();
870        write!(temp_file, "{}", ts_code).unwrap();
871        let path = temp_file.path().to_path_buf();
872
873        // Parse the file
874        let mut parser = TypeScriptParser::try_new()?;
875        parser.parse_file(&path)
876    }
877
878    #[test]
879    fn test_parse_function() -> Result<()> {
880        let ts_code = r#"
881        /**
882         * A function that adds two numbers.
883         * @param a First number
884         * @param b Second number
885         * @returns The sum of a and b
886         */
887        function add(a: number, b: number): number {
888            return a + b;
889        }
890        "#;
891
892        let file_unit = parse_ts_str(ts_code)?;
893
894        assert_eq!(file_unit.functions.len(), 1);
895        let func = &file_unit.functions[0];
896        assert_eq!(func.name, "add");
897        assert_eq!(func.visibility, Visibility::Private);
898        assert!(
899            func.doc
900                .as_ref()
901                .unwrap()
902                .contains("A function that adds two numbers")
903        );
904
905        Ok(())
906    }
907
908    #[test]
909    fn test_parse_exported_function() -> Result<()> {
910        let ts_code = r#"
911        /**
912         * An exported function.
913         */
914        export function multiply(a: number, b: number): number {
915            return a * b;
916        }
917        "#;
918
919        let file_unit = parse_ts_str(ts_code)?;
920
921        assert_eq!(file_unit.functions.len(), 1);
922        let func = &file_unit.functions[0];
923        assert_eq!(func.name, "multiply");
924        assert_eq!(func.visibility, Visibility::Public);
925        // Only check documentation if it exists
926        if let Some(doc) = &func.doc {
927            assert!(doc.contains("An exported function"));
928        }
929
930        Ok(())
931    }
932
933    #[test]
934    fn test_parse_function_variable() -> Result<()> {
935        let ts_code = r#"
936        /** Arrow function stored in a constant */
937        const arrowFunc = (a: number, b: number): number => a + b;
938        "#;
939
940        let file_unit = parse_ts_str(ts_code)?;
941
942        assert_eq!(file_unit.functions.len(), 1);
943        let func = &file_unit.functions[0];
944        assert_eq!(func.name, "arrowFunc");
945        assert_eq!(func.visibility, Visibility::Private);
946        if let Some(doc) = &func.doc {
947            assert!(doc.contains("Arrow function"));
948        }
949
950        Ok(())
951    }
952
953    #[test]
954    fn test_parse_class() -> Result<()> {
955        let ts_code = r#"
956        /**
957         * A person class.
958         */
959        class Person {
960            name: string;
961            age: number;
962
963            constructor(name: string, age: number) {
964                this.name = name;
965                this.age = age;
966            }
967
968            /**
969             * Get a greeting
970             */
971            greet(): string {
972                return `Hello, my name is ${this.name}`;
973            }
974        }
975        "#;
976
977        let file_unit = parse_ts_str(ts_code)?;
978
979        assert_eq!(file_unit.structs.len(), 1);
980        let class = &file_unit.structs[0];
981        assert_eq!(class.name, "Person");
982        assert_eq!(class.head, "class Person");
983        assert_eq!(class.visibility, Visibility::Private);
984        assert!(class.doc.as_ref().unwrap().contains("A person class"));
985
986        // TODO: When method extraction is implemented, test for those as well
987
988        Ok(())
989    }
990
991    #[test]
992    fn test_parse_interface() -> Result<()> {
993        let ts_code = r#"
994        /**
995         * Represents a shape.
996         */
997        export interface Shape {
998            area(): number;
999            perimeter(): number;
1000        }
1001        "#;
1002
1003        let file_unit = parse_ts_str(ts_code)?;
1004
1005        assert_eq!(file_unit.structs.len(), 1);
1006        let interface = &file_unit.structs[0];
1007        assert_eq!(interface.name, "Shape");
1008        assert_eq!(interface.head, "interface Shape");
1009        assert_eq!(interface.visibility, Visibility::Public);
1010        assert!(
1011            interface
1012                .doc
1013                .as_ref()
1014                .unwrap()
1015                .contains("Represents a shape")
1016        );
1017
1018        Ok(())
1019    }
1020
1021    #[test]
1022    fn test_parse_type_alias() -> Result<()> {
1023        let ts_code = r#"
1024        /** Represents a point in 2D space */
1025        type Point = {
1026            x: number;
1027            y: number;
1028        };
1029        "#;
1030
1031        let file_unit = parse_ts_str(ts_code)?;
1032
1033        assert_eq!(file_unit.structs.len(), 1);
1034        let type_alias = &file_unit.structs[0];
1035        assert_eq!(type_alias.name, "Point");
1036        assert_eq!(type_alias.head, "type Point");
1037        assert_eq!(type_alias.visibility, Visibility::Private);
1038        assert!(
1039            type_alias
1040                .doc
1041                .as_ref()
1042                .unwrap()
1043                .contains("Represents a point")
1044        );
1045
1046        Ok(())
1047    }
1048
1049    #[test]
1050    fn test_parse_enum() -> Result<()> {
1051        let ts_code = r#"
1052        /** Represents directions */
1053        enum Direction {
1054            North,
1055            East,
1056            South,
1057            West
1058        }
1059        "#;
1060
1061        let file_unit = parse_ts_str(ts_code)?;
1062
1063        assert_eq!(file_unit.structs.len(), 1);
1064        let enum_unit = &file_unit.structs[0];
1065        assert_eq!(enum_unit.name, "Direction");
1066        assert_eq!(enum_unit.head, "enum Direction");
1067        assert_eq!(enum_unit.visibility, Visibility::Private);
1068        assert!(
1069            enum_unit
1070                .doc
1071                .as_ref()
1072                .unwrap()
1073                .contains("Represents directions")
1074        );
1075
1076        Ok(())
1077    }
1078
1079    #[test]
1080    fn test_parse_imports_exports() -> Result<()> {
1081        let ts_code = r#"
1082        import { Component } from 'react';
1083        import * as util from './util';
1084
1085        export { add } from './math';
1086        export * from './helpers';
1087        "#;
1088
1089        let file_unit = parse_ts_str(ts_code)?;
1090
1091        // 2 imports + 2 exports
1092        assert_eq!(file_unit.declares.len(), 4);
1093
1094        // Check imports
1095        let mut found_imports = 0;
1096        for declare in &file_unit.declares {
1097            if let DeclareKind::Import = declare.kind {
1098                found_imports += 1;
1099                assert!(declare.source.contains("import"));
1100            }
1101        }
1102        assert_eq!(found_imports, 2);
1103
1104        // Check exports
1105        let mut found_exports = 0;
1106        for declare in &file_unit.declares {
1107            if let DeclareKind::Other(ref kind) = declare.kind {
1108                if kind == "export" {
1109                    found_exports += 1;
1110                    assert!(declare.source.contains("export"));
1111                }
1112            }
1113        }
1114        assert_eq!(found_exports, 2);
1115
1116        Ok(())
1117    }
1118
1119    #[test]
1120    fn test_parse_file_doc_comment() -> Result<()> {
1121        let ts_code = r#"/**
1122         * This is a file-level documentation comment.
1123         * @fileoverview Example TypeScript file
1124         */
1125
1126        const x = 1;
1127        "#;
1128
1129        let file_unit = parse_ts_str(ts_code)?;
1130
1131        assert!(file_unit.doc.is_some());
1132        assert!(
1133            file_unit
1134                .doc
1135                .as_ref()
1136                .unwrap()
1137                .contains("file-level documentation")
1138        );
1139        assert!(file_unit.doc.as_ref().unwrap().contains("@fileoverview"));
1140
1141        Ok(())
1142    }
1143
1144    #[test]
1145    fn test_complex_file() -> Result<()> {
1146        let ts_code = r#"/**
1147         * A complex TypeScript file with multiple declarations.
1148         * @fileoverview Test for parser
1149         */
1150
1151        import { useState, useEffect } from 'react';
1152
1153        /**
1154         * Interface for user data
1155         */
1156        export interface User {
1157            id: number;
1158            name: string;
1159            email: string;
1160        }
1161
1162        /**
1163         * Type for API response
1164         */
1165        type ApiResponse<T> = {
1166            data: T;
1167            status: number;
1168            message: string;
1169        };
1170
1171        /** Get a user by ID */
1172        export async function getUser(id: number): Promise<User> {
1173            // Implementation
1174            return { id, name: 'Test', email: 'test@example.com' };
1175        }
1176
1177        /** User class implementation */
1178        class UserImpl implements User {
1179            id: number;
1180            name: string;
1181            email: string;
1182
1183            constructor(id: number, name: string, email: string) {
1184                this.id = id;
1185                this.name = name;
1186                this.email = email;
1187            }
1188
1189            toString() {
1190                return `User(${this.id}): ${this.name}`;
1191            }
1192        }
1193
1194        /** Status enumeration */
1195        enum Status {
1196            Active = 'active',
1197            Inactive = 'inactive',
1198            Pending = 'pending'
1199        }
1200
1201        /** Create a formatter function */
1202        export const formatUser = (user: User): string => {
1203            return `${user.name} <${user.email}>`;
1204        };
1205        "#;
1206
1207        let file_unit = parse_ts_str(ts_code)?;
1208
1209        // Check all expected components
1210        assert!(file_unit.doc.is_some());
1211        assert_eq!(file_unit.declares.len(), 1); // One import
1212        assert_eq!(file_unit.functions.len(), 2); // getUser and formatUser
1213        assert_eq!(file_unit.structs.len(), 4); // User interface, ApiResponse type, UserImpl class, Status enum
1214
1215        // Verify exported items have Public visibility
1216        let mut exported_count = 0;
1217        for func in &file_unit.functions {
1218            if func.visibility == Visibility::Public {
1219                exported_count += 1;
1220            }
1221        }
1222        for struct_item in &file_unit.structs {
1223            if struct_item.visibility == Visibility::Public {
1224                exported_count += 1;
1225            }
1226        }
1227        assert_eq!(exported_count, 3); // User interface, getUser function, formatUser constant
1228
1229        Ok(())
1230    }
1231
1232    #[test]
1233    fn test_function_signatures() -> Result<()> {
1234        let ts_code = r#"
1235        // Regular function
1236        function publicFunction(param: string): string {
1237            return `Hello ${param}`;
1238        }
1239
1240        // Arrow function with type
1241        const arrowFunc = (x: number, y: number): number => x + y;
1242
1243        // Public arrow function
1244        const publicArrowFunction = (param: string): string => `Hello ${param}`;
1245
1246        // Private arrow function
1247        const _privateArrowFunction = (): number => 42;
1248
1249        // Function with multiple parameters and return type
1250        function complexFunc(
1251            name: string,
1252            age: number,
1253            options?: { debug: boolean }
1254        ): Promise<Record<string, unknown>> {
1255            return Promise.resolve({ name, age });
1256        }
1257        "#;
1258
1259        let file_unit = parse_ts_str(ts_code)?;
1260
1261        assert_eq!(file_unit.functions.len(), 5);
1262
1263        // Check regular function
1264        let func = &file_unit.functions[0];
1265        assert_eq!(func.name, "publicFunction");
1266        assert!(
1267            func.signature
1268                .as_ref()
1269                .unwrap()
1270                .contains("function publicFunction(param: string): string")
1271        );
1272
1273        // Check arrow function
1274        let arrow = &file_unit.functions[1];
1275        assert_eq!(arrow.name, "arrowFunc");
1276        assert_eq!(
1277            arrow.signature.as_ref().unwrap(),
1278            "arrowFunc(x: number, y: number): number"
1279        );
1280
1281        // Check public arrow function
1282        let public_arrow = &file_unit.functions[2];
1283        assert_eq!(public_arrow.name, "publicArrowFunction");
1284        assert_eq!(
1285            public_arrow.signature.as_ref().unwrap(),
1286            "publicArrowFunction(param: string): string"
1287        );
1288
1289        // Check private arrow function
1290        let private_arrow = &file_unit.functions[3];
1291        assert_eq!(private_arrow.name, "_privateArrowFunction");
1292        assert_eq!(
1293            private_arrow.signature.as_ref().unwrap(),
1294            "_privateArrowFunction(): number"
1295        );
1296
1297        // Check complex function
1298        let complex = &file_unit.functions[4];
1299        assert_eq!(complex.name, "complexFunc");
1300        assert!(
1301            complex
1302                .signature
1303                .as_ref()
1304                .unwrap()
1305                .contains("function complexFunc(")
1306        );
1307        assert!(
1308            complex
1309                .signature
1310                .as_ref()
1311                .unwrap()
1312                .contains("): Promise<Record<string, unknown>>")
1313        );
1314
1315        Ok(())
1316    }
1317
1318    #[test]
1319    fn test_class_methods() -> Result<()> {
1320        let ts_code = r#"
1321        class PublicClass extends BaseClass implements PublicInterface {
1322          public publicField: string;
1323          private _privateField: number;
1324
1325          constructor(publicField: string, privateField: number) {
1326            super();
1327            this.publicField = publicField;
1328            this._privateField = privateField;
1329          }
1330
1331          public publicMethod(param: string): string {
1332            return `Hello ${param}`;
1333          }
1334
1335          private _privateMethod(): number {
1336            return this._privateField;
1337          }
1338
1339          abstractMethod(): void {
1340            console.log("Implemented abstract method");
1341          }
1342        }
1343
1344        class GenericClass<T> {
1345          constructor(private value: T) { }
1346
1347          getValue(): T {
1348            return this.value;
1349          }
1350        }
1351
1352        @decorator
1353        class DecoratedClass {
1354          @decorator
1355          decoratedMethod() { }
1356        }
1357        "#;
1358
1359        let file_unit = parse_ts_str(ts_code)?;
1360
1361        assert_eq!(file_unit.structs.len(), 3);
1362
1363        // Check PublicClass
1364        let public_class = &file_unit.structs[0];
1365        assert_eq!(public_class.name, "PublicClass");
1366        assert_eq!(public_class.methods.len(), 4);
1367
1368        // Check constructor
1369        let constructor = public_class
1370            .methods
1371            .iter()
1372            .find(|m| m.name == "constructor")
1373            .unwrap();
1374        assert!(
1375            constructor
1376                .signature
1377                .as_ref()
1378                .unwrap()
1379                .starts_with("constructor(")
1380        );
1381
1382        // Check public method
1383        let public_method = public_class
1384            .methods
1385            .iter()
1386            .find(|m| m.name == "publicMethod")
1387            .unwrap();
1388        assert_eq!(
1389            public_method.signature.as_ref().unwrap(),
1390            "public publicMethod(param: string): string"
1391        );
1392
1393        // Check private method
1394        let private_method = public_class
1395            .methods
1396            .iter()
1397            .find(|m| m.name == "_privateMethod")
1398            .unwrap();
1399        assert_eq!(
1400            private_method.signature.as_ref().unwrap(),
1401            "private _privateMethod(): number"
1402        );
1403
1404        // Check unmodified method
1405        let abstract_method = public_class
1406            .methods
1407            .iter()
1408            .find(|m| m.name == "abstractMethod")
1409            .unwrap();
1410        assert_eq!(
1411            abstract_method.signature.as_ref().unwrap(),
1412            "abstractMethod(): void"
1413        );
1414
1415        // Check GenericClass
1416        let generic_class = &file_unit.structs[1];
1417        assert_eq!(generic_class.name, "GenericClass");
1418        assert_eq!(generic_class.methods.len(), 2);
1419
1420        // Check getter method
1421        let get_value = generic_class
1422            .methods
1423            .iter()
1424            .find(|m| m.name == "getValue")
1425            .unwrap();
1426        assert_eq!(get_value.signature.as_ref().unwrap(), "getValue(): T");
1427
1428        // Check DecoratedClass
1429        let decorated_class = &file_unit.structs[2];
1430        assert_eq!(decorated_class.name, "DecoratedClass");
1431        assert_eq!(decorated_class.methods.len(), 1);
1432
1433        // Check decorated method
1434        let decorated_method = decorated_class
1435            .methods
1436            .iter()
1437            .find(|m| m.name == "decoratedMethod")
1438            .unwrap();
1439        assert_eq!(
1440            decorated_method.signature.as_ref().unwrap(),
1441            "decoratedMethod()"
1442        );
1443
1444        Ok(())
1445    }
1446
1447    #[test]
1448    fn test_exports_and_visibility() -> Result<()> {
1449        let ts_code = r#"
1450        // Class with methods that have different visibility modifiers
1451        class PublicClass {
1452            // This should be public by default
1453            defaultMethod() {
1454                return "default visibility is public";
1455            }
1456
1457            // Explicitly public
1458            public publicMethod() {
1459                return "explicitly public";
1460            }
1461
1462            // Explicitly private
1463            private privateMethod() {
1464                return "explicitly private";
1465            }
1466        }
1467
1468        // Interface with method declarations (all public by default)
1469        interface PublicInterface {
1470            methodOne(): string;
1471            methodTwo(): number;
1472        }
1473
1474        // Enum definition
1475        enum PublicEnum {
1476            ONE,
1477            TWO,
1478            THREE
1479        }
1480
1481        // Function definition
1482        function publicFunction() {
1483            return "I'm a function";
1484        }
1485
1486        // Export statements
1487        export { PublicClass, PublicInterface, PublicEnum, publicFunction };
1488        export default PublicClass;
1489        "#;
1490
1491        let file_unit = parse_ts_str(ts_code)?;
1492
1493        // Check total counts
1494        assert_eq!(file_unit.functions.len(), 1);
1495        assert_eq!(file_unit.structs.len(), 3); // class, interface, and enum
1496        assert_eq!(file_unit.declares.len(), 2); // Two export statements
1497
1498        // Check that all exported items are public
1499        let function = &file_unit.functions[0];
1500        assert_eq!(function.name, "publicFunction");
1501        assert_eq!(function.visibility, Visibility::Public);
1502
1503        // Find PublicClass and check its visibility and methods
1504        let public_class = file_unit
1505            .structs
1506            .iter()
1507            .find(|s| s.name == "PublicClass")
1508            .unwrap();
1509        assert_eq!(public_class.visibility, Visibility::Public);
1510        assert_eq!(public_class.methods.len(), 3);
1511
1512        // Check method visibility
1513        let default_method = public_class
1514            .methods
1515            .iter()
1516            .find(|m| m.name == "defaultMethod")
1517            .unwrap();
1518        assert_eq!(default_method.visibility, Visibility::Public);
1519
1520        let public_method = public_class
1521            .methods
1522            .iter()
1523            .find(|m| m.name == "publicMethod")
1524            .unwrap();
1525        assert_eq!(public_method.visibility, Visibility::Public);
1526
1527        let private_method = public_class
1528            .methods
1529            .iter()
1530            .find(|m| m.name == "privateMethod")
1531            .unwrap();
1532        assert_eq!(private_method.visibility, Visibility::Private);
1533
1534        // Find PublicInterface and check its visibility
1535        let public_interface = file_unit
1536            .structs
1537            .iter()
1538            .find(|s| s.name == "PublicInterface")
1539            .unwrap();
1540        assert_eq!(public_interface.visibility, Visibility::Public);
1541
1542        // Find PublicEnum and check its visibility
1543        let public_enum = file_unit
1544            .structs
1545            .iter()
1546            .find(|s| s.name == "PublicEnum")
1547            .unwrap();
1548        assert_eq!(public_enum.visibility, Visibility::Public);
1549
1550        Ok(())
1551    }
1552
1553    #[test]
1554    fn test_class_with_fields() -> Result<()> {
1555        let ts_code = r#"
1556        /** Class with fields */
1557        class DataStore {
1558            /** The main data */
1559            public data: Map<string, number>;
1560
1561            private _initialized: boolean = false;
1562
1563            readonly creationTime: Date;
1564
1565            constructor() {
1566                this.data = new Map();
1567                this.creationTime = new Date();
1568            }
1569        }
1570        "#;
1571
1572        let file_unit = parse_ts_str(ts_code)?;
1573        assert_eq!(file_unit.structs.len(), 1);
1574        let class = &file_unit.structs[0];
1575        assert_eq!(class.name, "DataStore");
1576        assert_eq!(class.fields.len(), 3);
1577
1578        let data_field = class.fields.iter().find(|f| f.name == "data").unwrap();
1579        assert_eq!(data_field.name, "data");
1580        assert!(data_field.doc.as_ref().unwrap().contains("The main data"));
1581        assert!(
1582            data_field
1583                .source
1584                .as_ref()
1585                .unwrap()
1586                .contains("public data: Map<string, number>")
1587        );
1588
1589        let init_field = class
1590            .fields
1591            .iter()
1592            .find(|f| f.name == "_initialized")
1593            .unwrap();
1594        assert_eq!(init_field.name, "_initialized");
1595        assert!(init_field.doc.is_none());
1596        assert!(
1597            init_field
1598                .source
1599                .as_ref()
1600                .unwrap()
1601                .contains("private _initialized: boolean")
1602        );
1603
1604        let time_field = class
1605            .fields
1606            .iter()
1607            .find(|f| f.name == "creationTime")
1608            .unwrap();
1609        assert_eq!(time_field.name, "creationTime");
1610        assert!(time_field.doc.is_none());
1611        assert!(
1612            time_field
1613                .source
1614                .as_ref()
1615                .unwrap()
1616                .contains("readonly creationTime: Date")
1617        );
1618
1619        Ok(())
1620    }
1621
1622    #[test]
1623    fn test_interface_with_fields() -> Result<()> {
1624        let ts_code = r#"
1625        /** Interface with properties */
1626        interface Config {
1627            /** API endpoint URL */
1628            readonly apiUrl: string;
1629            timeout?: number; // Optional property
1630            retries: number;
1631        }
1632        "#;
1633
1634        let file_unit = parse_ts_str(ts_code)?;
1635        assert_eq!(file_unit.structs.len(), 1);
1636        let interface = &file_unit.structs[0];
1637        assert_eq!(interface.name, "Config");
1638        assert_eq!(interface.fields.len(), 3);
1639
1640        let api_field = interface
1641            .fields
1642            .iter()
1643            .find(|f| f.name == "apiUrl")
1644            .unwrap();
1645        assert_eq!(api_field.name, "apiUrl");
1646        assert!(api_field.doc.as_ref().unwrap().contains("API endpoint URL"));
1647        assert!(
1648            api_field
1649                .source
1650                .as_ref()
1651                .unwrap()
1652                .contains("readonly apiUrl: string")
1653        );
1654
1655        let timeout_field = interface
1656            .fields
1657            .iter()
1658            .find(|f| f.name == "timeout")
1659            .unwrap();
1660        assert_eq!(timeout_field.name, "timeout");
1661        assert!(timeout_field.doc.is_none());
1662        assert!(
1663            timeout_field
1664                .source
1665                .as_ref()
1666                .unwrap()
1667                .contains("timeout?: number")
1668        );
1669
1670        let retries_field = interface
1671            .fields
1672            .iter()
1673            .find(|f| f.name == "retries")
1674            .unwrap();
1675        assert_eq!(retries_field.name, "retries");
1676        assert!(retries_field.doc.is_none());
1677        assert!(
1678            retries_field
1679                .source
1680                .as_ref()
1681                .unwrap()
1682                .contains("retries: number")
1683        );
1684
1685        Ok(())
1686    }
1687}