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