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 let return_type = ts_return_type(node, src, imports_map);
157 Some(FunctionInfo {
158 name,
159 start_line,
160 end_line,
161 name_col,
162 name_end_col,
163 line_count: end_line - start_line + 1,
164 complexity: count_complexity(node),
165 body_hash,
166 is_exported: false,
167 parameter_count,
168 parameter_types,
169 chain_depth,
170 switch_arms,
171 external_refs,
172 is_delegating,
173 comment_lines: count_comment_lines(node),
174 referenced_fields: collect_this_fields(body, src),
175 null_check_fields: collect_null_checks_ts(body, src),
176 switch_dispatch_target: extract_switch_target_ts(body, src),
177 optional_param_count: count_optional_params_ts(node, src),
178 called_functions: collect_calls_ts(body, src),
179 cognitive_complexity: body.map(cognitive_complexity_ts).unwrap_or(0),
180 return_type,
181 })
182}
183
184fn ts_return_type(
185 node: Node,
186 src: &[u8],
187 imports_map: &crate::type_ref::ImportsMap,
188) -> Option<cha_core::TypeRef> {
189 let ann = node.child_by_field_name("return_type")?;
190 let raw = node_text(ann, src).trim_start_matches(':').trim();
191 Some(crate::type_ref::resolve(raw, imports_map))
192}
193
194fn extract_arrow_functions(
195 node: Node,
196 src: &[u8],
197 exported: bool,
198 imports_map: &crate::type_ref::ImportsMap,
199 functions: &mut Vec<FunctionInfo>,
200) {
201 let mut cursor = node.walk();
202 for child in node.children(&mut cursor) {
203 if child.kind() == "variable_declarator"
204 && let Some(f) = try_extract_arrow(child, node, src, exported, imports_map)
205 {
206 functions.push(f);
207 }
208 }
209}
210
211fn try_extract_arrow(
213 child: Node,
214 decl: Node,
215 src: &[u8],
216 exported: bool,
217 imports_map: &crate::type_ref::ImportsMap,
218) -> Option<FunctionInfo> {
219 let name_node = child.child_by_field_name("name")?;
220 let name = node_text(name_node, src).to_string();
221 let value = child.child_by_field_name("value")?;
222 if value.kind() != "arrow_function" {
223 return None;
224 }
225 let name_col = name_node.start_position().column;
226 let name_end_col = name_node.end_position().column;
227 let start_line = decl.start_position().row + 1;
228 let end_line = decl.end_position().row + 1;
229 let body = value.child_by_field_name("body");
230 let body_hash = body.map(hash_ast_structure);
231 Some(FunctionInfo {
232 name,
233 start_line,
234 end_line,
235 name_col,
236 name_end_col,
237 line_count: end_line - start_line + 1,
238 complexity: count_complexity(value),
239 body_hash,
240 is_exported: exported,
241 parameter_count: count_parameters(value),
242 parameter_types: extract_param_types(value, src, imports_map),
243 chain_depth: body.map(max_chain_depth).unwrap_or(0),
244 switch_arms: body.map(count_switch_arms).unwrap_or(0),
245 external_refs: body
246 .map(|b| collect_external_refs(b, src))
247 .unwrap_or_default(),
248 is_delegating: body.map(|b| check_delegating(b, src)).unwrap_or(false),
249 comment_lines: count_comment_lines(value),
250 referenced_fields: collect_this_fields(body, src),
251 null_check_fields: collect_null_checks_ts(body, src),
252 switch_dispatch_target: extract_switch_target_ts(body, src),
253 optional_param_count: count_optional_params_ts(value, src),
254 called_functions: collect_calls_ts(Some(value), src),
255 cognitive_complexity: cognitive_complexity_ts(value),
256 return_type: ts_return_type(value, src, imports_map),
257 })
258}
259
260fn extract_class(node: Node, src: &[u8]) -> Option<ClassInfo> {
261 let name_node = node.child_by_field_name("name")?;
262 let name = node_text(name_node, src).to_string();
263 let name_col = name_node.start_position().column;
264 let name_end_col = name_node.end_position().column;
265 let start_line = node.start_position().row + 1;
266 let end_line = node.end_position().row + 1;
267 let body = node.child_by_field_name("body")?;
268 let (methods, delegating, fields, has_behavior, cb_fields) = scan_class_body(body, src);
269 let is_interface =
270 node.kind() == "interface_declaration" || node.kind() == "abstract_class_declaration";
271 let has_listener_field = !cb_fields.is_empty();
272 let has_notify_method = has_iterate_and_call_ts(body, src, &cb_fields);
273
274 Some(ClassInfo {
275 name,
276 start_line,
277 end_line,
278 name_col,
279 name_end_col,
280 method_count: methods,
281 line_count: end_line - start_line + 1,
282 is_exported: false,
283 delegating_method_count: delegating,
284 field_count: fields.len(),
285 field_names: fields,
286 field_types: Vec::new(),
287 has_behavior,
288 is_interface,
289 parent_name: extract_parent_name(node, src),
290 override_count: 0,
291 self_call_count: 0,
292 has_listener_field,
293 has_notify_method,
294 })
295}
296
297fn scan_class_body(body: Node, src: &[u8]) -> (usize, usize, Vec<String>, bool, Vec<String>) {
299 let mut methods = 0;
300 let mut delegating = 0;
301 let mut fields = Vec::new();
302 let mut callback_fields = Vec::new();
303 let mut has_behavior = false;
304 let mut cursor = body.walk();
305 for child in body.children(&mut cursor) {
306 match child.kind() {
307 "method_definition" => {
308 let (is_behavior, is_delegating) = classify_method(child, src);
309 methods += 1;
310 has_behavior |= is_behavior;
311 delegating += usize::from(is_delegating);
312 }
313 "public_field_definition" | "property_definition" => {
314 if let Some(n) = child.child_by_field_name("name") {
315 let name = node_text(n, src).to_string();
316 if is_callback_collection_type_ts(child, src) {
317 callback_fields.push(name.clone());
318 }
319 fields.push(name);
320 }
321 }
322 _ => {}
323 }
324 }
325 (methods, delegating, fields, has_behavior, callback_fields)
326}
327
328fn classify_method(node: Node, src: &[u8]) -> (bool, bool) {
330 let mname = node
331 .child_by_field_name("name")
332 .map(|n| node_text(n, src))
333 .unwrap_or("");
334 let is_behavior = !is_accessor_name(mname) && mname != "constructor";
335 let is_delegating = node
336 .child_by_field_name("body")
337 .is_some_and(|b| check_delegating(b, src));
338 (is_behavior, is_delegating)
339}
340
341fn count_parameters(node: Node) -> usize {
342 let params = match node.child_by_field_name("parameters") {
343 Some(p) => p,
344 None => return 0,
345 };
346 let mut cursor = params.walk();
347 params
348 .children(&mut cursor)
349 .filter(|c| {
350 matches!(
351 c.kind(),
352 "required_parameter" | "optional_parameter" | "rest_parameter"
353 )
354 })
355 .count()
356}
357
358fn extract_param_types(
359 node: Node,
360 src: &[u8],
361 imports_map: &crate::type_ref::ImportsMap,
362) -> Vec<cha_core::TypeRef> {
363 let params = match node.child_by_field_name("parameters") {
364 Some(p) => p,
365 None => return vec![],
366 };
367 let mut types = Vec::new();
368 let mut cursor = params.walk();
369 for child in params.children(&mut cursor) {
370 if let Some(ann) = child.child_by_field_name("type") {
371 let raw = node_text(ann, src)
373 .trim_start_matches(':')
374 .trim()
375 .to_string();
376 types.push(crate::type_ref::resolve(raw, imports_map));
377 }
378 }
379 types
380}
381
382fn max_chain_depth(node: Node) -> usize {
383 let mut max = 0;
384 walk_chain_depth(node, &mut max);
385 max
386}
387
388fn walk_chain_depth(node: Node, max: &mut usize) {
389 if node.kind() == "member_expression" {
390 let depth = measure_chain(node);
391 if depth > *max {
392 *max = depth;
393 }
394 }
395 let mut cursor = node.walk();
396 for child in node.children(&mut cursor) {
397 walk_chain_depth(child, max);
398 }
399}
400
401fn measure_chain(node: Node) -> usize {
403 let mut depth = 0;
404 let mut current = node;
405 while current.kind() == "member_expression" {
406 depth += 1;
407 if let Some(obj) = current.child_by_field_name("object") {
408 current = obj;
409 } else {
410 break;
411 }
412 }
413 depth
414}
415
416fn count_switch_arms(node: Node) -> usize {
417 let mut count = 0;
418 walk_switch_arms(node, &mut count);
419 count
420}
421
422fn walk_switch_arms(node: Node, count: &mut usize) {
423 if node.kind() == "switch_case" || node.kind() == "switch_default" {
424 *count += 1;
425 }
426 let mut cursor = node.walk();
427 for child in node.children(&mut cursor) {
428 walk_switch_arms(child, count);
429 }
430}
431
432fn collect_external_refs(node: Node, src: &[u8]) -> Vec<String> {
433 let mut refs = Vec::new();
434 walk_external_refs(node, src, &mut refs);
435 refs.sort();
436 refs.dedup();
437 refs
438}
439
440fn member_chain_root(node: Node) -> Node {
441 let mut current = node;
442 while current.kind() == "member_expression" {
443 match current.child_by_field_name("object") {
444 Some(child) => current = child,
445 None => break,
446 }
447 }
448 current
449}
450
451fn walk_external_refs(node: Node, src: &[u8], refs: &mut Vec<String>) {
452 if node.kind() == "member_expression" {
453 let root = member_chain_root(node);
454 let text = node_text(root, src);
455 if text != "this" && text != "self" && !text.is_empty() {
456 refs.push(text.to_string());
457 }
458 }
459 let mut cursor = node.walk();
460 for child in node.children(&mut cursor) {
461 walk_external_refs(child, src, refs);
462 }
463}
464
465fn single_stmt(body: Node) -> Option<Node> {
466 let mut cursor = body.walk();
467 let stmts: Vec<_> = body
468 .children(&mut cursor)
469 .filter(|c| c.kind() != "{" && c.kind() != "}")
470 .collect();
471 (stmts.len() == 1).then(|| stmts[0])
472}
473
474fn is_external_call(node: Node, src: &[u8]) -> bool {
475 node.kind() == "call_expression"
476 && node.child_by_field_name("function").is_some_and(|func| {
477 func.kind() == "member_expression"
478 && func
479 .child_by_field_name("object")
480 .is_some_and(|obj| node_text(obj, src) != "this")
481 })
482}
483
484fn check_delegating(body: Node, src: &[u8]) -> bool {
485 let Some(stmt) = single_stmt(body) else {
486 return false;
487 };
488 let expr = match stmt.kind() {
489 "return_statement" => stmt.child(1).unwrap_or(stmt),
490 "expression_statement" => stmt.child(0).unwrap_or(stmt),
491 _ => stmt,
492 };
493 is_external_call(expr, src)
494}
495
496fn count_complexity(node: Node) -> usize {
497 let mut complexity = 1;
498 walk_complexity(node, &mut complexity);
499 complexity
500}
501
502fn walk_complexity(node: Node, count: &mut usize) {
503 match node.kind() {
504 "if_statement" | "else_clause" | "for_statement" | "for_in_statement"
505 | "while_statement" | "do_statement" | "switch_case" | "catch_clause"
506 | "ternary_expression" => {
507 *count += 1;
508 }
509 "binary_expression" => {
510 let mut cursor = node.walk();
511 for child in node.children(&mut cursor) {
512 if child.kind() == "&&" || child.kind() == "||" {
513 *count += 1;
514 }
515 }
516 }
517 _ => {}
518 }
519 let mut cursor = node.walk();
520 for child in node.children(&mut cursor) {
521 walk_complexity(child, count);
522 }
523}
524
525fn extract_import(node: Node, src: &[u8]) -> Option<ImportInfo> {
526 let mut cursor = node.walk();
527 for child in node.children(&mut cursor) {
528 if child.kind() == "string" {
529 let raw = node_text(child, src);
530 let source = raw.trim_matches(|c| c == '\'' || c == '"').to_string();
531 return Some(ImportInfo {
532 source,
533 line: node.start_position().row + 1,
534 col: node.start_position().column,
535 ..Default::default()
536 });
537 }
538 }
539 None
540}
541
542fn count_comment_lines(node: Node) -> usize {
544 let mut count = 0;
545 let mut cursor = node.walk();
546 for child in node.children(&mut cursor) {
547 if child.kind() == "comment" {
548 count += child.end_position().row - child.start_position().row + 1;
549 } else if child.child_count() > 0 {
550 count += count_comment_lines(child);
551 }
552 }
553 count
554}
555
556fn collect_this_fields(body: Option<Node>, src: &[u8]) -> Vec<String> {
559 let Some(body) = body else { return vec![] };
560 let mut refs = Vec::new();
561 collect_this_refs(body, src, &mut refs);
562 refs.sort();
563 refs.dedup();
564 refs
565}
566
567fn collect_this_refs(node: Node, src: &[u8], refs: &mut Vec<String>) {
568 if node.kind() == "member_expression"
569 && let Some(obj) = node.child_by_field_name("object")
570 && node_text(obj, src) == "this"
571 && let Some(prop) = node.child_by_field_name("property")
572 {
573 refs.push(node_text(prop, src).to_string());
574 }
575 let mut cursor = node.walk();
576 for child in node.children(&mut cursor) {
577 collect_this_refs(child, src, refs);
578 }
579}
580
581fn is_accessor_name(name: &str) -> bool {
583 let lower = name.to_lowercase();
584 lower.starts_with("get") || lower.starts_with("set") || lower.starts_with("is")
585}
586
587fn extract_parent_name(node: Node, src: &[u8]) -> Option<String> {
590 let mut cursor = node.walk();
591 for child in node.children(&mut cursor) {
592 if child.kind() == "class_heritage" {
593 let mut inner = child.walk();
594 for c in child.children(&mut inner) {
595 if c.kind() == "extends_clause" {
596 let mut ec = c.walk();
598 for e in c.children(&mut ec) {
599 if e.kind() == "identifier" || e.kind() == "type_identifier" {
600 return Some(node_text(e, src).to_string());
601 }
602 }
603 }
604 }
605 }
606 }
607 None
608}
609
610fn collect_null_checks_ts(body: Option<Node>, src: &[u8]) -> Vec<String> {
612 let Some(body) = body else { return vec![] };
613 let mut fields = Vec::new();
614 walk_null_checks_ts(body, src, &mut fields);
615 fields.sort();
616 fields.dedup();
617 fields
618}
619
620fn walk_null_checks_ts(node: Node, src: &[u8], fields: &mut Vec<String>) {
621 if node.kind() == "binary_expression"
622 && let text = node_text(node, src)
623 && (text.contains("null") || text.contains("undefined"))
624 && let Some(left) = node.child_by_field_name("left")
625 && let ltext = node_text(left, src)
626 && let Some(f) = ltext.strip_prefix("this.")
627 {
628 fields.push(f.to_string());
629 }
630 let mut cursor = node.walk();
631 for child in node.children(&mut cursor) {
632 walk_null_checks_ts(child, src, fields);
633 }
634}
635
636fn extract_switch_target_ts(body: Option<Node>, src: &[u8]) -> Option<String> {
638 let body = body?;
639 find_switch_target_ts(body, src)
640}
641
642fn find_switch_target_ts(node: Node, src: &[u8]) -> Option<String> {
643 if node.kind() == "switch_statement"
644 && let Some(value) = node.child_by_field_name("value")
645 {
646 return Some(node_text(value, src).to_string());
647 }
648 let mut cursor = node.walk();
649 for child in node.children(&mut cursor) {
650 if let Some(t) = find_switch_target_ts(child, src) {
651 return Some(t);
652 }
653 }
654 None
655}
656
657fn count_optional_params_ts(node: Node, src: &[u8]) -> usize {
659 let Some(params) = node.child_by_field_name("parameters") else {
660 return 0;
661 };
662 let mut count = 0;
663 let mut cursor = params.walk();
664 for child in params.children(&mut cursor) {
665 let text = node_text(child, src);
666 if text.contains('?') || child.child_by_field_name("value").is_some() {
667 count += 1;
668 }
669 }
670 count
671}
672
673fn is_callback_collection_type_ts(field_node: Node, src: &[u8]) -> bool {
676 let Some(ty) = field_node.child_by_field_name("type") else {
677 if let Some(init) = field_node.child_by_field_name("value") {
679 let text = node_text(init, src);
680 return text == "[]" || text.contains("new Array");
681 }
682 return false;
683 };
684 let text = node_text(ty, src);
685 (text.contains("Function") && (text.contains("[]") || text.contains("Array<")))
687 || (text.contains("=>") && text.contains("[]"))
688 || text.contains("Array<(")
689}
690
691fn has_iterate_and_call_ts(body: Node, src: &[u8], cb_fields: &[String]) -> bool {
694 if cb_fields.is_empty() {
695 return false;
696 }
697 let mut cursor = body.walk();
698 for child in body.children(&mut cursor) {
699 if child.kind() == "method_definition"
700 && let Some(fn_body) = child.child_by_field_name("body")
701 {
702 for field in cb_fields {
703 let this_field = format!("this.{field}");
704 if walk_for_iterate_call_ts(fn_body, src, &this_field) {
705 return true;
706 }
707 }
708 }
709 }
710 false
711}
712
713fn walk_for_iterate_call_ts(node: Node, src: &[u8], this_field: &str) -> bool {
714 if node.kind() == "for_in_statement"
716 && node_text(node, src).contains(this_field)
717 && let Some(loop_body) = node.child_by_field_name("body")
718 && has_call_expression_ts(loop_body)
719 {
720 return true;
721 }
722 if node.kind() == "call_expression" || node.kind() == "expression_statement" {
724 let text = node_text(node, src);
725 if text.contains(this_field) && text.contains("forEach") {
726 return true;
727 }
728 }
729 let mut cursor = node.walk();
730 for child in node.children(&mut cursor) {
731 if walk_for_iterate_call_ts(child, src, this_field) {
732 return true;
733 }
734 }
735 false
736}
737
738fn collect_comments(root: Node, src: &[u8]) -> Vec<cha_core::CommentInfo> {
739 let mut comments = Vec::new();
740 let mut cursor = root.walk();
741 visit_all(root, &mut cursor, &mut |n| {
742 if n.kind().contains("comment") {
743 comments.push(cha_core::CommentInfo {
744 text: node_text(n, src).to_string(),
745 line: n.start_position().row + 1,
746 });
747 }
748 });
749 comments
750}
751
752fn has_call_expression_ts(node: Node) -> bool {
753 if node.kind() == "call_expression" {
754 return true;
755 }
756 let mut cursor = node.walk();
757 for child in node.children(&mut cursor) {
758 if has_call_expression_ts(child) {
759 return true;
760 }
761 }
762 false
763}
764
765fn cognitive_complexity_ts(node: tree_sitter::Node) -> usize {
766 let mut score = 0;
767 cc_walk_ts(node, 0, &mut score);
768 score
769}
770
771fn cc_walk_ts(node: tree_sitter::Node, nesting: usize, score: &mut usize) {
772 match node.kind() {
773 "if_statement" => {
774 *score += 1 + nesting;
775 cc_children_ts(node, nesting + 1, score);
776 return;
777 }
778 "for_statement" | "for_in_statement" | "while_statement" | "do_statement" => {
779 *score += 1 + nesting;
780 cc_children_ts(node, nesting + 1, score);
781 return;
782 }
783 "switch_statement" => {
784 *score += 1 + nesting;
785 cc_children_ts(node, nesting + 1, score);
786 return;
787 }
788 "else_clause" => {
789 *score += 1;
790 }
791 "binary_expression" => {
792 if let Some(op) = node.child_by_field_name("operator")
793 && (op.kind() == "&&" || op.kind() == "||")
794 {
795 *score += 1;
796 }
797 }
798 "catch_clause" => {
799 *score += 1 + nesting;
800 cc_children_ts(node, nesting + 1, score);
801 return;
802 }
803 "arrow_function" | "function_expression" => {
804 cc_children_ts(node, nesting + 1, score);
805 return;
806 }
807 _ => {}
808 }
809 cc_children_ts(node, nesting, score);
810}
811
812fn cc_children_ts(node: tree_sitter::Node, nesting: usize, score: &mut usize) {
813 let mut cursor = node.walk();
814 for child in node.children(&mut cursor) {
815 cc_walk_ts(child, nesting, score);
816 }
817}
818
819fn collect_calls_ts(body: Option<tree_sitter::Node>, src: &[u8]) -> Vec<String> {
820 let Some(body) = body else { return Vec::new() };
821 let mut calls = Vec::new();
822 let mut cursor = body.walk();
823 visit_all(body, &mut cursor, &mut |n| {
824 if n.kind() == "call_expression"
825 && let Some(func) = n.child(0)
826 {
827 let name = node_text(func, src).to_string();
828 if !calls.contains(&name) {
829 calls.push(name);
830 }
831 }
832 });
833 calls
834}
835
836fn visit_all<F: FnMut(Node)>(node: Node, cursor: &mut tree_sitter::TreeCursor, f: &mut F) {
837 f(node);
838 if cursor.goto_first_child() {
839 loop {
840 let child_node = cursor.node();
841 let mut child_cursor = child_node.walk();
842 visit_all(child_node, &mut child_cursor, f);
843 if !cursor.goto_next_sibling() {
844 break;
845 }
846 }
847 cursor.goto_parent();
848 }
849}