Skip to main content

cha_parser/
rust_lang.rs

1use std::collections::hash_map::DefaultHasher;
2use std::hash::{Hash, Hasher};
3
4use cha_core::{ClassInfo, FunctionInfo, ImportInfo, SourceFile, SourceModel};
5use tree_sitter::{Node, Parser};
6
7use crate::LanguageParser;
8
9pub struct RustParser;
10
11impl LanguageParser for RustParser {
12    fn language_name(&self) -> &str {
13        "rust"
14    }
15
16    fn parse(&self, file: &SourceFile) -> Option<SourceModel> {
17        let mut parser = Parser::new();
18        parser
19            .set_language(&tree_sitter_rust::LANGUAGE.into())
20            .ok()?;
21        let tree = parser.parse(&file.content, None)?;
22        let root = tree.root_node();
23        let src = file.content.as_bytes();
24
25        let mut ctx = ParseContext::new(src);
26        ctx.collect_nodes(root, false);
27
28        Some(SourceModel {
29            language: "rust".into(),
30            total_lines: file.line_count(),
31            functions: ctx.col.functions,
32            classes: ctx.col.classes,
33            imports: ctx.col.imports,
34            comments: collect_comments(root, src),
35            type_aliases: vec![], // TODO(parser): extract type aliases from 'type X = Y' declarations
36        })
37    }
38}
39
40/// Accumulator for collected AST items.
41struct Collector {
42    functions: Vec<FunctionInfo>,
43    classes: Vec<ClassInfo>,
44    imports: Vec<ImportInfo>,
45}
46
47/// Bundles source bytes and collector to eliminate repeated parameter passing.
48struct ParseContext<'a> {
49    src: &'a [u8],
50    col: Collector,
51    last_self_call_count: usize,
52    last_has_notify: bool,
53    /// Callback collection field names per class name.
54    callback_fields: std::collections::HashMap<String, Vec<String>>,
55}
56
57impl<'a> ParseContext<'a> {
58    fn new(src: &'a [u8]) -> Self {
59        Self {
60            src,
61            last_self_call_count: 0,
62            last_has_notify: false,
63            callback_fields: std::collections::HashMap::new(),
64            col: Collector {
65                functions: Vec::new(),
66                classes: Vec::new(),
67                imports: Vec::new(),
68            },
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            "function_item" => self.push_function(child, exported),
82            "impl_item" => self.extract_impl_methods(child),
83            "struct_item" | "enum_item" | "trait_item" => self.push_struct(child),
84            "use_declaration" => self.push_import(child),
85            "mod_item" => self.push_mod_as_import(child),
86            _ => self.collect_nodes(child, false),
87        }
88    }
89
90    fn push_function(&mut self, node: Node, exported: bool) {
91        if let Some(mut f) = extract_function(node, self.src) {
92            f.is_exported = exported || has_pub(node);
93            self.col.functions.push(f);
94        }
95    }
96
97    fn push_struct(&mut self, node: Node) {
98        if let Some((mut c, cb_fields)) = extract_struct(node, self.src) {
99            c.is_exported = has_pub(node);
100            if !cb_fields.is_empty() {
101                self.callback_fields.insert(c.name.clone(), cb_fields);
102            }
103            self.col.classes.push(c);
104        }
105    }
106
107    fn push_import(&mut self, node: Node) {
108        if let Some(i) = extract_use(node, self.src) {
109            self.col.imports.push(i);
110        }
111    }
112
113    /// Treat `mod foo;` as an import of `foo.rs` (file-level dependency).
114    fn push_mod_as_import(&mut self, node: Node) {
115        // Skip inline mod blocks: `mod foo { ... }` has a body
116        if node.child_by_field_name("body").is_some() {
117            return;
118        }
119        if let Some(name_node) = node.child_by_field_name("name") {
120            let name = node_text(name_node, self.src);
121            self.col.imports.push(crate::ImportInfo {
122                source: format!("{name}.rs"),
123                line: node.start_position().row + 1,
124                col: node.start_position().column,
125                is_module_decl: true,
126            });
127        }
128    }
129
130    fn extract_impl_methods(&mut self, node: Node) {
131        let Some(body) = node.child_by_field_name("body") else {
132            return;
133        };
134        let impl_name = node
135            .child_by_field_name("type")
136            .map(|t| node_text(t, self.src).to_string());
137        let trait_name = node
138            .child_by_field_name("trait")
139            .map(|t| node_text(t, self.src).to_string());
140
141        let cb_fields = impl_name
142            .as_ref()
143            .and_then(|n| self.callback_fields.get(n))
144            .cloned()
145            .unwrap_or_default();
146
147        let (methods, delegating, has_behavior) = self.scan_impl_body(body, &cb_fields);
148
149        if let Some(name) = &impl_name
150            && let Some(class) = self.col.classes.iter_mut().find(|c| &c.name == name)
151        {
152            class.method_count += methods;
153            class.delegating_method_count += delegating;
154            class.has_behavior |= has_behavior;
155            class.self_call_count = class.self_call_count.max(self.last_self_call_count);
156            class.has_notify_method |= self.last_has_notify;
157            if let Some(t) = &trait_name {
158                class.parent_name = Some(t.clone());
159            }
160        }
161    }
162
163    fn scan_impl_body(&mut self, body: Node, cb_fields: &[String]) -> (usize, usize, bool) {
164        let mut methods = 0;
165        let mut delegating = 0;
166        let mut has_behavior = false;
167        let mut max_self_calls = 0;
168        let mut has_notify = false;
169        let mut cursor = body.walk();
170        for child in body.children(&mut cursor) {
171            if child.kind() == "function_item"
172                && let Some(mut f) = extract_function(child, self.src)
173            {
174                f.is_exported = has_pub(child);
175                methods += 1;
176                if f.is_delegating {
177                    delegating += 1;
178                }
179                if f.line_count > 3 {
180                    has_behavior = true;
181                }
182                let fn_body = child.child_by_field_name("body");
183                let self_calls = count_self_method_calls(fn_body, self.src);
184                max_self_calls = max_self_calls.max(self_calls);
185                // Structural Observer: method iterates a callback field and calls elements
186                if !has_notify && has_iterate_and_call(fn_body, self.src, cb_fields) {
187                    has_notify = true;
188                }
189                self.col.functions.push(f);
190            }
191        }
192        // Store extra signals in the class via the caller
193        self.last_self_call_count = max_self_calls;
194        self.last_has_notify = has_notify;
195        (methods, delegating, has_behavior)
196    }
197}
198
199// Extract and push a function item node.
200fn node_text<'a>(node: Node, src: &'a [u8]) -> &'a str {
201    node.utf8_text(src).unwrap_or("")
202}
203
204fn has_pub(node: Node) -> bool {
205    let mut cursor = node.walk();
206    node.children(&mut cursor)
207        .any(|c| c.kind() == "visibility_modifier")
208}
209
210fn hash_ast_structure(node: Node) -> u64 {
211    let mut hasher = DefaultHasher::new();
212    walk_hash(node, &mut hasher);
213    hasher.finish()
214}
215
216fn walk_hash(node: Node, hasher: &mut DefaultHasher) {
217    node.kind().hash(hasher);
218    let mut cursor = node.walk();
219    for child in node.children(&mut cursor) {
220        walk_hash(child, hasher);
221    }
222}
223
224fn count_complexity(node: Node) -> usize {
225    let mut complexity = 1;
226    walk_complexity(node, &mut complexity);
227    complexity
228}
229
230fn walk_complexity(node: Node, count: &mut usize) {
231    match node.kind() {
232        "if_expression" | "else_clause" | "for_expression" | "while_expression"
233        | "loop_expression" | "match_arm" | "closure_expression" => {
234            *count += 1;
235        }
236        "binary_expression" => {
237            let mut cursor = node.walk();
238            for child in node.children(&mut cursor) {
239                if child.kind() == "&&" || child.kind() == "||" {
240                    *count += 1;
241                }
242            }
243        }
244        _ => {}
245    }
246    let mut cursor = node.walk();
247    for child in node.children(&mut cursor) {
248        walk_complexity(child, count);
249    }
250}
251
252fn extract_function(node: Node, src: &[u8]) -> Option<FunctionInfo> {
253    let name_node = node.child_by_field_name("name")?;
254    let name = node_text(name_node, src).to_string();
255    let name_col = name_node.start_position().column;
256    let name_end_col = name_node.end_position().column;
257    let start_line = node.start_position().row + 1;
258    let end_line = node.end_position().row + 1;
259    let body = node.child_by_field_name("body");
260    let body_hash = body.map(hash_ast_structure);
261    let parameter_count = count_parameters(node);
262    let parameter_types = extract_param_types(node, src);
263    let chain_depth = body.map(max_chain_depth).unwrap_or(0);
264    let switch_arms = body.map(count_switch_arms).unwrap_or(0);
265    let external_refs = body
266        .map(|b| collect_external_refs(b, src))
267        .unwrap_or_default();
268    let is_delegating = body.map(|b| check_delegating(b, src)).unwrap_or(false);
269    Some(FunctionInfo {
270        name,
271        start_line,
272        end_line,
273        name_col,
274        name_end_col,
275        line_count: end_line - start_line + 1,
276        complexity: count_complexity(node),
277        body_hash,
278        is_exported: false,
279        parameter_count,
280        parameter_types,
281        chain_depth,
282        switch_arms,
283        external_refs,
284        is_delegating,
285        comment_lines: count_comment_lines(node, src),
286        referenced_fields: collect_field_refs(body, src),
287        null_check_fields: collect_null_checks(body, src),
288        switch_dispatch_target: extract_switch_target(body, src),
289        optional_param_count: count_optional_params(node, src),
290        called_functions: collect_calls_rs(body, src),
291        cognitive_complexity: body.map(cognitive_complexity_rs).unwrap_or(0),
292    })
293}
294
295fn extract_struct(node: Node, src: &[u8]) -> Option<(ClassInfo, Vec<String>)> {
296    let name_node = node.child_by_field_name("name")?;
297    let name = node_text(name_node, src).to_string();
298    let name_col = name_node.start_position().column;
299    let name_end_col = name_node.end_position().column;
300    let start_line = node.start_position().row + 1;
301    let end_line = node.end_position().row + 1;
302    let (field_count, field_names, callback_fields) = extract_fields(node, src);
303    let is_interface = node.kind() == "trait_item";
304    let has_listener_field = !callback_fields.is_empty();
305    Some((
306        ClassInfo {
307            name,
308            start_line,
309            end_line,
310            name_col,
311            name_end_col,
312            method_count: 0,
313            line_count: end_line - start_line + 1,
314            is_exported: false,
315            delegating_method_count: 0,
316            field_count,
317            field_names,
318            field_types: Vec::new(),
319            has_behavior: false,
320            is_interface,
321            parent_name: None,
322            override_count: 0,
323            self_call_count: 0,
324            has_listener_field,
325            has_notify_method: false,
326        },
327        callback_fields,
328    ))
329}
330
331fn count_parameters(node: Node) -> usize {
332    let params = match node.child_by_field_name("parameters") {
333        Some(p) => p,
334        None => return 0,
335    };
336    let mut cursor = params.walk();
337    params
338        .children(&mut cursor)
339        .filter(|c| c.kind() == "parameter" || c.kind() == "self_parameter")
340        .count()
341}
342
343fn extract_param_types(node: Node, src: &[u8]) -> Vec<String> {
344    let params = match node.child_by_field_name("parameters") {
345        Some(p) => p,
346        None => return vec![],
347    };
348    let mut types = Vec::new();
349    let mut cursor = params.walk();
350    for child in params.children(&mut cursor) {
351        if child.kind() == "parameter"
352            && let Some(ty) = child.child_by_field_name("type")
353        {
354            types.push(normalize_type(node_text(ty, src)));
355        }
356    }
357    types.sort();
358    types
359}
360
361/// Strip reference/lifetime wrappers to get the canonical type name.
362fn normalize_type(raw: &str) -> String {
363    raw.trim_start_matches('&')
364        .trim_start_matches("mut ")
365        .trim()
366        .to_string()
367}
368
369fn max_chain_depth(node: Node) -> usize {
370    let mut max = 0;
371    walk_chain_depth(node, &mut max);
372    max
373}
374
375fn walk_chain_depth(node: Node, max: &mut usize) {
376    if node.kind() == "field_expression" {
377        let depth = measure_chain(node);
378        if depth > *max {
379            *max = depth;
380        }
381    }
382    let mut cursor = node.walk();
383    for child in node.children(&mut cursor) {
384        walk_chain_depth(child, max);
385    }
386}
387
388/// Count consecutive field accesses (a.b.c.d), skipping method calls.
389fn measure_chain(node: Node) -> usize {
390    let mut depth = 0;
391    let mut current = node;
392    while current.kind() == "field_expression" {
393        depth += 1;
394        if let Some(obj) = current.child_by_field_name("value") {
395            current = obj;
396        } else {
397            break;
398        }
399    }
400    depth
401}
402
403fn count_switch_arms(node: Node) -> usize {
404    let mut count = 0;
405    walk_switch_arms(node, &mut count);
406    count
407}
408
409fn walk_switch_arms(node: Node, count: &mut usize) {
410    if node.kind() == "match_arm" {
411        *count += 1;
412    }
413    let mut cursor = node.walk();
414    for child in node.children(&mut cursor) {
415        walk_switch_arms(child, count);
416    }
417}
418
419fn collect_external_refs(node: Node, src: &[u8]) -> Vec<String> {
420    let mut refs = Vec::new();
421    walk_external_refs(node, src, &mut refs);
422    refs.sort();
423    refs.dedup();
424    refs
425}
426
427/// Walk a field_expression chain to its root identifier.
428fn field_chain_root(node: Node) -> Node {
429    let mut current = node;
430    while current.kind() == "field_expression" {
431        match current.child_by_field_name("value") {
432            Some(child) => current = child,
433            None => break,
434        }
435    }
436    current
437}
438
439fn walk_external_refs(node: Node, src: &[u8], refs: &mut Vec<String>) {
440    if node.kind() == "field_expression" {
441        // Walk to the root object of the chain (a.b.c → a)
442        let root = field_chain_root(node);
443        let text = node_text(root, src);
444        if text != "self" && !text.is_empty() {
445            refs.push(text.to_string());
446        }
447    }
448    let mut cursor = node.walk();
449    for child in node.children(&mut cursor) {
450        walk_external_refs(child, src, refs);
451    }
452}
453
454/// Extract the single statement from a block body, if exactly one exists.
455fn single_stmt(body: Node) -> Option<Node> {
456    let mut cursor = body.walk();
457    let stmts: Vec<_> = body
458        .children(&mut cursor)
459        .filter(|c| c.kind() != "{" && c.kind() != "}")
460        .collect();
461    (stmts.len() == 1).then(|| stmts[0])
462}
463
464/// Check if a node is a method call on an external object (not self).
465fn is_external_call(node: Node, src: &[u8]) -> bool {
466    node.kind() == "call_expression"
467        && node.child_by_field_name("function").is_some_and(|func| {
468            func.kind() == "field_expression"
469                && func
470                    .child_by_field_name("value")
471                    .is_some_and(|obj| node_text(obj, src) != "self")
472        })
473}
474
475fn check_delegating(body: Node, src: &[u8]) -> bool {
476    let Some(stmt) = single_stmt(body) else {
477        return false;
478    };
479    let expr = match stmt.kind() {
480        "expression_statement" => stmt.child(0).unwrap_or(stmt),
481        "return_expression" => stmt.child(1).unwrap_or(stmt),
482        _ => stmt,
483    };
484    is_external_call(expr, src)
485}
486
487fn extract_use(node: Node, src: &[u8]) -> Option<ImportInfo> {
488    let text = node_text(node, src);
489    // Extract the path from "use foo::bar::baz;"
490    let source = text
491        .strip_prefix("use ")?
492        .trim_end_matches(';')
493        .trim()
494        .to_string();
495    Some(ImportInfo {
496        source,
497        line: node.start_position().row + 1,
498        col: node.start_position().column,
499        ..Default::default()
500    })
501}
502
503/// Count comment lines inside a function node.
504fn count_comment_lines(node: Node, src: &[u8]) -> usize {
505    let mut count = 0;
506    let mut cursor = node.walk();
507    for child in node.children(&mut cursor) {
508        if child.kind() == "line_comment" || child.kind() == "block_comment" {
509            count += child.end_position().row - child.start_position().row + 1;
510        }
511    }
512    // Also recurse into body
513    if let Some(body) = node.child_by_field_name("body") {
514        count += count_comment_lines_recursive(body, src);
515    }
516    count
517}
518
519fn count_comment_lines_recursive(node: Node, _src: &[u8]) -> usize {
520    let mut count = 0;
521    let mut cursor = node.walk();
522    for child in node.children(&mut cursor) {
523        if child.kind() == "line_comment" || child.kind() == "block_comment" {
524            count += child.end_position().row - child.start_position().row + 1;
525        } else if child.child_count() > 0 {
526            count += count_comment_lines_recursive(child, _src);
527        }
528    }
529    count
530}
531
532// cha:ignore todo_comment
533/// Collect field references (self.xxx) from a function body.
534fn collect_field_refs(body: Option<Node>, src: &[u8]) -> Vec<String> {
535    let Some(body) = body else { return vec![] };
536    let mut refs = Vec::new();
537    collect_self_fields(body, src, &mut refs);
538    refs.sort();
539    refs.dedup();
540    refs
541}
542
543fn collect_self_fields(node: Node, src: &[u8], refs: &mut Vec<String>) {
544    if node.kind() == "field_expression"
545        && let Some(obj) = node.child_by_field_name("value")
546        && node_text(obj, src) == "self"
547        && let Some(field) = node.child_by_field_name("field")
548    {
549        refs.push(node_text(field, src).to_string());
550    }
551    let mut cursor = node.walk();
552    for child in node.children(&mut cursor) {
553        collect_self_fields(child, src, refs);
554    }
555}
556
557/// Extract field names from a struct body.
558/// Returns (field_count, field_names, callback_collection_field_names).
559fn extract_fields(node: Node, src: &[u8]) -> (usize, Vec<String>, Vec<String>) {
560    let mut names = Vec::new();
561    let mut callback_fields = Vec::new();
562    if let Some(body) = node.child_by_field_name("body") {
563        let mut cursor = body.walk();
564        for child in body.children(&mut cursor) {
565            if child.kind() == "field_declaration"
566                && let Some(name_node) = child.child_by_field_name("name")
567            {
568                let name = node_text(name_node, src).to_string();
569                if let Some(ty) = child.child_by_field_name("type")
570                    && is_callback_collection_type_rs(node_text(ty, src))
571                {
572                    callback_fields.push(name.clone());
573                }
574                names.push(name);
575            }
576        }
577    }
578    (names.len(), names, callback_fields)
579}
580
581/// Check if a type looks like a collection of callbacks: Vec<Box<dyn Fn*>>, Vec<fn(...)>.
582fn is_callback_collection_type_rs(ty: &str) -> bool {
583    if !ty.contains("Vec<") {
584        return false;
585    }
586    ty.contains("Fn(") || ty.contains("FnMut(") || ty.contains("FnOnce(") || ty.contains("fn(")
587}
588
589/// Collect field names checked for None/null in match/if-let patterns.
590fn collect_null_checks(body: Option<Node>, src: &[u8]) -> Vec<String> {
591    let Some(body) = body else { return vec![] };
592    let mut fields = Vec::new();
593    walk_null_checks_rs(body, src, &mut fields);
594    fields.sort();
595    fields.dedup();
596    fields
597}
598
599fn walk_null_checks_rs(node: Node, src: &[u8], fields: &mut Vec<String>) {
600    if node.kind() == "if_let_expression" {
601        // if let Some(x) = self.field { ... }
602        if let Some(pattern) = node.child_by_field_name("pattern")
603            && node_text(pattern, src).contains("Some")
604            && let Some(value) = node.child_by_field_name("value")
605        {
606            let vtext = node_text(value, src);
607            if let Some(f) = vtext.strip_prefix("self.") {
608                fields.push(f.to_string());
609            }
610        }
611    } else if node.kind() == "if_expression"
612        && let Some(cond) = node.child_by_field_name("condition")
613    {
614        let text = node_text(cond, src);
615        if text.contains("is_some") || text.contains("is_none") {
616            extract_null_checked_fields(text, fields);
617        }
618    }
619    let mut cursor = node.walk();
620    for child in node.children(&mut cursor) {
621        walk_null_checks_rs(child, src, fields);
622    }
623}
624
625/// Extract self.field names from a condition text containing null checks.
626fn extract_null_checked_fields(text: &str, fields: &mut Vec<String>) {
627    if !(text.contains("is_some") || text.contains("is_none") || text.contains("Some")) {
628        return;
629    }
630    for part in text.split("self.") {
631        if let Some(field) = part
632            .split(|c: char| !c.is_alphanumeric() && c != '_')
633            .next()
634            && !field.is_empty()
635            && field != "is_some"
636            && field != "is_none"
637        {
638            fields.push(field.to_string());
639        }
640    }
641}
642
643/// Extract the variable/field being dispatched on in a match expression.
644fn extract_switch_target(body: Option<Node>, src: &[u8]) -> Option<String> {
645    let body = body?;
646    find_match_target(body, src)
647}
648
649fn find_match_target(node: Node, src: &[u8]) -> Option<String> {
650    if node.kind() == "match_expression"
651        && let Some(value) = node.child_by_field_name("value")
652    {
653        return Some(node_text(value, src).to_string());
654    }
655    let mut cursor = node.walk();
656    for child in node.children(&mut cursor) {
657        if let Some(t) = find_match_target(child, src) {
658            return Some(t);
659        }
660    }
661    None
662}
663
664/// Count optional parameters (those with default values or Option<T> type).
665fn count_optional_params(node: Node, src: &[u8]) -> usize {
666    let Some(params) = node.child_by_field_name("parameters") else {
667        return 0;
668    };
669    let mut count = 0;
670    let mut cursor = params.walk();
671    for child in params.children(&mut cursor) {
672        if child.kind() == "parameter" {
673            let text = node_text(child, src);
674            if text.contains("Option<") {
675                count += 1;
676            }
677        }
678    }
679    count
680}
681
682/// Count self.method() calls in a function body (for Template Method detection).
683fn count_self_method_calls(body: Option<Node>, src: &[u8]) -> usize {
684    let Some(body) = body else { return 0 };
685    let mut count = 0;
686    walk_self_calls(body, src, &mut count);
687    count
688}
689
690fn walk_self_calls(node: Node, src: &[u8], count: &mut usize) {
691    if node.kind() == "call_expression"
692        && let Some(func) = node.child_by_field_name("function")
693        && node_text(func, src).starts_with("self.")
694    {
695        *count += 1;
696    }
697    let mut cursor = node.walk();
698    for child in node.children(&mut cursor) {
699        walk_self_calls(child, src, count);
700    }
701}
702
703/// Structural Observer detection: method body iterates a callback collection field and calls elements.
704/// Pattern: `for cb in &self.field { cb(...) }` or `self.field.iter().for_each(|cb| cb(...))`
705fn has_iterate_and_call(body: Option<Node>, src: &[u8], cb_fields: &[String]) -> bool {
706    let Some(body) = body else { return false };
707    for field in cb_fields {
708        let self_field = format!("self.{field}");
709        if walk_for_iterate_call(body, src, &self_field) {
710            return true;
711        }
712    }
713    false
714}
715
716fn walk_for_iterate_call(node: Node, src: &[u8], self_field: &str) -> bool {
717    // for x in &self.field { x(...) }
718    if node.kind() == "for_expression"
719        && let Some(value) = node.child_by_field_name("value")
720        && node_text(value, src).contains(self_field)
721        && let Some(loop_body) = node.child_by_field_name("body")
722        && has_call_expression(loop_body)
723    {
724        return true;
725    }
726    // self.field.iter().for_each(|cb| cb(...))
727    if node.kind() == "call_expression" {
728        let text = node_text(node, src);
729        if text.contains(self_field) && text.contains("for_each") {
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(child, src, self_field) {
736            return true;
737        }
738    }
739    false
740}
741
742fn cognitive_complexity_rs(node: Node) -> usize {
743    let mut score = 0;
744    cc_walk_rs(node, 0, &mut score);
745    score
746}
747
748fn cc_walk_rs(node: Node, nesting: usize, score: &mut usize) {
749    match node.kind() {
750        "if_expression" => {
751            *score += 1 + nesting;
752            cc_children_rs(node, nesting + 1, score);
753            return;
754        }
755        "for_expression" | "while_expression" | "loop_expression" => {
756            *score += 1 + nesting;
757            cc_children_rs(node, nesting + 1, score);
758            return;
759        }
760        "match_expression" => {
761            *score += 1 + nesting;
762            cc_children_rs(node, nesting + 1, score);
763            return;
764        }
765        "else_clause" => {
766            *score += 1;
767        }
768        "binary_expression" => {
769            if let Some(op) = node.child_by_field_name("operator")
770                && (op.kind() == "&&" || op.kind() == "||")
771            {
772                *score += 1;
773            }
774        }
775        "closure_expression" => {
776            cc_children_rs(node, nesting + 1, score);
777            return;
778        }
779        _ => {}
780    }
781    cc_children_rs(node, nesting, score);
782}
783
784fn cc_children_rs(node: Node, nesting: usize, score: &mut usize) {
785    let mut cursor = node.walk();
786    for child in node.children(&mut cursor) {
787        cc_walk_rs(child, nesting, score);
788    }
789}
790
791fn collect_calls_rs(body: Option<tree_sitter::Node>, src: &[u8]) -> Vec<String> {
792    let Some(body) = body else { return Vec::new() };
793    let mut calls = Vec::new();
794    let mut cursor = body.walk();
795    visit_all(body, &mut cursor, &mut |n| {
796        if n.kind() == "call_expression"
797            && let Some(func) = n.child(0)
798        {
799            let name = node_text(func, src).to_string();
800            if !calls.contains(&name) {
801                calls.push(name);
802            }
803        }
804    });
805    calls
806}
807
808fn visit_all<F: FnMut(Node)>(node: Node, cursor: &mut tree_sitter::TreeCursor, f: &mut F) {
809    f(node);
810    if cursor.goto_first_child() {
811        loop {
812            visit_all(cursor.node(), cursor, f);
813            if !cursor.goto_next_sibling() {
814                break;
815            }
816        }
817        cursor.goto_parent();
818    }
819}
820
821fn collect_comments(root: Node, src: &[u8]) -> Vec<cha_core::CommentInfo> {
822    let mut comments = Vec::new();
823    let mut cursor = root.walk();
824    visit_all(root, &mut cursor, &mut |n| {
825        if n.kind().contains("comment") {
826            comments.push(cha_core::CommentInfo {
827                text: node_text(n, src).to_string(),
828                line: n.start_position().row + 1,
829            });
830        }
831    });
832    comments
833}
834
835fn has_call_expression(node: Node) -> bool {
836    if node.kind() == "call_expression" {
837        return true;
838    }
839    let mut cursor = node.walk();
840    for child in node.children(&mut cursor) {
841        if has_call_expression(child) {
842            return true;
843        }
844    }
845    false
846}