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