Skip to main content

cha_parser/
typescript.rs

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