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