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