1use std::cell::RefCell;
2use std::path::Path;
3
4use tree_sitter::{Node, Parser};
5use tree_sitter_language::LanguageFn;
6
7use domain::error::CodeGraphError;
8use domain::model::{Edge, EdgeKind, Language, Location, SymbolKind, SymbolNode, Visibility};
9
10use crate::{ImportName, LanguageParser, ParseResult, RawImport};
11
12thread_local! {
14 static TS_PARSER: RefCell<Parser> = RefCell::new(Parser::new());
15}
16
17fn parse_with_grammar(
19 source: &[u8],
20 path: &Path,
21 lang_fn: LanguageFn,
22) -> domain::error::Result<ParseResult> {
23 let lang: tree_sitter::Language = lang_fn.into();
24
25 TS_PARSER.with(|parser_cell| {
26 let mut parser = parser_cell.borrow_mut();
27 parser
28 .set_language(&lang)
29 .map_err(|e| CodeGraphError::Parse {
30 file: path.to_path_buf(),
31 message: format!("failed to set language: {e}"),
32 })?;
33
34 let tree = parser
35 .parse(source, None)
36 .ok_or_else(|| CodeGraphError::Parse {
37 file: path.to_path_buf(),
38 message: "tree-sitter parse returned None".into(),
39 })?;
40
41 extract_all(source, path, &tree)
42 })
43}
44
45pub struct TypeScriptParser {
51 ts_language: LanguageFn,
52 tsx_language: LanguageFn,
53}
54
55impl TypeScriptParser {
56 pub fn new() -> Self {
57 Self {
58 ts_language: tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
59 tsx_language: tree_sitter_typescript::LANGUAGE_TSX,
60 }
61 }
62
63 fn language_fn_for_path(&self, path: &Path) -> LanguageFn {
64 match path.extension().and_then(|e| e.to_str()) {
65 Some("tsx") => self.tsx_language,
66 _ => self.ts_language,
67 }
68 }
69}
70
71impl Default for TypeScriptParser {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl LanguageParser for TypeScriptParser {
78 fn language(&self) -> Language {
79 Language::TypeScript
80 }
81
82 fn file_extensions(&self) -> &[&str] {
83 &["ts", "tsx"]
84 }
85
86 fn parse(&self, source: &[u8], path: &Path) -> domain::error::Result<ParseResult> {
87 parse_with_grammar(source, path, self.language_fn_for_path(path))
88 }
89}
90
91pub struct JavaScriptParser {
97 js_language: LanguageFn,
98 jsx_language: LanguageFn,
99}
100
101impl JavaScriptParser {
102 pub fn new() -> Self {
103 Self {
104 js_language: tree_sitter_javascript::LANGUAGE,
105 jsx_language: tree_sitter_typescript::LANGUAGE_TSX,
106 }
107 }
108
109 fn language_fn_for_path(&self, path: &Path) -> LanguageFn {
110 match path.extension().and_then(|e| e.to_str()) {
111 Some("jsx") => self.jsx_language,
112 _ => self.js_language,
113 }
114 }
115}
116
117impl Default for JavaScriptParser {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123impl LanguageParser for JavaScriptParser {
124 fn language(&self) -> Language {
125 Language::JavaScript
126 }
127
128 fn file_extensions(&self) -> &[&str] {
129 &["js", "jsx"]
130 }
131
132 fn parse(&self, source: &[u8], path: &Path) -> domain::error::Result<ParseResult> {
133 parse_with_grammar(source, path, self.language_fn_for_path(path))
134 }
135}
136
137fn extract_all(
142 source: &[u8],
143 path: &Path,
144 tree: &tree_sitter::Tree,
145) -> domain::error::Result<ParseResult> {
146 let mut symbols = Vec::new();
147 let mut edges = Vec::new();
148 let file_path = path.to_string_lossy().to_string();
149 let root = tree.root_node();
150 let mut cursor = root.walk();
151
152 for child in root.children(&mut cursor) {
153 if !child.is_named() {
154 continue;
155 }
156 match child.kind() {
157 "export_statement" => {
158 let is_default = {
160 let mut dc = child.walk();
161 let result = child
162 .children(&mut dc)
163 .any(|c| !c.is_named() && c.kind() == "default");
164 result
165 };
166
167 let mut found_declaration = false;
168 let mut inner_cursor = child.walk();
169 for inner in child.children(&mut inner_cursor) {
170 if !inner.is_named() {
171 continue;
172 }
173 if is_declaration_kind(inner.kind()) {
174 found_declaration = true;
175 extract_declaration(
176 source,
177 &file_path,
178 inner,
179 true,
180 &mut symbols,
181 &mut edges,
182 );
183 if is_default && symbol_name(inner, source).is_none() {
185 let qn = format!("{file_path}::default");
186 symbols.push(SymbolNode {
187 name: "default".to_string(),
188 qualified_name: qn.clone(),
189 kind: match inner.kind() {
190 "function_declaration" => SymbolKind::Function,
191 "class_declaration" | "abstract_class_declaration" => {
192 SymbolKind::Class
193 }
194 _ => SymbolKind::Variable,
195 },
196 location: node_location(&file_path, inner),
197 visibility: Visibility::Public,
198 is_exported: true,
199 is_async: has_async_keyword(inner),
200 is_test: false,
201 decorators: Vec::new(),
202 signature: build_signature(source, inner),
203 });
204 edges.push(contains_edge(&file_path, &qn));
205 }
206 }
207 }
208
209 if is_default && !found_declaration {
211 if let Some(value) = child.child_by_field_name("value") {
212 let kind = match value.kind() {
213 "function_expression" | "arrow_function" => Some(SymbolKind::Function),
214 "class" => Some(SymbolKind::Class),
215 _ => None,
216 };
217 if let Some(sym_kind) = kind {
218 let qn = format!("{file_path}::default");
219 symbols.push(SymbolNode {
220 name: "default".to_string(),
221 qualified_name: qn.clone(),
222 kind: sym_kind,
223 location: node_location(&file_path, value),
224 visibility: Visibility::Public,
225 is_exported: true,
226 is_async: has_async_keyword(value),
227 is_test: false,
228 decorators: Vec::new(),
229 signature: build_signature(source, value),
230 });
231 edges.push(contains_edge(&file_path, &qn));
232 }
233 }
234 }
235 }
236 kind if is_declaration_kind(kind) => {
237 extract_declaration(source, &file_path, child, false, &mut symbols, &mut edges);
238 }
239 _ => {}
240 }
241 }
242
243 let imports = extract_imports(&root, source);
244 let exports = extract_exports(&root, source);
245
246 Ok(ParseResult {
247 symbols,
248 edges,
249 imports,
250 exports,
251 })
252}
253
254fn is_declaration_kind(kind: &str) -> bool {
259 matches!(
260 kind,
261 "function_declaration"
262 | "class_declaration"
263 | "abstract_class_declaration"
264 | "interface_declaration"
265 | "type_alias_declaration"
266 | "enum_declaration"
267 | "lexical_declaration"
268 | "variable_declaration"
269 )
270}
271
272fn extract_declaration(
274 source: &[u8],
275 file_path: &str,
276 node: Node,
277 is_exported: bool,
278 symbols: &mut Vec<SymbolNode>,
279 edges: &mut Vec<Edge>,
280) {
281 match node.kind() {
282 "function_declaration" => {
283 if let Some(sym) = extract_function(source, file_path, node, is_exported) {
284 edges.push(contains_edge(file_path, &sym.qualified_name));
285 symbols.push(sym);
286 }
287 }
288 "class_declaration" | "abstract_class_declaration" => {
289 extract_class(source, file_path, node, is_exported, symbols, edges);
290 }
291 "interface_declaration" => {
292 extract_interface(source, file_path, node, is_exported, symbols, edges);
293 }
294 "type_alias_declaration" => {
295 if let Some(sym) = extract_type_alias(source, file_path, node, is_exported) {
296 edges.push(contains_edge(file_path, &sym.qualified_name));
297 symbols.push(sym);
298 }
299 }
300 "enum_declaration" => {
301 if let Some(sym) = extract_enum(source, file_path, node, is_exported) {
302 edges.push(contains_edge(file_path, &sym.qualified_name));
303 symbols.push(sym);
304 }
305 }
306 "lexical_declaration" | "variable_declaration" => {
307 extract_variable_declaration(source, file_path, node, is_exported, symbols, edges);
308 }
309 _ => {}
310 }
311}
312
313fn extract_function(
314 source: &[u8],
315 file_path: &str,
316 node: Node,
317 is_exported: bool,
318) -> Option<SymbolNode> {
319 let name = symbol_name(node, source)?;
320 let qualified_name = format!("{file_path}::{name}");
321 let is_async = has_async_keyword(node);
322 let signature = build_signature(source, node);
323 let decorators = extract_decorators(source, node);
324
325 Some(SymbolNode {
326 name: name.clone(),
327 qualified_name,
328 kind: SymbolKind::Function,
329 location: node_location(file_path, node),
330 visibility: export_visibility(is_exported),
331 is_exported,
332 is_async,
333 is_test: is_test_name(&name),
334 decorators,
335 signature,
336 })
337}
338
339fn extract_class(
340 source: &[u8],
341 file_path: &str,
342 node: Node,
343 is_exported: bool,
344 symbols: &mut Vec<SymbolNode>,
345 edges: &mut Vec<Edge>,
346) {
347 let name = match symbol_name(node, source) {
348 Some(n) => n,
349 None => return,
350 };
351 let qualified_name = format!("{file_path}::{name}");
352 let decorators = extract_decorators(source, node);
353
354 let class_sym = SymbolNode {
355 name: name.clone(),
356 qualified_name: qualified_name.clone(),
357 kind: SymbolKind::Class,
358 location: node_location(file_path, node),
359 visibility: export_visibility(is_exported),
360 is_exported,
361 is_async: false,
362 is_test: is_test_name(&name),
363 decorators,
364 signature: None,
365 };
366 edges.push(contains_edge(file_path, &class_sym.qualified_name));
367 symbols.push(class_sym);
368
369 if let Some(body) = node.child_by_field_name("body") {
371 let mut body_cursor = body.walk();
372 for member in body.children(&mut body_cursor) {
373 if !member.is_named() {
374 continue;
375 }
376 extract_class_member(
377 source,
378 file_path,
379 &name,
380 &qualified_name,
381 member,
382 is_exported,
383 symbols,
384 edges,
385 );
386 }
387 }
388}
389
390#[allow(clippy::too_many_arguments)]
391fn extract_class_member(
392 source: &[u8],
393 file_path: &str,
394 class_name: &str,
395 class_qualified_name: &str,
396 member: Node,
397 is_exported: bool,
398 symbols: &mut Vec<SymbolNode>,
399 edges: &mut Vec<Edge>,
400) {
401 let (kind, member_name) = match member.kind() {
402 "method_definition" => (SymbolKind::Method, symbol_name(member, source)),
403 "public_field_definition" => {
404 let n = member
406 .child_by_field_name("name")
407 .and_then(|n| n.utf8_text(source).ok())
408 .map(|s| s.to_string());
409 (SymbolKind::Property, n)
410 }
411 "field_definition" => {
412 let n = member
414 .child_by_field_name("property")
415 .and_then(|n| n.utf8_text(source).ok())
416 .map(|s| s.to_string());
417 (SymbolKind::Property, n)
418 }
419 _ => return,
420 };
421
422 let member_name = match member_name {
423 Some(n) => n,
424 None => return,
425 };
426
427 let member_qualified = format!("{file_path}::{class_name}.{member_name}");
428 let is_async = has_async_keyword(member);
429 let signature = if kind == SymbolKind::Method {
430 build_signature(source, member)
431 } else {
432 None
433 };
434 let decorators = extract_decorators(source, member);
435
436 let sym = SymbolNode {
437 name: member_name.clone(),
438 qualified_name: member_qualified.clone(),
439 kind,
440 location: node_location(file_path, member),
441 visibility: export_visibility(is_exported),
442 is_exported,
443 is_async,
444 is_test: is_test_name(&member_name),
445 decorators,
446 signature,
447 };
448 symbols.push(sym);
449 edges.push(Edge {
450 kind: EdgeKind::ChildOf,
451 source: member_qualified,
452 target: class_qualified_name.to_string(),
453 metadata: None,
454 });
455}
456
457fn extract_interface(
458 source: &[u8],
459 file_path: &str,
460 node: Node,
461 is_exported: bool,
462 symbols: &mut Vec<SymbolNode>,
463 edges: &mut Vec<Edge>,
464) {
465 let name = match symbol_name(node, source) {
466 Some(n) => n,
467 None => return,
468 };
469 let qualified_name = format!("{file_path}::{name}");
470
471 let iface_sym = SymbolNode {
472 name: name.clone(),
473 qualified_name: qualified_name.clone(),
474 kind: SymbolKind::Interface,
475 location: node_location(file_path, node),
476 visibility: export_visibility(is_exported),
477 is_exported,
478 is_async: false,
479 is_test: false,
480 decorators: Vec::new(),
481 signature: None,
482 };
483 edges.push(contains_edge(file_path, &iface_sym.qualified_name));
484 symbols.push(iface_sym);
485
486 if let Some(body) = node.child_by_field_name("body") {
488 let mut body_cursor = body.walk();
489 for member in body.children(&mut body_cursor) {
490 if !member.is_named() {
491 continue;
492 }
493 extract_interface_member(
494 source,
495 file_path,
496 &name,
497 &qualified_name,
498 member,
499 is_exported,
500 symbols,
501 edges,
502 );
503 }
504 }
505}
506
507#[allow(clippy::too_many_arguments)]
508fn extract_interface_member(
509 source: &[u8],
510 file_path: &str,
511 iface_name: &str,
512 iface_qualified_name: &str,
513 member: Node,
514 is_exported: bool,
515 symbols: &mut Vec<SymbolNode>,
516 edges: &mut Vec<Edge>,
517) {
518 let (kind, member_name) = match member.kind() {
519 "property_signature" => {
520 let n = member
521 .child_by_field_name("name")
522 .and_then(|n| n.utf8_text(source).ok())
523 .map(|s| s.to_string());
524 (SymbolKind::Property, n)
525 }
526 "method_signature" => {
527 let n = symbol_name(member, source);
528 (SymbolKind::Method, n)
529 }
530 _ => return,
531 };
532
533 let member_name = match member_name {
534 Some(n) => n,
535 None => return,
536 };
537
538 let member_qualified = format!("{file_path}::{iface_name}.{member_name}");
539 let signature = if kind == SymbolKind::Method {
540 build_signature(source, member)
541 } else {
542 None
543 };
544
545 let sym = SymbolNode {
546 name: member_name.clone(),
547 qualified_name: member_qualified.clone(),
548 kind,
549 location: node_location(file_path, member),
550 visibility: export_visibility(is_exported),
551 is_exported,
552 is_async: false,
553 is_test: false,
554 decorators: Vec::new(),
555 signature,
556 };
557 symbols.push(sym);
558 edges.push(Edge {
559 kind: EdgeKind::ChildOf,
560 source: member_qualified,
561 target: iface_qualified_name.to_string(),
562 metadata: None,
563 });
564}
565
566fn extract_type_alias(
567 source: &[u8],
568 file_path: &str,
569 node: Node,
570 is_exported: bool,
571) -> Option<SymbolNode> {
572 let name = symbol_name(node, source)?;
573 let qualified_name = format!("{file_path}::{name}");
574
575 Some(SymbolNode {
576 name: name.clone(),
577 qualified_name,
578 kind: SymbolKind::TypeAlias,
579 location: node_location(file_path, node),
580 visibility: export_visibility(is_exported),
581 is_exported,
582 is_async: false,
583 is_test: false,
584 decorators: Vec::new(),
585 signature: None,
586 })
587}
588
589fn extract_enum(
590 source: &[u8],
591 file_path: &str,
592 node: Node,
593 is_exported: bool,
594) -> Option<SymbolNode> {
595 let name = symbol_name(node, source)?;
596 let qualified_name = format!("{file_path}::{name}");
597
598 Some(SymbolNode {
599 name: name.clone(),
600 qualified_name,
601 kind: SymbolKind::Enum,
602 location: node_location(file_path, node),
603 visibility: export_visibility(is_exported),
604 is_exported,
605 is_async: false,
606 is_test: false,
607 decorators: Vec::new(),
608 signature: None,
609 })
610}
611
612fn extract_variable_declaration(
613 source: &[u8],
614 file_path: &str,
615 node: Node,
616 is_exported: bool,
617 symbols: &mut Vec<SymbolNode>,
618 edges: &mut Vec<Edge>,
619) {
620 let is_const = is_const_declaration(source, node);
621
622 let mut cursor = node.walk();
624 for child in node.children(&mut cursor) {
625 if child.kind() != "variable_declarator" {
626 continue;
627 }
628 let name = match child
629 .child_by_field_name("name")
630 .and_then(|n| n.utf8_text(source).ok())
631 .map(|s| s.to_string())
632 {
633 Some(n) => n,
634 None => continue,
635 };
636
637 let value_node = child.child_by_field_name("value");
639 let is_function_value = value_node
640 .map(|v| {
641 matches!(
642 v.kind(),
643 "arrow_function" | "function_expression" | "function"
644 )
645 })
646 .unwrap_or(false);
647
648 let kind = if is_function_value {
649 SymbolKind::Function
650 } else if is_const {
651 SymbolKind::Const
652 } else {
653 SymbolKind::Variable
654 };
655
656 let is_async = value_node.map(|v| has_async_keyword(v)).unwrap_or(false);
658
659 let qualified_name = format!("{file_path}::{name}");
660 let signature = if is_function_value {
661 value_node.and_then(|v| build_signature(source, v))
662 } else {
663 None
664 };
665 let decorators = extract_decorators(source, node);
666
667 let sym = SymbolNode {
668 name: name.clone(),
669 qualified_name: qualified_name.clone(),
670 kind,
671 location: node_location(file_path, node),
672 visibility: export_visibility(is_exported),
673 is_exported,
674 is_async,
675 is_test: is_test_name(&name),
676 decorators,
677 signature,
678 };
679 edges.push(contains_edge(file_path, &sym.qualified_name));
680 symbols.push(sym);
681 }
682}
683
684fn symbol_name(node: Node, source: &[u8]) -> Option<String> {
686 node.child_by_field_name("name")
687 .and_then(|n| n.utf8_text(source).ok())
688 .map(|s| s.to_string())
689}
690
691fn node_location(file_path: &str, node: Node) -> Location {
693 let start = node.start_position();
694 let end = node.end_position();
695 Location {
696 file: file_path.into(),
697 line_start: start.row + 1, line_end: end.row + 1,
699 col_start: start.column,
700 col_end: end.column,
701 }
702}
703
704fn has_async_keyword(node: Node) -> bool {
706 let mut cursor = node.walk();
707 for child in node.children(&mut cursor) {
708 if !child.is_named() && child.kind() == "async" {
709 return true;
710 }
711 }
712 false
713}
714
715fn is_const_declaration(source: &[u8], node: Node) -> bool {
717 let mut cursor = node.walk();
718 for child in node.children(&mut cursor) {
719 if !child.is_named() {
720 if let Ok(text) = child.utf8_text(source) {
721 if text == "const" {
722 return true;
723 }
724 }
725 }
726 if child.is_named() {
728 break;
729 }
730 }
731 false
732}
733
734fn build_signature(source: &[u8], node: Node) -> Option<String> {
736 let params = node
737 .child_by_field_name("parameters")
738 .and_then(|n| n.utf8_text(source).ok())?;
739
740 let return_type = node
741 .child_by_field_name("return_type")
742 .and_then(|n| n.utf8_text(source).ok());
743
744 if let Some(ret) = return_type {
745 Some(format!("{params}{ret}"))
746 } else {
747 Some(params.to_string())
748 }
749}
750
751fn extract_decorators(source: &[u8], node: Node) -> Vec<String> {
753 let mut decorators = Vec::new();
754 let mut cursor = node.walk();
755 for child in node.children(&mut cursor) {
756 if child.is_named() && child.kind() == "decorator" {
757 if let Ok(text) = child.utf8_text(source) {
758 decorators.push(text.to_string());
759 }
760 }
761 }
762 decorators
763}
764
765fn is_test_name(name: &str) -> bool {
768 if name.starts_with("test") || name.starts_with("describe") {
769 return true;
770 }
771 if name == "it" {
772 return true;
773 }
774 if let Some(rest) = name.strip_prefix("it") {
776 if rest.starts_with('_') || rest.starts_with(|c: char| c.is_ascii_uppercase()) {
777 return true;
778 }
779 }
780 false
781}
782
783fn export_visibility(is_exported: bool) -> Visibility {
785 if is_exported {
786 Visibility::Public
787 } else {
788 Visibility::Private
789 }
790}
791
792fn contains_edge(file_path: &str, qualified_name: &str) -> Edge {
794 Edge {
795 kind: EdgeKind::Contains,
796 source: file_path.to_string(),
797 target: qualified_name.to_string(),
798 metadata: None,
799 }
800}
801
802pub(crate) fn extract_imports(root: &tree_sitter::Node, source: &[u8]) -> Vec<RawImport> {
808 let mut imports = Vec::new();
809 let mut cursor = root.walk();
810
811 for child in root.children(&mut cursor) {
812 if child.kind() == "import_statement" {
813 if let Some(imp) = parse_import_statement(&child, source) {
814 imports.push(imp);
815 }
816 }
817 }
818
819 imports
820}
821
822fn node_text<'a>(node: &tree_sitter::Node, source: &'a [u8]) -> &'a str {
823 node.utf8_text(source).unwrap_or("")
824}
825
826fn strip_quotes(s: &str) -> &str {
827 s.trim_matches(|c: char| c == '"' || c == '\'' || c == '`')
828}
829
830fn parse_import_statement(node: &tree_sitter::Node, source: &[u8]) -> Option<RawImport> {
831 let specifier_node = node.child_by_field_name("source")?;
833 let specifier = strip_quotes(node_text(&specifier_node, source)).to_string();
834
835 let line = node.start_position().row + 1; let mut is_type_only = false;
839 let mut cursor = node.walk();
840 for child in node.children(&mut cursor) {
841 if !child.is_named() && child.kind() == "type" {
842 is_type_only = true;
843 break;
844 }
845 }
846
847 let mut import_clause = None;
849 let mut cursor2 = node.walk();
850 for child in node.children(&mut cursor2) {
851 if child.kind() == "import_clause" {
852 import_clause = Some(child);
853 break;
854 }
855 }
856
857 let Some(clause) = import_clause else {
859 return Some(RawImport {
860 specifier,
861 names: Vec::new(),
862 is_type_only,
863 is_side_effect: true,
864 is_namespace: false,
865 line,
866 });
867 };
868
869 let mut names = Vec::new();
870 let mut is_namespace = false;
871
872 let mut clause_cursor = clause.walk();
873 for child in clause.children(&mut clause_cursor) {
874 match child.kind() {
875 "identifier" => {
877 let local_name = node_text(&child, source).to_string();
878 names.push(ImportName {
879 name: "default".to_string(),
880 alias: Some(local_name),
881 is_type: false,
882 });
883 }
884 "named_imports" => {
886 let mut ni_cursor = child.walk();
887 for spec in child.children(&mut ni_cursor) {
888 if spec.kind() == "import_specifier" {
889 let name_node = spec.child_by_field_name("name");
890 let alias_node = spec.child_by_field_name("alias");
891
892 if let Some(n) = name_node {
893 let name = node_text(&n, source).to_string();
894 let alias = alias_node.map(|a| node_text(&a, source).to_string());
895 names.push(ImportName {
896 name,
897 alias,
898 is_type: false,
899 });
900 }
901 }
902 }
903 }
904 "namespace_import" => {
906 is_namespace = true;
907 let mut ns_cursor = child.walk();
908 for ns_child in child.children(&mut ns_cursor) {
909 if ns_child.kind() == "identifier" {
910 let alias = node_text(&ns_child, source).to_string();
911 names.push(ImportName {
912 name: "*".to_string(),
913 alias: Some(alias),
914 is_type: false,
915 });
916 break;
917 }
918 }
919 }
920 _ => {}
921 }
922 }
923
924 Some(RawImport {
925 specifier,
926 names,
927 is_type_only,
928 is_side_effect: false,
929 is_namespace,
930 line,
931 })
932}
933
934fn declaration_names(node: &tree_sitter::Node, source: &[u8]) -> Vec<String> {
942 if node.kind() == "lexical_declaration" || node.kind() == "variable_declaration" {
943 let mut names = Vec::new();
944 let mut cursor = node.walk();
945 for child in node.children(&mut cursor) {
946 if child.kind() == "variable_declarator" {
947 if let Some(name) = child
948 .child_by_field_name("name")
949 .and_then(|n| n.utf8_text(source).ok())
950 {
951 names.push(name.to_string());
952 }
953 }
954 }
955 names
956 } else {
957 node.child_by_field_name("name")
958 .and_then(|n| n.utf8_text(source).ok())
959 .map(|s| vec![s.to_string()])
960 .unwrap_or_default()
961 }
962}
963
964pub(crate) fn extract_exports(root: &tree_sitter::Node, source: &[u8]) -> Vec<crate::Export> {
969 let mut exports = Vec::new();
970 let mut cursor = root.walk();
971
972 for top_node in root.children(&mut cursor) {
973 if top_node.kind() != "export_statement" {
974 continue;
975 }
976
977 let node = &top_node;
978
979 let mut has_default = false;
981 let mut has_type = false;
982 let mut has_star = false;
983 {
984 let mut child_cursor = node.walk();
985 for child in node.children(&mut child_cursor) {
986 if !child.is_named() {
987 match child.kind() {
988 "default" => has_default = true,
989 "type" => has_type = true,
990 "*" => has_star = true,
991 _ => {}
992 }
993 }
994 }
995 }
996
997 let source_specifier = node
999 .child_by_field_name("source")
1000 .and_then(|n| n.utf8_text(source).ok())
1001 .map(|s| strip_quotes(s).to_string());
1002 let is_reexport = source_specifier.is_some();
1003
1004 if has_star {
1006 exports.push(crate::Export {
1007 name: "*".to_string(),
1008 is_reexport: true,
1009 source_specifier,
1010 ..crate::Export::default()
1011 });
1012 continue;
1013 }
1014
1015 let export_clause = {
1017 let mut child_cursor = node.walk();
1018 let mut found = None;
1019 for child in node.children(&mut child_cursor) {
1020 if child.kind() == "export_clause" {
1021 found = Some(child);
1022 break;
1023 }
1024 }
1025 found
1026 };
1027
1028 if let Some(clause) = export_clause {
1029 let mut spec_cursor = clause.walk();
1030 for spec in clause.children(&mut spec_cursor) {
1031 if spec.kind() != "export_specifier" {
1032 continue;
1033 }
1034 let name_node = spec.child_by_field_name("name");
1035 let alias_node = spec.child_by_field_name("alias");
1036
1037 let local_text = name_node
1038 .and_then(|n| n.utf8_text(source).ok())
1039 .unwrap_or("")
1040 .to_string();
1041
1042 let (export_name, local_name) = if let Some(alias) = alias_node {
1043 let alias_text = alias.utf8_text(source).unwrap_or("").to_string();
1044 (alias_text, Some(local_text))
1045 } else {
1046 (local_text, None)
1047 };
1048
1049 exports.push(crate::Export {
1050 name: export_name,
1051 local_name,
1052 is_type_only: has_type,
1053 is_reexport,
1054 source_specifier: source_specifier.clone(),
1055 ..crate::Export::default()
1056 });
1057 }
1058 continue;
1059 }
1060
1061 if let Some(decl) = node.child_by_field_name("declaration") {
1063 let names = declaration_names(&decl, source);
1064
1065 if has_default {
1066 exports.push(crate::Export {
1067 name: "default".to_string(),
1068 local_name: names.into_iter().next(),
1069 is_default: true,
1070 ..crate::Export::default()
1071 });
1072 } else {
1073 for name in names {
1074 exports.push(crate::Export {
1075 name,
1076 ..crate::Export::default()
1077 });
1078 }
1079 }
1080 continue;
1081 }
1082
1083 if has_default {
1085 let value_node = node.child_by_field_name("value");
1086 let local_name = value_node.and_then(|n| {
1087 if n.kind() == "identifier" {
1088 n.utf8_text(source).ok().map(|s| s.to_string())
1089 } else {
1090 None
1091 }
1092 });
1093
1094 exports.push(crate::Export {
1095 name: "default".to_string(),
1096 local_name,
1097 is_default: true,
1098 ..crate::Export::default()
1099 });
1100 }
1101 }
1102
1103 exports
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108 use super::*;
1109
1110 #[test]
1111 fn language_returns_typescript() {
1112 let parser = TypeScriptParser::new();
1113 assert_eq!(parser.language(), Language::TypeScript);
1114 }
1115
1116 #[test]
1117 fn ts_file_extensions() {
1118 let parser = TypeScriptParser::new();
1119 let exts = parser.file_extensions();
1120 assert!(exts.contains(&"ts"));
1121 assert!(exts.contains(&"tsx"));
1122 assert!(!exts.contains(&"js"));
1123 }
1124
1125 #[test]
1126 fn js_file_extensions() {
1127 let parser = JavaScriptParser::new();
1128 let exts = parser.file_extensions();
1129 assert!(exts.contains(&"js"));
1130 assert!(exts.contains(&"jsx"));
1131 assert!(!exts.contains(&"ts"));
1132 }
1133
1134 #[test]
1135 fn js_language_returns_javascript() {
1136 let parser = JavaScriptParser::new();
1137 assert_eq!(parser.language(), Language::JavaScript);
1138 }
1139
1140 #[test]
1141 fn parse_empty_ts_file() {
1142 let parser = TypeScriptParser::new();
1143 let result = parser
1144 .parse(b"", Path::new("test.ts"))
1145 .expect("should parse empty file");
1146 assert!(result.symbols.is_empty());
1147 assert!(result.edges.is_empty());
1148 assert!(result.imports.is_empty());
1149 assert!(result.exports.is_empty());
1150 }
1151
1152 #[test]
1153 fn parse_empty_js_file() {
1154 let parser = JavaScriptParser::new();
1155 let result = parser
1156 .parse(b"", Path::new("test.js"))
1157 .expect("should parse empty file");
1158 assert!(result.symbols.is_empty());
1159 }
1160
1161 #[test]
1162 fn language_fn_selects_correct_grammar() {
1163 let ts_parser = TypeScriptParser::new();
1164 let _ = ts_parser.language_fn_for_path(Path::new("a.ts"));
1165 let _ = ts_parser.language_fn_for_path(Path::new("a.tsx"));
1166
1167 let js_parser = JavaScriptParser::new();
1168 let _ = js_parser.language_fn_for_path(Path::new("a.js"));
1169 let _ = js_parser.language_fn_for_path(Path::new("a.jsx"));
1170 }
1171
1172 fn parse_ts_imports(source: &str) -> Vec<crate::RawImport> {
1177 let lang: tree_sitter::Language = tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into();
1178 let mut ts_parser = tree_sitter::Parser::new();
1179 ts_parser.set_language(&lang).unwrap();
1180 let tree = ts_parser.parse(source.as_bytes(), None).unwrap();
1181 extract_imports(&tree.root_node(), source.as_bytes())
1182 }
1183
1184 #[test]
1185 fn import_named() {
1186 let imports = parse_ts_imports(r#"import { a, b } from "./mod""#);
1187 assert_eq!(imports.len(), 1);
1188 assert_eq!(imports[0].specifier, "./mod");
1189 assert_eq!(imports[0].names.len(), 2);
1190 assert_eq!(imports[0].names[0].name, "a");
1191 assert_eq!(imports[0].names[1].name, "b");
1192 assert!(!imports[0].is_type_only);
1193 }
1194
1195 #[test]
1196 fn import_type_only() {
1197 let imports = parse_ts_imports(r#"import type { T } from "./types""#);
1198 assert_eq!(imports.len(), 1);
1199 assert!(imports[0].is_type_only);
1200 }
1201
1202 #[test]
1203 fn import_namespace() {
1204 let imports = parse_ts_imports(r#"import * as ns from "./ns""#);
1205 assert_eq!(imports.len(), 1);
1206 assert!(imports[0].is_namespace);
1207 }
1208
1209 #[test]
1210 fn import_side_effect() {
1211 let imports = parse_ts_imports(r#"import "./polyfill""#);
1212 assert_eq!(imports.len(), 1);
1213 assert!(imports[0].is_side_effect);
1214 assert!(imports[0].names.is_empty());
1215 }
1216
1217 #[test]
1218 fn import_default() {
1219 let imports = parse_ts_imports(r#"import def from "./mod""#);
1220 assert_eq!(imports.len(), 1);
1221 assert_eq!(imports[0].names.len(), 1);
1222 assert_eq!(imports[0].names[0].name, "default");
1223 assert_eq!(imports[0].names[0].alias, Some("def".to_string()));
1224 }
1225
1226 #[test]
1227 fn import_mixed_default_and_named() {
1228 let imports = parse_ts_imports(r#"import def, { a } from "./mod""#);
1229 assert_eq!(imports.len(), 1);
1230 assert_eq!(imports[0].names.len(), 2);
1231 }
1232
1233 fn parse_ts_exports(source: &str) -> Vec<crate::Export> {
1238 let lang: tree_sitter::Language = tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into();
1239 let mut ts_parser = tree_sitter::Parser::new();
1240 ts_parser.set_language(&lang).unwrap();
1241 let tree = ts_parser.parse(source.as_bytes(), None).unwrap();
1242 extract_exports(&tree.root_node(), source.as_bytes())
1243 }
1244
1245 #[test]
1246 fn export_function() {
1247 let exports = parse_ts_exports("export function foo() {}");
1248 assert_eq!(exports.len(), 1);
1249 assert_eq!(exports[0].name, "foo");
1250 assert!(!exports[0].is_default);
1251 assert!(!exports[0].is_reexport);
1252 }
1253
1254 #[test]
1255 fn export_default_class() {
1256 let exports = parse_ts_exports("export default class Bar {}");
1257 assert_eq!(exports.len(), 1);
1258 assert_eq!(exports[0].name, "default");
1259 assert_eq!(exports[0].local_name, Some("Bar".to_string()));
1260 assert!(exports[0].is_default);
1261 }
1262
1263 #[test]
1264 fn export_reexport() {
1265 let exports = parse_ts_exports(r#"export { foo } from "./mod""#);
1266 assert_eq!(exports.len(), 1);
1267 assert_eq!(exports[0].name, "foo");
1268 assert!(exports[0].is_reexport);
1269 assert_eq!(exports[0].source_specifier, Some("./mod".to_string()));
1270 }
1271
1272 #[test]
1273 fn export_with_alias() {
1274 let exports = parse_ts_exports("export { foo as bar }");
1275 assert_eq!(exports.len(), 1);
1276 assert_eq!(exports[0].name, "bar");
1277 assert_eq!(exports[0].local_name, Some("foo".to_string()));
1278 }
1279
1280 #[test]
1281 fn export_star() {
1282 let exports = parse_ts_exports(r#"export * from "./barrel""#);
1283 assert_eq!(exports.len(), 1);
1284 assert_eq!(exports[0].name, "*");
1285 assert!(exports[0].is_reexport);
1286 assert_eq!(exports[0].source_specifier, Some("./barrel".to_string()));
1287 }
1288
1289 #[test]
1290 fn export_type_only() {
1291 let exports = parse_ts_exports("export type { Foo }");
1292 assert_eq!(exports.len(), 1);
1293 assert_eq!(exports[0].name, "Foo");
1294 assert!(exports[0].is_type_only);
1295 }
1296
1297 #[test]
1298 fn export_default_expression() {
1299 let exports = parse_ts_exports("export default 42");
1300 assert_eq!(exports.len(), 1);
1301 assert_eq!(exports[0].name, "default");
1302 assert!(exports[0].is_default);
1303 assert!(exports[0].local_name.is_none());
1304 }
1305
1306 fn parse_ts(source: &str) -> ParseResult {
1311 let parser = TypeScriptParser::new();
1312 parser
1313 .parse(source.as_bytes(), Path::new("test.ts"))
1314 .expect("parse failed")
1315 }
1316
1317 #[test]
1318 fn symbol_function_declaration() {
1319 let result = parse_ts("function foo() {}");
1320 assert_eq!(result.symbols.len(), 1);
1321 let sym = &result.symbols[0];
1322 assert_eq!(sym.name, "foo");
1323 assert_eq!(sym.kind, SymbolKind::Function);
1324 assert!(!sym.is_exported);
1325 assert_eq!(sym.visibility, Visibility::Private);
1326 }
1327
1328 #[test]
1329 fn symbol_class_with_method() {
1330 let result = parse_ts("class Bar { baz() {} }");
1331 assert_eq!(result.symbols.len(), 2);
1332
1333 let class_sym = result.symbols.iter().find(|s| s.name == "Bar").unwrap();
1334 assert_eq!(class_sym.kind, SymbolKind::Class);
1335
1336 let method_sym = result.symbols.iter().find(|s| s.name == "baz").unwrap();
1337 assert_eq!(method_sym.kind, SymbolKind::Method);
1338
1339 let child_of = result
1341 .edges
1342 .iter()
1343 .find(|e| e.kind == EdgeKind::ChildOf)
1344 .unwrap();
1345 assert_eq!(child_of.source, "test.ts::Bar.baz");
1346 assert_eq!(child_of.target, "test.ts::Bar");
1347 }
1348
1349 #[test]
1350 fn symbol_interface_with_property() {
1351 let result = parse_ts("interface IFoo { prop: string }");
1352 assert_eq!(result.symbols.len(), 2);
1353
1354 let iface = result.symbols.iter().find(|s| s.name == "IFoo").unwrap();
1355 assert_eq!(iface.kind, SymbolKind::Interface);
1356
1357 let prop = result.symbols.iter().find(|s| s.name == "prop").unwrap();
1358 assert_eq!(prop.kind, SymbolKind::Property);
1359 }
1360
1361 #[test]
1362 fn symbol_type_alias() {
1363 let result = parse_ts("type Alias = string");
1364 assert_eq!(result.symbols.len(), 1);
1365 let sym = &result.symbols[0];
1366 assert_eq!(sym.name, "Alias");
1367 assert_eq!(sym.kind, SymbolKind::TypeAlias);
1368 }
1369
1370 #[test]
1371 fn symbol_enum_declaration() {
1372 let result = parse_ts("enum Color { Red, Green }");
1373 assert_eq!(result.symbols.len(), 1);
1374 let sym = &result.symbols[0];
1375 assert_eq!(sym.name, "Color");
1376 assert_eq!(sym.kind, SymbolKind::Enum);
1377 }
1378
1379 #[test]
1380 fn symbol_exported_async_arrow() {
1381 let result = parse_ts("export const handler = async () => {}");
1382 assert_eq!(result.symbols.len(), 1);
1383 let sym = &result.symbols[0];
1384 assert_eq!(sym.name, "handler");
1385 assert_eq!(sym.kind, SymbolKind::Function);
1386 assert!(sym.is_async);
1387 assert!(sym.is_exported);
1388 assert_eq!(sym.visibility, Visibility::Public);
1389 }
1390
1391 #[test]
1392 fn symbol_export_default_function() {
1393 let result = parse_ts("export default function main() {}");
1394 assert_eq!(result.symbols.len(), 1);
1395 let sym = &result.symbols[0];
1396 assert_eq!(sym.name, "main");
1397 assert_eq!(sym.kind, SymbolKind::Function);
1398 assert!(sym.is_exported);
1399 }
1400
1401 #[test]
1402 fn symbol_non_exported_private() {
1403 let result = parse_ts("function helper() {}");
1404 assert_eq!(result.symbols.len(), 1);
1405 let sym = &result.symbols[0];
1406 assert_eq!(sym.visibility, Visibility::Private);
1407 assert!(!sym.is_exported);
1408 }
1409
1410 #[test]
1411 fn symbol_contains_edges() {
1412 let result = parse_ts("function a() {}\nfunction b() {}");
1413 let contains: Vec<_> = result
1414 .edges
1415 .iter()
1416 .filter(|e| e.kind == EdgeKind::Contains)
1417 .collect();
1418 assert_eq!(contains.len(), 2);
1419 for edge in &contains {
1420 assert_eq!(edge.source, "test.ts");
1421 }
1422 assert!(contains.iter().any(|e| e.target == "test.ts::a"));
1423 assert!(contains.iter().any(|e| e.target == "test.ts::b"));
1424 }
1425
1426 #[test]
1427 fn symbol_child_of_edges() {
1428 let result = parse_ts("class Foo { bar() {} }");
1429 let child_of: Vec<_> = result
1430 .edges
1431 .iter()
1432 .filter(|e| e.kind == EdgeKind::ChildOf)
1433 .collect();
1434 assert_eq!(child_of.len(), 1);
1435 assert_eq!(child_of[0].source, "test.ts::Foo.bar");
1436 assert_eq!(child_of[0].target, "test.ts::Foo");
1437 }
1438
1439 #[test]
1440 fn symbol_qualified_names() {
1441 let result = parse_ts("class MyClass { myMethod() {} }");
1442 let class_sym = result.symbols.iter().find(|s| s.name == "MyClass").unwrap();
1443 assert_eq!(class_sym.qualified_name, "test.ts::MyClass");
1444
1445 let method_sym = result
1446 .symbols
1447 .iter()
1448 .find(|s| s.name == "myMethod")
1449 .unwrap();
1450 assert_eq!(method_sym.qualified_name, "test.ts::MyClass.myMethod");
1451 }
1452
1453 #[test]
1454 fn symbol_const_variable() {
1455 let result = parse_ts("const MAX = 100");
1456 assert_eq!(result.symbols.len(), 1);
1457 let sym = &result.symbols[0];
1458 assert_eq!(sym.name, "MAX");
1459 assert_eq!(sym.kind, SymbolKind::Const);
1460 }
1461
1462 #[test]
1463 fn symbol_let_variable() {
1464 let result = parse_ts("let count = 0");
1465 assert_eq!(result.symbols.len(), 1);
1466 let sym = &result.symbols[0];
1467 assert_eq!(sym.name, "count");
1468 assert_eq!(sym.kind, SymbolKind::Variable);
1469 }
1470
1471 #[test]
1472 fn symbol_function_signature() {
1473 let result = parse_ts("function add(x: number, y: number): number { return x + y; }");
1474 let sym = &result.symbols[0];
1475 assert!(sym.signature.is_some());
1476 let sig = sym.signature.as_ref().unwrap();
1477 assert!(sig.contains("x: number"));
1478 assert!(sig.contains("number")); }
1480
1481 #[test]
1482 fn symbol_interface_method_signature() {
1483 let result = parse_ts("interface ICalc { add(a: number, b: number): number }");
1484 let method = result.symbols.iter().find(|s| s.name == "add").unwrap();
1485 assert_eq!(method.kind, SymbolKind::Method);
1486 assert!(method.signature.is_some());
1487 }
1488
1489 #[test]
1490 fn symbol_is_test_heuristic() {
1491 let result = parse_ts("function testSomething() {}");
1492 let sym = &result.symbols[0];
1493 assert!(sym.is_test);
1494
1495 let result2 = parse_ts("function helper() {}");
1496 let sym2 = &result2.symbols[0];
1497 assert!(!sym2.is_test);
1498 }
1499
1500 #[test]
1501 fn symbol_location_is_populated() {
1502 let result = parse_ts("function foo() {}");
1503 let sym = &result.symbols[0];
1504 assert_eq!(sym.location.file.to_string_lossy(), "test.ts");
1505 assert_eq!(sym.location.line_start, 1);
1506 assert!(sym.location.line_end >= 1);
1507 }
1508
1509 #[test]
1514 fn parse_invalid_source_returns_error_not_panic() {
1515 let parser = TypeScriptParser::new();
1517 let result = parser.parse(b"", Path::new("empty.ts"));
1518 assert!(result.is_ok());
1519 }
1520
1521 #[test]
1522 fn parse_syntax_errors_returns_partial_result() {
1523 let source = r#"
1525function valid() {}
1526const x = {{{;
1527function alsoValid() {}
1528"#;
1529 let parser = TypeScriptParser::new();
1530 let result = parser
1531 .parse(source.as_bytes(), Path::new("partial.ts"))
1532 .expect("should not error on syntax errors");
1533 assert!(
1535 !result.symbols.is_empty(),
1536 "should extract at least some symbols from partially broken source"
1537 );
1538 assert!(result.symbols.iter().any(|s| s.name == "valid"));
1539 }
1540
1541 #[test]
1546 fn parse_from_two_threads_concurrently() {
1547 use std::thread;
1548
1549 let handles: Vec<_> = (0..2)
1550 .map(|i| {
1551 thread::spawn(move || {
1552 let parser = TypeScriptParser::new();
1553 let source = format!("function thread{i}() {{}}");
1554 let result = parser
1555 .parse(source.as_bytes(), Path::new(&format!("thread{i}.ts")))
1556 .expect("concurrent parse failed");
1557 assert_eq!(result.symbols.len(), 1);
1558 assert_eq!(result.symbols[0].name, format!("thread{i}"));
1559 })
1560 })
1561 .collect();
1562
1563 for h in handles {
1564 h.join().expect("thread panicked");
1565 }
1566 }
1567
1568 #[test]
1573 fn integration_multi_construct_file() {
1574 let source = r#"
1575import { helper } from "./utils";
1576import type { Config } from "./config";
1577
1578export function processData(data: string[]): number {
1579 return data.length;
1580}
1581
1582class DataProcessor {
1583 private cache: Map<string, number>;
1584
1585 constructor() {
1586 this.cache = new Map();
1587 }
1588
1589 process(input: string): number {
1590 return input.length;
1591 }
1592}
1593
1594interface IProcessor {
1595 process(input: string): number;
1596}
1597
1598type ProcessorFn = (input: string) => number;
1599
1600enum Status {
1601 Active,
1602 Inactive,
1603}
1604
1605export const transformer = async (x: number) => x * 2;
1606
1607export default class MainProcessor {}
1608
1609export { DataProcessor };
1610export * from "./helpers";
1611"#;
1612 let result = parse_ts(source);
1613
1614 assert!(result
1616 .symbols
1617 .iter()
1618 .any(|s| s.name == "processData" && s.kind == SymbolKind::Function));
1619 assert!(result
1620 .symbols
1621 .iter()
1622 .any(|s| s.name == "DataProcessor" && s.kind == SymbolKind::Class));
1623 assert!(result
1624 .symbols
1625 .iter()
1626 .any(|s| s.name == "process" && s.kind == SymbolKind::Method));
1627 assert!(result
1628 .symbols
1629 .iter()
1630 .any(|s| s.name == "IProcessor" && s.kind == SymbolKind::Interface));
1631 assert!(result
1632 .symbols
1633 .iter()
1634 .any(|s| s.name == "ProcessorFn" && s.kind == SymbolKind::TypeAlias));
1635 assert!(result
1636 .symbols
1637 .iter()
1638 .any(|s| s.name == "Status" && s.kind == SymbolKind::Enum));
1639 assert!(result
1640 .symbols
1641 .iter()
1642 .any(|s| s.name == "transformer" && s.kind == SymbolKind::Function && s.is_async));
1643 assert!(result
1644 .symbols
1645 .iter()
1646 .any(|s| s.name == "MainProcessor" && s.kind == SymbolKind::Class));
1647
1648 let contains_count = result
1650 .edges
1651 .iter()
1652 .filter(|e| e.kind == EdgeKind::Contains)
1653 .count();
1654 assert!(
1655 contains_count >= 7,
1656 "expected at least 7 Contains edges, got {contains_count}"
1657 );
1658
1659 let child_of_count = result
1660 .edges
1661 .iter()
1662 .filter(|e| e.kind == EdgeKind::ChildOf)
1663 .count();
1664 assert!(
1665 child_of_count >= 2,
1666 "expected at least 2 ChildOf edges, got {child_of_count}"
1667 );
1668
1669 assert_eq!(result.imports.len(), 2);
1671 assert!(result.imports.iter().any(|i| i.specifier == "./utils"));
1672 assert!(result
1673 .imports
1674 .iter()
1675 .any(|i| i.specifier == "./config" && i.is_type_only));
1676
1677 assert!(result.exports.iter().any(|e| e.name == "processData"));
1679 assert!(result.exports.iter().any(|e| e.name == "transformer"));
1680 assert!(result
1681 .exports
1682 .iter()
1683 .any(|e| e.name == "default" && e.is_default));
1684 assert!(result.exports.iter().any(|e| e.name == "DataProcessor"));
1685 assert!(result
1686 .exports
1687 .iter()
1688 .any(|e| e.name == "*" && e.is_reexport));
1689 }
1690
1691 #[test]
1692 fn extract_integrates_imports_and_exports() {
1693 let source = r#"
1694import { x } from "./mod";
1695export function foo() {}
1696"#;
1697 let result = parse_ts(source);
1698 assert_eq!(result.imports.len(), 1);
1699 assert_eq!(result.imports[0].specifier, "./mod");
1700 assert_eq!(result.exports.len(), 1);
1701 assert_eq!(result.exports[0].name, "foo");
1702 }
1703
1704 #[test]
1709 fn is_test_no_false_positive_on_items() {
1710 let result = parse_ts("function items() {}");
1712 let sym = &result.symbols[0];
1713 assert!(!sym.is_test, "'items' should not be detected as test");
1714
1715 let result2 = parse_ts("function iterator() {}");
1716 let sym2 = &result2.symbols[0];
1717 assert!(!sym2.is_test, "'iterator' should not be detected as test");
1718 }
1719
1720 #[test]
1721 fn is_test_matches_it_exact_and_camel() {
1722 let result = parse_ts("function itShouldWork() {}");
1724 assert!(result.symbols[0].is_test);
1725 }
1726
1727 #[test]
1728 fn export_multi_declarator() {
1729 let exports = parse_ts_exports("export const a = 1, b = 2");
1731 assert_eq!(exports.len(), 2);
1732 assert!(exports.iter().any(|e| e.name == "a"));
1733 assert!(exports.iter().any(|e| e.name == "b"));
1734 }
1735
1736 #[test]
1737 fn export_default_anonymous_function() {
1738 let result = parse_ts("export default function() {}");
1740 assert!(
1741 result
1742 .symbols
1743 .iter()
1744 .any(|s| s.name == "default" && s.kind == SymbolKind::Function),
1745 "anonymous default export should produce a 'default' symbol, got: {:?}",
1746 result
1747 .symbols
1748 .iter()
1749 .map(|s| (&s.name, &s.kind))
1750 .collect::<Vec<_>>()
1751 );
1752 }
1753}