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