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