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