1use std::collections::hash_map::DefaultHasher;
2use std::hash::{Hash, Hasher};
3
4use cha_core::{ClassInfo, FunctionInfo, ImportInfo, SourceFile, SourceModel};
5use tree_sitter::{Node, Parser};
6
7use crate::LanguageParser;
8
9pub struct TypeScriptParser;
10
11impl LanguageParser for TypeScriptParser {
12 fn language_name(&self) -> &str {
13 "typescript"
14 }
15
16 fn parse(&self, file: &SourceFile) -> Option<SourceModel> {
17 let mut parser = Parser::new();
18 parser
19 .set_language(&tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into())
20 .ok()?;
21 let tree = parser.parse(&file.content, None)?;
22 let root = tree.root_node();
23 let src = file.content.as_bytes();
24
25 let imports_map = crate::typescript_imports::build(root, src);
26 let mut ctx = ParseContext::new(src, imports_map);
27 ctx.collect_nodes(root, false);
28
29 Some(SourceModel {
30 language: "typescript".into(),
31 total_lines: file.line_count(),
32 functions: ctx.col.functions,
33 classes: ctx.col.classes,
34 imports: ctx.col.imports,
35 comments: collect_comments(root, src),
36 type_aliases: vec![], })
38 }
39}
40
41struct Collector {
43 functions: Vec<FunctionInfo>,
44 classes: Vec<ClassInfo>,
45 imports: Vec<ImportInfo>,
46}
47
48struct ParseContext<'a> {
50 src: &'a [u8],
51 col: Collector,
52 imports_map: crate::type_ref::ImportsMap,
53}
54
55impl<'a> ParseContext<'a> {
56 fn new(src: &'a [u8], imports_map: crate::type_ref::ImportsMap) -> Self {
57 Self {
58 src,
59 imports_map,
60 col: Collector {
61 functions: Vec::new(),
62 classes: Vec::new(),
63 imports: Vec::new(),
64 },
65 }
66 }
67
68 fn collect_nodes(&mut self, node: Node, exported: bool) {
69 let mut cursor = node.walk();
70 for child in node.children(&mut cursor) {
71 self.collect_single_node(child, exported);
72 }
73 }
74
75 fn collect_single_node(&mut self, child: Node, exported: bool) {
76 match child.kind() {
77 "export_statement" => self.collect_nodes(child, true),
78 "function_declaration" | "method_definition" => self.push_function(child, exported),
79 "lexical_declaration" | "variable_declaration" => {
80 extract_arrow_functions(
81 child,
82 self.src,
83 exported,
84 &self.imports_map,
85 &mut self.col.functions,
86 );
87 self.collect_nodes(child, exported);
88 }
89 "class_declaration" => self.push_class(child, exported),
90 "import_statement" => self.push_import(child),
91 _ => self.collect_nodes(child, false),
92 }
93 }
94
95 fn push_function(&mut self, node: Node, exported: bool) {
96 if let Some(mut f) = extract_function(node, self.src, &self.imports_map) {
97 f.is_exported = exported;
98 self.col.functions.push(f);
99 }
100 }
101
102 fn push_class(&mut self, node: Node, exported: bool) {
103 if let Some(mut c) = extract_class(node, self.src) {
104 c.is_exported = exported;
105 self.col.classes.push(c);
106 }
107 }
108
109 fn push_import(&mut self, node: Node) {
110 if let Some(i) = extract_import(node, self.src) {
111 self.col.imports.push(i);
112 }
113 }
114}
115
116fn node_text<'a>(node: Node, src: &'a [u8]) -> &'a str {
117 node.utf8_text(src).unwrap_or("")
118}
119
120fn hash_ast_structure(node: Node) -> u64 {
122 let mut hasher = DefaultHasher::new();
123 walk_hash(node, &mut hasher);
124 hasher.finish()
125}
126
127fn walk_hash(node: Node, hasher: &mut DefaultHasher) {
128 node.kind().hash(hasher);
129 let mut cursor = node.walk();
130 for child in node.children(&mut cursor) {
131 walk_hash(child, hasher);
132 }
133}
134
135fn extract_function(
136 node: Node,
137 src: &[u8],
138 imports_map: &crate::type_ref::ImportsMap,
139) -> Option<FunctionInfo> {
140 let name_node = node.child_by_field_name("name")?;
141 let name = node_text(name_node, src).to_string();
142 let name_col = name_node.start_position().column;
143 let name_end_col = name_node.end_position().column;
144 let start_line = node.start_position().row + 1;
145 let end_line = node.end_position().row + 1;
146 let body = node.child_by_field_name("body");
147 let body_hash = body.map(hash_ast_structure);
148 let parameter_count = count_parameters(node);
149 let parameter_types = extract_param_types(node, src, imports_map);
150 let chain_depth = body.map(max_chain_depth).unwrap_or(0);
151 let switch_arms = body.map(count_switch_arms).unwrap_or(0);
152 let external_refs = body
153 .map(|b| collect_external_refs(b, src))
154 .unwrap_or_default();
155 let is_delegating = body.map(|b| check_delegating(b, src)).unwrap_or(false);
156 Some(FunctionInfo {
157 name,
158 start_line,
159 end_line,
160 name_col,
161 name_end_col,
162 line_count: end_line - start_line + 1,
163 complexity: count_complexity(node),
164 body_hash,
165 is_exported: false,
166 parameter_count,
167 parameter_types,
168 chain_depth,
169 switch_arms,
170 external_refs,
171 is_delegating,
172 comment_lines: count_comment_lines(node),
173 referenced_fields: collect_this_fields(body, src),
174 null_check_fields: collect_null_checks_ts(body, src),
175 switch_dispatch_target: extract_switch_target_ts(body, src),
176 optional_param_count: count_optional_params_ts(node, src),
177 called_functions: collect_calls_ts(body, src),
178 cognitive_complexity: body.map(cognitive_complexity_ts).unwrap_or(0),
179 })
180}
181
182fn extract_arrow_functions(
183 node: Node,
184 src: &[u8],
185 exported: bool,
186 imports_map: &crate::type_ref::ImportsMap,
187 functions: &mut Vec<FunctionInfo>,
188) {
189 let mut cursor = node.walk();
190 for child in node.children(&mut cursor) {
191 if child.kind() == "variable_declarator"
192 && let Some(f) = try_extract_arrow(child, node, src, exported, imports_map)
193 {
194 functions.push(f);
195 }
196 }
197}
198
199fn try_extract_arrow(
201 child: Node,
202 decl: Node,
203 src: &[u8],
204 exported: bool,
205 imports_map: &crate::type_ref::ImportsMap,
206) -> Option<FunctionInfo> {
207 let name_node = child.child_by_field_name("name")?;
208 let name = node_text(name_node, src).to_string();
209 let value = child.child_by_field_name("value")?;
210 if value.kind() != "arrow_function" {
211 return None;
212 }
213 let name_col = name_node.start_position().column;
214 let name_end_col = name_node.end_position().column;
215 let start_line = decl.start_position().row + 1;
216 let end_line = decl.end_position().row + 1;
217 let body = value.child_by_field_name("body");
218 let body_hash = body.map(hash_ast_structure);
219 Some(FunctionInfo {
220 name,
221 start_line,
222 end_line,
223 name_col,
224 name_end_col,
225 line_count: end_line - start_line + 1,
226 complexity: count_complexity(value),
227 body_hash,
228 is_exported: exported,
229 parameter_count: count_parameters(value),
230 parameter_types: extract_param_types(value, src, imports_map),
231 chain_depth: body.map(max_chain_depth).unwrap_or(0),
232 switch_arms: body.map(count_switch_arms).unwrap_or(0),
233 external_refs: body
234 .map(|b| collect_external_refs(b, src))
235 .unwrap_or_default(),
236 is_delegating: body.map(|b| check_delegating(b, src)).unwrap_or(false),
237 comment_lines: count_comment_lines(value),
238 referenced_fields: collect_this_fields(body, src),
239 null_check_fields: collect_null_checks_ts(body, src),
240 switch_dispatch_target: extract_switch_target_ts(body, src),
241 optional_param_count: count_optional_params_ts(value, src),
242 called_functions: collect_calls_ts(Some(value), src),
243 cognitive_complexity: cognitive_complexity_ts(value),
244 })
245}
246
247fn extract_class(node: Node, src: &[u8]) -> Option<ClassInfo> {
248 let name_node = node.child_by_field_name("name")?;
249 let name = node_text(name_node, src).to_string();
250 let name_col = name_node.start_position().column;
251 let name_end_col = name_node.end_position().column;
252 let start_line = node.start_position().row + 1;
253 let end_line = node.end_position().row + 1;
254 let body = node.child_by_field_name("body")?;
255 let (methods, delegating, fields, has_behavior, cb_fields) = scan_class_body(body, src);
256 let is_interface =
257 node.kind() == "interface_declaration" || node.kind() == "abstract_class_declaration";
258 let has_listener_field = !cb_fields.is_empty();
259 let has_notify_method = has_iterate_and_call_ts(body, src, &cb_fields);
260
261 Some(ClassInfo {
262 name,
263 start_line,
264 end_line,
265 name_col,
266 name_end_col,
267 method_count: methods,
268 line_count: end_line - start_line + 1,
269 is_exported: false,
270 delegating_method_count: delegating,
271 field_count: fields.len(),
272 field_names: fields,
273 field_types: Vec::new(),
274 has_behavior,
275 is_interface,
276 parent_name: extract_parent_name(node, src),
277 override_count: 0,
278 self_call_count: 0,
279 has_listener_field,
280 has_notify_method,
281 })
282}
283
284fn scan_class_body(body: Node, src: &[u8]) -> (usize, usize, Vec<String>, bool, Vec<String>) {
286 let mut methods = 0;
287 let mut delegating = 0;
288 let mut fields = Vec::new();
289 let mut callback_fields = Vec::new();
290 let mut has_behavior = false;
291 let mut cursor = body.walk();
292 for child in body.children(&mut cursor) {
293 match child.kind() {
294 "method_definition" => {
295 let (is_behavior, is_delegating) = classify_method(child, src);
296 methods += 1;
297 has_behavior |= is_behavior;
298 delegating += usize::from(is_delegating);
299 }
300 "public_field_definition" | "property_definition" => {
301 if let Some(n) = child.child_by_field_name("name") {
302 let name = node_text(n, src).to_string();
303 if is_callback_collection_type_ts(child, src) {
304 callback_fields.push(name.clone());
305 }
306 fields.push(name);
307 }
308 }
309 _ => {}
310 }
311 }
312 (methods, delegating, fields, has_behavior, callback_fields)
313}
314
315fn classify_method(node: Node, src: &[u8]) -> (bool, bool) {
317 let mname = node
318 .child_by_field_name("name")
319 .map(|n| node_text(n, src))
320 .unwrap_or("");
321 let is_behavior = !is_accessor_name(mname) && mname != "constructor";
322 let is_delegating = node
323 .child_by_field_name("body")
324 .is_some_and(|b| check_delegating(b, src));
325 (is_behavior, is_delegating)
326}
327
328fn count_parameters(node: Node) -> usize {
329 let params = match node.child_by_field_name("parameters") {
330 Some(p) => p,
331 None => return 0,
332 };
333 let mut cursor = params.walk();
334 params
335 .children(&mut cursor)
336 .filter(|c| {
337 matches!(
338 c.kind(),
339 "required_parameter" | "optional_parameter" | "rest_parameter"
340 )
341 })
342 .count()
343}
344
345fn extract_param_types(
346 node: Node,
347 src: &[u8],
348 imports_map: &crate::type_ref::ImportsMap,
349) -> Vec<cha_core::TypeRef> {
350 let params = match node.child_by_field_name("parameters") {
351 Some(p) => p,
352 None => return vec![],
353 };
354 let mut types = Vec::new();
355 let mut cursor = params.walk();
356 for child in params.children(&mut cursor) {
357 if let Some(ann) = child.child_by_field_name("type") {
358 let raw = node_text(ann, src)
360 .trim_start_matches(':')
361 .trim()
362 .to_string();
363 types.push(crate::type_ref::resolve(raw, imports_map));
364 }
365 }
366 types
367}
368
369fn max_chain_depth(node: Node) -> usize {
370 let mut max = 0;
371 walk_chain_depth(node, &mut max);
372 max
373}
374
375fn walk_chain_depth(node: Node, max: &mut usize) {
376 if node.kind() == "member_expression" {
377 let depth = measure_chain(node);
378 if depth > *max {
379 *max = depth;
380 }
381 }
382 let mut cursor = node.walk();
383 for child in node.children(&mut cursor) {
384 walk_chain_depth(child, max);
385 }
386}
387
388fn measure_chain(node: Node) -> usize {
390 let mut depth = 0;
391 let mut current = node;
392 while current.kind() == "member_expression" {
393 depth += 1;
394 if let Some(obj) = current.child_by_field_name("object") {
395 current = obj;
396 } else {
397 break;
398 }
399 }
400 depth
401}
402
403fn count_switch_arms(node: Node) -> usize {
404 let mut count = 0;
405 walk_switch_arms(node, &mut count);
406 count
407}
408
409fn walk_switch_arms(node: Node, count: &mut usize) {
410 if node.kind() == "switch_case" || node.kind() == "switch_default" {
411 *count += 1;
412 }
413 let mut cursor = node.walk();
414 for child in node.children(&mut cursor) {
415 walk_switch_arms(child, count);
416 }
417}
418
419fn collect_external_refs(node: Node, src: &[u8]) -> Vec<String> {
420 let mut refs = Vec::new();
421 walk_external_refs(node, src, &mut refs);
422 refs.sort();
423 refs.dedup();
424 refs
425}
426
427fn member_chain_root(node: Node) -> Node {
428 let mut current = node;
429 while current.kind() == "member_expression" {
430 match current.child_by_field_name("object") {
431 Some(child) => current = child,
432 None => break,
433 }
434 }
435 current
436}
437
438fn walk_external_refs(node: Node, src: &[u8], refs: &mut Vec<String>) {
439 if node.kind() == "member_expression" {
440 let root = member_chain_root(node);
441 let text = node_text(root, src);
442 if text != "this" && text != "self" && !text.is_empty() {
443 refs.push(text.to_string());
444 }
445 }
446 let mut cursor = node.walk();
447 for child in node.children(&mut cursor) {
448 walk_external_refs(child, src, refs);
449 }
450}
451
452fn single_stmt(body: Node) -> Option<Node> {
453 let mut cursor = body.walk();
454 let stmts: Vec<_> = body
455 .children(&mut cursor)
456 .filter(|c| c.kind() != "{" && c.kind() != "}")
457 .collect();
458 (stmts.len() == 1).then(|| stmts[0])
459}
460
461fn is_external_call(node: Node, src: &[u8]) -> bool {
462 node.kind() == "call_expression"
463 && node.child_by_field_name("function").is_some_and(|func| {
464 func.kind() == "member_expression"
465 && func
466 .child_by_field_name("object")
467 .is_some_and(|obj| node_text(obj, src) != "this")
468 })
469}
470
471fn check_delegating(body: Node, src: &[u8]) -> bool {
472 let Some(stmt) = single_stmt(body) else {
473 return false;
474 };
475 let expr = match stmt.kind() {
476 "return_statement" => stmt.child(1).unwrap_or(stmt),
477 "expression_statement" => stmt.child(0).unwrap_or(stmt),
478 _ => stmt,
479 };
480 is_external_call(expr, src)
481}
482
483fn count_complexity(node: Node) -> usize {
484 let mut complexity = 1;
485 walk_complexity(node, &mut complexity);
486 complexity
487}
488
489fn walk_complexity(node: Node, count: &mut usize) {
490 match node.kind() {
491 "if_statement" | "else_clause" | "for_statement" | "for_in_statement"
492 | "while_statement" | "do_statement" | "switch_case" | "catch_clause"
493 | "ternary_expression" => {
494 *count += 1;
495 }
496 "binary_expression" => {
497 let mut cursor = node.walk();
498 for child in node.children(&mut cursor) {
499 if child.kind() == "&&" || child.kind() == "||" {
500 *count += 1;
501 }
502 }
503 }
504 _ => {}
505 }
506 let mut cursor = node.walk();
507 for child in node.children(&mut cursor) {
508 walk_complexity(child, count);
509 }
510}
511
512fn extract_import(node: Node, src: &[u8]) -> Option<ImportInfo> {
513 let mut cursor = node.walk();
514 for child in node.children(&mut cursor) {
515 if child.kind() == "string" {
516 let raw = node_text(child, src);
517 let source = raw.trim_matches(|c| c == '\'' || c == '"').to_string();
518 return Some(ImportInfo {
519 source,
520 line: node.start_position().row + 1,
521 col: node.start_position().column,
522 ..Default::default()
523 });
524 }
525 }
526 None
527}
528
529fn count_comment_lines(node: Node) -> usize {
531 let mut count = 0;
532 let mut cursor = node.walk();
533 for child in node.children(&mut cursor) {
534 if child.kind() == "comment" {
535 count += child.end_position().row - child.start_position().row + 1;
536 } else if child.child_count() > 0 {
537 count += count_comment_lines(child);
538 }
539 }
540 count
541}
542
543fn collect_this_fields(body: Option<Node>, src: &[u8]) -> Vec<String> {
546 let Some(body) = body else { return vec![] };
547 let mut refs = Vec::new();
548 collect_this_refs(body, src, &mut refs);
549 refs.sort();
550 refs.dedup();
551 refs
552}
553
554fn collect_this_refs(node: Node, src: &[u8], refs: &mut Vec<String>) {
555 if node.kind() == "member_expression"
556 && let Some(obj) = node.child_by_field_name("object")
557 && node_text(obj, src) == "this"
558 && let Some(prop) = node.child_by_field_name("property")
559 {
560 refs.push(node_text(prop, src).to_string());
561 }
562 let mut cursor = node.walk();
563 for child in node.children(&mut cursor) {
564 collect_this_refs(child, src, refs);
565 }
566}
567
568fn is_accessor_name(name: &str) -> bool {
570 let lower = name.to_lowercase();
571 lower.starts_with("get") || lower.starts_with("set") || lower.starts_with("is")
572}
573
574fn extract_parent_name(node: Node, src: &[u8]) -> Option<String> {
577 let mut cursor = node.walk();
578 for child in node.children(&mut cursor) {
579 if child.kind() == "class_heritage" {
580 let mut inner = child.walk();
581 for c in child.children(&mut inner) {
582 if c.kind() == "extends_clause" {
583 let mut ec = c.walk();
585 for e in c.children(&mut ec) {
586 if e.kind() == "identifier" || e.kind() == "type_identifier" {
587 return Some(node_text(e, src).to_string());
588 }
589 }
590 }
591 }
592 }
593 }
594 None
595}
596
597fn collect_null_checks_ts(body: Option<Node>, src: &[u8]) -> Vec<String> {
599 let Some(body) = body else { return vec![] };
600 let mut fields = Vec::new();
601 walk_null_checks_ts(body, src, &mut fields);
602 fields.sort();
603 fields.dedup();
604 fields
605}
606
607fn walk_null_checks_ts(node: Node, src: &[u8], fields: &mut Vec<String>) {
608 if node.kind() == "binary_expression"
609 && let text = node_text(node, src)
610 && (text.contains("null") || text.contains("undefined"))
611 && let Some(left) = node.child_by_field_name("left")
612 && let ltext = node_text(left, src)
613 && let Some(f) = ltext.strip_prefix("this.")
614 {
615 fields.push(f.to_string());
616 }
617 let mut cursor = node.walk();
618 for child in node.children(&mut cursor) {
619 walk_null_checks_ts(child, src, fields);
620 }
621}
622
623fn extract_switch_target_ts(body: Option<Node>, src: &[u8]) -> Option<String> {
625 let body = body?;
626 find_switch_target_ts(body, src)
627}
628
629fn find_switch_target_ts(node: Node, src: &[u8]) -> Option<String> {
630 if node.kind() == "switch_statement"
631 && let Some(value) = node.child_by_field_name("value")
632 {
633 return Some(node_text(value, src).to_string());
634 }
635 let mut cursor = node.walk();
636 for child in node.children(&mut cursor) {
637 if let Some(t) = find_switch_target_ts(child, src) {
638 return Some(t);
639 }
640 }
641 None
642}
643
644fn count_optional_params_ts(node: Node, src: &[u8]) -> usize {
646 let Some(params) = node.child_by_field_name("parameters") else {
647 return 0;
648 };
649 let mut count = 0;
650 let mut cursor = params.walk();
651 for child in params.children(&mut cursor) {
652 let text = node_text(child, src);
653 if text.contains('?') || child.child_by_field_name("value").is_some() {
654 count += 1;
655 }
656 }
657 count
658}
659
660fn is_callback_collection_type_ts(field_node: Node, src: &[u8]) -> bool {
663 let Some(ty) = field_node.child_by_field_name("type") else {
664 if let Some(init) = field_node.child_by_field_name("value") {
666 let text = node_text(init, src);
667 return text == "[]" || text.contains("new Array");
668 }
669 return false;
670 };
671 let text = node_text(ty, src);
672 (text.contains("Function") && (text.contains("[]") || text.contains("Array<")))
674 || (text.contains("=>") && text.contains("[]"))
675 || text.contains("Array<(")
676}
677
678fn has_iterate_and_call_ts(body: Node, src: &[u8], cb_fields: &[String]) -> bool {
681 if cb_fields.is_empty() {
682 return false;
683 }
684 let mut cursor = body.walk();
685 for child in body.children(&mut cursor) {
686 if child.kind() == "method_definition"
687 && let Some(fn_body) = child.child_by_field_name("body")
688 {
689 for field in cb_fields {
690 let this_field = format!("this.{field}");
691 if walk_for_iterate_call_ts(fn_body, src, &this_field) {
692 return true;
693 }
694 }
695 }
696 }
697 false
698}
699
700fn walk_for_iterate_call_ts(node: Node, src: &[u8], this_field: &str) -> bool {
701 if node.kind() == "for_in_statement"
703 && node_text(node, src).contains(this_field)
704 && let Some(loop_body) = node.child_by_field_name("body")
705 && has_call_expression_ts(loop_body)
706 {
707 return true;
708 }
709 if node.kind() == "call_expression" || node.kind() == "expression_statement" {
711 let text = node_text(node, src);
712 if text.contains(this_field) && text.contains("forEach") {
713 return true;
714 }
715 }
716 let mut cursor = node.walk();
717 for child in node.children(&mut cursor) {
718 if walk_for_iterate_call_ts(child, src, this_field) {
719 return true;
720 }
721 }
722 false
723}
724
725fn collect_comments(root: Node, src: &[u8]) -> Vec<cha_core::CommentInfo> {
726 let mut comments = Vec::new();
727 let mut cursor = root.walk();
728 visit_all(root, &mut cursor, &mut |n| {
729 if n.kind().contains("comment") {
730 comments.push(cha_core::CommentInfo {
731 text: node_text(n, src).to_string(),
732 line: n.start_position().row + 1,
733 });
734 }
735 });
736 comments
737}
738
739fn has_call_expression_ts(node: Node) -> bool {
740 if node.kind() == "call_expression" {
741 return true;
742 }
743 let mut cursor = node.walk();
744 for child in node.children(&mut cursor) {
745 if has_call_expression_ts(child) {
746 return true;
747 }
748 }
749 false
750}
751
752fn cognitive_complexity_ts(node: tree_sitter::Node) -> usize {
753 let mut score = 0;
754 cc_walk_ts(node, 0, &mut score);
755 score
756}
757
758fn cc_walk_ts(node: tree_sitter::Node, nesting: usize, score: &mut usize) {
759 match node.kind() {
760 "if_statement" => {
761 *score += 1 + nesting;
762 cc_children_ts(node, nesting + 1, score);
763 return;
764 }
765 "for_statement" | "for_in_statement" | "while_statement" | "do_statement" => {
766 *score += 1 + nesting;
767 cc_children_ts(node, nesting + 1, score);
768 return;
769 }
770 "switch_statement" => {
771 *score += 1 + nesting;
772 cc_children_ts(node, nesting + 1, score);
773 return;
774 }
775 "else_clause" => {
776 *score += 1;
777 }
778 "binary_expression" => {
779 if let Some(op) = node.child_by_field_name("operator")
780 && (op.kind() == "&&" || op.kind() == "||")
781 {
782 *score += 1;
783 }
784 }
785 "catch_clause" => {
786 *score += 1 + nesting;
787 cc_children_ts(node, nesting + 1, score);
788 return;
789 }
790 "arrow_function" | "function_expression" => {
791 cc_children_ts(node, nesting + 1, score);
792 return;
793 }
794 _ => {}
795 }
796 cc_children_ts(node, nesting, score);
797}
798
799fn cc_children_ts(node: tree_sitter::Node, nesting: usize, score: &mut usize) {
800 let mut cursor = node.walk();
801 for child in node.children(&mut cursor) {
802 cc_walk_ts(child, nesting, score);
803 }
804}
805
806fn collect_calls_ts(body: Option<tree_sitter::Node>, src: &[u8]) -> Vec<String> {
807 let Some(body) = body else { return Vec::new() };
808 let mut calls = Vec::new();
809 let mut cursor = body.walk();
810 visit_all(body, &mut cursor, &mut |n| {
811 if n.kind() == "call_expression"
812 && let Some(func) = n.child(0)
813 {
814 let name = node_text(func, src).to_string();
815 if !calls.contains(&name) {
816 calls.push(name);
817 }
818 }
819 });
820 calls
821}
822
823fn visit_all<F: FnMut(Node)>(node: Node, cursor: &mut tree_sitter::TreeCursor, f: &mut F) {
824 f(node);
825 if cursor.goto_first_child() {
826 loop {
827 let child_node = cursor.node();
828 let mut child_cursor = child_node.walk();
829 visit_all(child_node, &mut child_cursor, f);
830 if !cursor.goto_next_sibling() {
831 break;
832 }
833 }
834 cursor.goto_parent();
835 }
836}