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 fn process_export(&self, file_unit: &mut FileUnit, node: Node, source: &[u8]) {
24 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 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 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 let documentation = find_documentation_for_node(node, source);
93
94 let mut signature = String::from("function ");
96 signature.push_str(&name);
97
98 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 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 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 let documentation = find_documentation_for_node(decl_node, source);
140
141 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 if value_node.kind() == "arrow_function" {
152 sig.push_str(&name);
153
154 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 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 } else {
166 sig.push_str("function ");
168 sig.push_str(&name);
169
170 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 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 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 let documentation = find_documentation_for_node(node, source);
216
217 let mut fields = Vec::new();
219 let mut methods = Vec::new();
220
221 if let Some(body_node) = node.child_by_field_name("body") {
223 for i in 0..body_node.child_count() {
225 if let Some(method_node) = body_node.child(i) {
226 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 let mut signature = String::new();
239
240 let mut method_visibility = Visibility::Public;
242
243 if method_node.kind() == "constructor_definition" {
245 signature.push_str("constructor");
246 } else {
247 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 if modifier_text == "private" {
258 method_visibility = Visibility::Private;
259 }
260 break;
261 }
262 }
263 }
264
265 signature.push_str(&method_name);
267 }
268
269 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 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 methods.push(FunctionUnit {
287 name: method_name,
288 source: Some(method_source),
289 visibility: method_visibility,
290 doc: None, signature: Some(signature),
292 body: None,
293 attributes: vec![],
294 });
295 }
296 }
297 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 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 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 let documentation = find_documentation_for_node(node, source);
351
352 let mut fields = Vec::new();
354 let mut methods = Vec::new();
355
356 if let Some(body_node) = node.child_by_field_name("body") {
358 for i in 0..body_node.child_count() {
360 if let Some(method_node) = body_node.child(i) {
361 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 let mut signature = String::new();
372
373 signature.push_str(&method_name);
375
376 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 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 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 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 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 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 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
497fn find_documentation_for_node(node: Node, source: &[u8]) -> Option<String> {
501 let mut current_node = node;
502
503 while let Some(prev) = current_node.prev_sibling() {
505 if prev.kind() == "comment" {
507 let text = prev.utf8_text(source).ok()?;
508 if text.starts_with("/**") {
510 if prev.end_byte() == current_node.start_byte() - 1 || (prev.end_byte() < current_node.start_byte() && source[prev.end_byte()..current_node.start_byte()].iter().all(|&b| b.is_ascii_whitespace()))
515 {
516 return extract_doc_comment(prev, source); }
518 break;
521 }
522 }
523 else if !prev.is_extra() {
525 break;
526 }
527
528 current_node = prev;
530 }
531
532 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; }
551 } else if !prev.is_extra() {
552 break; }
554 current_node = prev;
555 }
556 }
557 }
558
559 None }
561
562fn 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 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#[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#[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 if !prev.is_extra() {
621 break;
623 }
624 current = prev;
625 }
626
627 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 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 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 let node_text = node.utf8_text(source_bytes).unwrap_or("");
690
691 if node_text.contains("{") && node_text.contains("}") {
693 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(file_unit.declares.len(), 4);
1093
1094 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 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 assert!(file_unit.doc.is_some());
1211 assert_eq!(file_unit.declares.len(), 1); assert_eq!(file_unit.functions.len(), 2); assert_eq!(file_unit.structs.len(), 4); 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); 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 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 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 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 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 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 let public_class = &file_unit.structs[0];
1365 assert_eq!(public_class.name, "PublicClass");
1366 assert_eq!(public_class.methods.len(), 4);
1367
1368 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 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 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 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 let generic_class = &file_unit.structs[1];
1417 assert_eq!(generic_class.name, "GenericClass");
1418 assert_eq!(generic_class.methods.len(), 2);
1419
1420 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 let decorated_class = &file_unit.structs[2];
1430 assert_eq!(decorated_class.name, "DecoratedClass");
1431 assert_eq!(decorated_class.methods.len(), 1);
1432
1433 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 assert_eq!(file_unit.functions.len(), 1);
1495 assert_eq!(file_unit.structs.len(), 3); assert_eq!(file_unit.declares.len(), 2); let function = &file_unit.functions[0];
1500 assert_eq!(function.name, "publicFunction");
1501 assert_eq!(function.visibility, Visibility::Public);
1502
1503 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 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 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 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}