Skip to main content

cha_parser/
typescript.rs

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