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