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