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                ..Default::default()
483            });
484        }
485    }
486    None
487}
488
489/// Count comment lines inside a node (recursive).
490fn count_comment_lines(node: Node) -> usize {
491    let mut count = 0;
492    let mut cursor = node.walk();
493    for child in node.children(&mut cursor) {
494        if child.kind() == "comment" {
495            count += child.end_position().row - child.start_position().row + 1;
496        } else if child.child_count() > 0 {
497            count += count_comment_lines(child);
498        }
499    }
500    count
501}
502
503// cha:ignore todo_comment
504/// Collect `this.xxx` field references from a function body.
505fn collect_this_fields(body: Option<Node>, src: &[u8]) -> Vec<String> {
506    let Some(body) = body else { return vec![] };
507    let mut refs = Vec::new();
508    collect_this_refs(body, src, &mut refs);
509    refs.sort();
510    refs.dedup();
511    refs
512}
513
514fn collect_this_refs(node: Node, src: &[u8], refs: &mut Vec<String>) {
515    if node.kind() == "member_expression"
516        && let Some(obj) = node.child_by_field_name("object")
517        && node_text(obj, src) == "this"
518        && let Some(prop) = node.child_by_field_name("property")
519    {
520        refs.push(node_text(prop, src).to_string());
521    }
522    let mut cursor = node.walk();
523    for child in node.children(&mut cursor) {
524        collect_this_refs(child, src, refs);
525    }
526}
527
528/// Check if a method name looks like a getter/setter.
529fn is_accessor_name(name: &str) -> bool {
530    let lower = name.to_lowercase();
531    lower.starts_with("get") || lower.starts_with("set") || lower.starts_with("is")
532}
533
534/// Extract parent class name from `extends` clause.
535// cha:ignore cognitive_complexity
536fn extract_parent_name(node: Node, src: &[u8]) -> Option<String> {
537    let mut cursor = node.walk();
538    for child in node.children(&mut cursor) {
539        if child.kind() == "class_heritage" {
540            let mut inner = child.walk();
541            for c in child.children(&mut inner) {
542                if c.kind() == "extends_clause" {
543                    // First identifier child is the parent name
544                    let mut ec = c.walk();
545                    for e in c.children(&mut ec) {
546                        if e.kind() == "identifier" || e.kind() == "type_identifier" {
547                            return Some(node_text(e, src).to_string());
548                        }
549                    }
550                }
551            }
552        }
553    }
554    None
555}
556
557/// Collect field names checked for null/undefined in TS.
558fn collect_null_checks_ts(body: Option<Node>, src: &[u8]) -> Vec<String> {
559    let Some(body) = body else { return vec![] };
560    let mut fields = Vec::new();
561    walk_null_checks_ts(body, src, &mut fields);
562    fields.sort();
563    fields.dedup();
564    fields
565}
566
567fn walk_null_checks_ts(node: Node, src: &[u8], fields: &mut Vec<String>) {
568    if node.kind() == "binary_expression"
569        && let text = node_text(node, src)
570        && (text.contains("null") || text.contains("undefined"))
571        && let Some(left) = node.child_by_field_name("left")
572        && let ltext = node_text(left, src)
573        && let Some(f) = ltext.strip_prefix("this.")
574    {
575        fields.push(f.to_string());
576    }
577    let mut cursor = node.walk();
578    for child in node.children(&mut cursor) {
579        walk_null_checks_ts(child, src, fields);
580    }
581}
582
583/// Extract switch dispatch target in TS.
584fn extract_switch_target_ts(body: Option<Node>, src: &[u8]) -> Option<String> {
585    let body = body?;
586    find_switch_target_ts(body, src)
587}
588
589fn find_switch_target_ts(node: Node, src: &[u8]) -> Option<String> {
590    if node.kind() == "switch_statement"
591        && let Some(value) = node.child_by_field_name("value")
592    {
593        return Some(node_text(value, src).to_string());
594    }
595    let mut cursor = node.walk();
596    for child in node.children(&mut cursor) {
597        if let Some(t) = find_switch_target_ts(child, src) {
598            return Some(t);
599        }
600    }
601    None
602}
603
604/// Count optional parameters in TS (those with ? or default value).
605fn count_optional_params_ts(node: Node, src: &[u8]) -> usize {
606    let Some(params) = node.child_by_field_name("parameters") else {
607        return 0;
608    };
609    let mut count = 0;
610    let mut cursor = params.walk();
611    for child in params.children(&mut cursor) {
612        let text = node_text(child, src);
613        if text.contains('?') || child.child_by_field_name("value").is_some() {
614            count += 1;
615        }
616    }
617    count
618}
619
620/// Check if a field's type annotation is a callback collection.
621/// Matches: `Function[]`, `Array<Function>`, `(() => void)[]`, `((x: T) => R)[]`
622fn is_callback_collection_type_ts(field_node: Node, src: &[u8]) -> bool {
623    let Some(ty) = field_node.child_by_field_name("type") else {
624        // No type annotation — check initializer for array literal
625        if let Some(init) = field_node.child_by_field_name("value") {
626            let text = node_text(init, src);
627            return text == "[]" || text.contains("new Array");
628        }
629        return false;
630    };
631    let text = node_text(ty, src);
632    // Function[] or Array<Function> or (() => void)[] or ((...) => ...)[]
633    (text.contains("Function") && (text.contains("[]") || text.contains("Array<")))
634        || (text.contains("=>") && text.contains("[]"))
635        || text.contains("Array<(")
636}
637
638/// Structural Observer detection for TS: method iterates a callback field and calls elements.
639/// Pattern: `this.field.forEach(cb => cb(...))` or `for (const cb of this.field) { cb(...) }`
640fn has_iterate_and_call_ts(body: Node, src: &[u8], cb_fields: &[String]) -> bool {
641    if cb_fields.is_empty() {
642        return false;
643    }
644    let mut cursor = body.walk();
645    for child in body.children(&mut cursor) {
646        if child.kind() == "method_definition"
647            && let Some(fn_body) = child.child_by_field_name("body")
648        {
649            for field in cb_fields {
650                let this_field = format!("this.{field}");
651                if walk_for_iterate_call_ts(fn_body, src, &this_field) {
652                    return true;
653                }
654            }
655        }
656    }
657    false
658}
659
660fn walk_for_iterate_call_ts(node: Node, src: &[u8], this_field: &str) -> bool {
661    // for (const x of this.field) { x(...) }
662    if node.kind() == "for_in_statement"
663        && node_text(node, src).contains(this_field)
664        && let Some(loop_body) = node.child_by_field_name("body")
665        && has_call_expression_ts(loop_body)
666    {
667        return true;
668    }
669    // this.field.forEach(cb => cb(...))
670    if node.kind() == "call_expression" || node.kind() == "expression_statement" {
671        let text = node_text(node, src);
672        if text.contains(this_field) && text.contains("forEach") {
673            return true;
674        }
675    }
676    let mut cursor = node.walk();
677    for child in node.children(&mut cursor) {
678        if walk_for_iterate_call_ts(child, src, this_field) {
679            return true;
680        }
681    }
682    false
683}
684
685fn collect_comments(root: Node, src: &[u8]) -> Vec<cha_core::CommentInfo> {
686    let mut comments = Vec::new();
687    let mut cursor = root.walk();
688    visit_all(root, &mut cursor, &mut |n| {
689        if n.kind().contains("comment") {
690            comments.push(cha_core::CommentInfo {
691                text: node_text(n, src).to_string(),
692                line: n.start_position().row + 1,
693            });
694        }
695    });
696    comments
697}
698
699fn has_call_expression_ts(node: Node) -> bool {
700    if node.kind() == "call_expression" {
701        return true;
702    }
703    let mut cursor = node.walk();
704    for child in node.children(&mut cursor) {
705        if has_call_expression_ts(child) {
706            return true;
707        }
708    }
709    false
710}
711
712fn cognitive_complexity_ts(node: tree_sitter::Node) -> usize {
713    let mut score = 0;
714    cc_walk_ts(node, 0, &mut score);
715    score
716}
717
718fn cc_walk_ts(node: tree_sitter::Node, nesting: usize, score: &mut usize) {
719    match node.kind() {
720        "if_statement" => {
721            *score += 1 + nesting;
722            cc_children_ts(node, nesting + 1, score);
723            return;
724        }
725        "for_statement" | "for_in_statement" | "while_statement" | "do_statement" => {
726            *score += 1 + nesting;
727            cc_children_ts(node, nesting + 1, score);
728            return;
729        }
730        "switch_statement" => {
731            *score += 1 + nesting;
732            cc_children_ts(node, nesting + 1, score);
733            return;
734        }
735        "else_clause" => {
736            *score += 1;
737        }
738        "binary_expression" => {
739            if let Some(op) = node.child_by_field_name("operator")
740                && (op.kind() == "&&" || op.kind() == "||")
741            {
742                *score += 1;
743            }
744        }
745        "catch_clause" => {
746            *score += 1 + nesting;
747            cc_children_ts(node, nesting + 1, score);
748            return;
749        }
750        "arrow_function" | "function_expression" => {
751            cc_children_ts(node, nesting + 1, score);
752            return;
753        }
754        _ => {}
755    }
756    cc_children_ts(node, nesting, score);
757}
758
759fn cc_children_ts(node: tree_sitter::Node, nesting: usize, score: &mut usize) {
760    let mut cursor = node.walk();
761    for child in node.children(&mut cursor) {
762        cc_walk_ts(child, nesting, score);
763    }
764}
765
766fn collect_calls_ts(body: Option<tree_sitter::Node>, src: &[u8]) -> Vec<String> {
767    let Some(body) = body else { return Vec::new() };
768    let mut calls = Vec::new();
769    let mut cursor = body.walk();
770    visit_all(body, &mut cursor, &mut |n| {
771        if n.kind() == "call_expression"
772            && let Some(func) = n.child(0)
773        {
774            let name = node_text(func, src).to_string();
775            if !calls.contains(&name) {
776                calls.push(name);
777            }
778        }
779    });
780    calls
781}
782
783fn visit_all<F: FnMut(Node)>(node: Node, cursor: &mut tree_sitter::TreeCursor, f: &mut F) {
784    f(node);
785    if cursor.goto_first_child() {
786        loop {
787            let child_node = cursor.node();
788            let mut child_cursor = child_node.walk();
789            visit_all(child_node, &mut child_cursor, f);
790            if !cursor.goto_next_sibling() {
791                break;
792            }
793        }
794        cursor.goto_parent();
795    }
796}