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) = 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    Some(FunctionInfo {
280        name,
281        start_line,
282        end_line,
283        name_col,
284        name_end_col,
285        line_count: end_line - start_line + 1,
286        complexity: count_complexity(node),
287        body_hash,
288        is_exported: false,
289        parameter_count,
290        parameter_types,
291        chain_depth,
292        switch_arms,
293        external_refs,
294        is_delegating,
295        comment_lines: count_comment_lines(node, src),
296        referenced_fields: collect_field_refs(body, src),
297        null_check_fields: collect_null_checks(body, src),
298        switch_dispatch_target: extract_switch_target(body, src),
299        optional_param_count: count_optional_params(node, src),
300        called_functions: collect_calls_rs(body, src),
301        cognitive_complexity: body.map(cognitive_complexity_rs).unwrap_or(0),
302    })
303}
304
305fn extract_struct(node: Node, src: &[u8]) -> Option<(ClassInfo, Vec<String>)> {
306    let name_node = node.child_by_field_name("name")?;
307    let name = node_text(name_node, src).to_string();
308    let name_col = name_node.start_position().column;
309    let name_end_col = name_node.end_position().column;
310    let start_line = node.start_position().row + 1;
311    let end_line = node.end_position().row + 1;
312    let (field_count, field_names, callback_fields) = extract_fields(node, src);
313    let is_interface = node.kind() == "trait_item";
314    let has_listener_field = !callback_fields.is_empty();
315    Some((
316        ClassInfo {
317            name,
318            start_line,
319            end_line,
320            name_col,
321            name_end_col,
322            method_count: 0,
323            line_count: end_line - start_line + 1,
324            is_exported: false,
325            delegating_method_count: 0,
326            field_count,
327            field_names,
328            field_types: Vec::new(),
329            has_behavior: false,
330            is_interface,
331            parent_name: None,
332            override_count: 0,
333            self_call_count: 0,
334            has_listener_field,
335            has_notify_method: false,
336        },
337        callback_fields,
338    ))
339}
340
341fn count_parameters(node: Node) -> usize {
342    let params = match node.child_by_field_name("parameters") {
343        Some(p) => p,
344        None => return 0,
345    };
346    let mut cursor = params.walk();
347    params
348        .children(&mut cursor)
349        .filter(|c| c.kind() == "parameter" || c.kind() == "self_parameter")
350        .count()
351}
352
353fn extract_param_types(
354    node: Node,
355    src: &[u8],
356    imports_map: &crate::type_ref::ImportsMap,
357) -> Vec<cha_core::TypeRef> {
358    let params = match node.child_by_field_name("parameters") {
359        Some(p) => p,
360        None => return vec![],
361    };
362    let mut types = Vec::new();
363    let mut cursor = params.walk();
364    for child in params.children(&mut cursor) {
365        if child.kind() == "parameter"
366            && let Some(ty) = child.child_by_field_name("type")
367        {
368            types.push(crate::type_ref::resolve(node_text(ty, src), imports_map));
369        }
370    }
371    types
372}
373
374fn max_chain_depth(node: Node) -> usize {
375    let mut max = 0;
376    walk_chain_depth(node, &mut max);
377    max
378}
379
380fn walk_chain_depth(node: Node, max: &mut usize) {
381    if node.kind() == "field_expression" {
382        let depth = measure_chain(node);
383        if depth > *max {
384            *max = depth;
385        }
386    }
387    let mut cursor = node.walk();
388    for child in node.children(&mut cursor) {
389        walk_chain_depth(child, max);
390    }
391}
392
393/// Count consecutive field accesses (a.b.c.d), skipping method calls.
394fn measure_chain(node: Node) -> usize {
395    let mut depth = 0;
396    let mut current = node;
397    while current.kind() == "field_expression" {
398        depth += 1;
399        if let Some(obj) = current.child_by_field_name("value") {
400            current = obj;
401        } else {
402            break;
403        }
404    }
405    depth
406}
407
408fn count_switch_arms(node: Node) -> usize {
409    let mut count = 0;
410    walk_switch_arms(node, &mut count);
411    count
412}
413
414fn walk_switch_arms(node: Node, count: &mut usize) {
415    if node.kind() == "match_arm" {
416        *count += 1;
417    }
418    let mut cursor = node.walk();
419    for child in node.children(&mut cursor) {
420        walk_switch_arms(child, count);
421    }
422}
423
424fn collect_external_refs(node: Node, src: &[u8]) -> Vec<String> {
425    let mut refs = Vec::new();
426    walk_external_refs(node, src, &mut refs);
427    refs.sort();
428    refs.dedup();
429    refs
430}
431
432/// Walk a field_expression chain to its root identifier.
433fn field_chain_root(node: Node) -> Node {
434    let mut current = node;
435    while current.kind() == "field_expression" {
436        match current.child_by_field_name("value") {
437            Some(child) => current = child,
438            None => break,
439        }
440    }
441    current
442}
443
444fn walk_external_refs(node: Node, src: &[u8], refs: &mut Vec<String>) {
445    if node.kind() == "field_expression" {
446        // Walk to the root object of the chain (a.b.c → a)
447        let root = field_chain_root(node);
448        let text = node_text(root, src);
449        if text != "self" && !text.is_empty() {
450            refs.push(text.to_string());
451        }
452    }
453    let mut cursor = node.walk();
454    for child in node.children(&mut cursor) {
455        walk_external_refs(child, src, refs);
456    }
457}
458
459/// Extract the single statement from a block body, if exactly one exists.
460fn single_stmt(body: Node) -> Option<Node> {
461    let mut cursor = body.walk();
462    let stmts: Vec<_> = body
463        .children(&mut cursor)
464        .filter(|c| c.kind() != "{" && c.kind() != "}")
465        .collect();
466    (stmts.len() == 1).then(|| stmts[0])
467}
468
469/// Check if a node is a method call on an external object (not self).
470fn is_external_call(node: Node, src: &[u8]) -> bool {
471    node.kind() == "call_expression"
472        && node.child_by_field_name("function").is_some_and(|func| {
473            func.kind() == "field_expression"
474                && func
475                    .child_by_field_name("value")
476                    .is_some_and(|obj| node_text(obj, src) != "self")
477        })
478}
479
480fn check_delegating(body: Node, src: &[u8]) -> bool {
481    let Some(stmt) = single_stmt(body) else {
482        return false;
483    };
484    let expr = match stmt.kind() {
485        "expression_statement" => stmt.child(0).unwrap_or(stmt),
486        "return_expression" => stmt.child(1).unwrap_or(stmt),
487        _ => stmt,
488    };
489    is_external_call(expr, src)
490}
491
492fn extract_use(node: Node, src: &[u8]) -> Option<ImportInfo> {
493    // Extract the path from "use foo::bar::baz;"
494    let source = node_text(node, src)
495        .strip_prefix("use ")?
496        .trim_end_matches(';')
497        .trim()
498        .to_string();
499    Some(ImportInfo {
500        source,
501        line: node.start_position().row + 1,
502        col: node.start_position().column,
503        ..Default::default()
504    })
505}
506
507/// Count comment lines inside a function node.
508fn count_comment_lines(node: Node, src: &[u8]) -> usize {
509    let mut count = 0;
510    let mut cursor = node.walk();
511    for child in node.children(&mut cursor) {
512        if child.kind() == "line_comment" || child.kind() == "block_comment" {
513            count += child.end_position().row - child.start_position().row + 1;
514        }
515    }
516    // Also recurse into body
517    if let Some(body) = node.child_by_field_name("body") {
518        count += count_comment_lines_recursive(body, src);
519    }
520    count
521}
522
523fn count_comment_lines_recursive(node: Node, _src: &[u8]) -> usize {
524    let mut count = 0;
525    let mut cursor = node.walk();
526    for child in node.children(&mut cursor) {
527        if child.kind() == "line_comment" || child.kind() == "block_comment" {
528            count += child.end_position().row - child.start_position().row + 1;
529        } else if child.child_count() > 0 {
530            count += count_comment_lines_recursive(child, _src);
531        }
532    }
533    count
534}
535
536// cha:ignore todo_comment
537/// Collect field references (self.xxx) from a function body.
538fn collect_field_refs(body: Option<Node>, src: &[u8]) -> Vec<String> {
539    let Some(body) = body else { return vec![] };
540    let mut refs = Vec::new();
541    collect_self_fields(body, src, &mut refs);
542    refs.sort();
543    refs.dedup();
544    refs
545}
546
547fn collect_self_fields(node: Node, src: &[u8], refs: &mut Vec<String>) {
548    if node.kind() == "field_expression"
549        && let Some(obj) = node.child_by_field_name("value")
550        && node_text(obj, src) == "self"
551        && let Some(field) = node.child_by_field_name("field")
552    {
553        refs.push(node_text(field, src).to_string());
554    }
555    let mut cursor = node.walk();
556    for child in node.children(&mut cursor) {
557        collect_self_fields(child, src, refs);
558    }
559}
560
561/// Extract field names from a struct body.
562/// Returns (field_count, field_names, callback_collection_field_names).
563fn extract_fields(node: Node, src: &[u8]) -> (usize, Vec<String>, Vec<String>) {
564    let mut names = Vec::new();
565    let mut callback_fields = Vec::new();
566    if let Some(body) = node.child_by_field_name("body") {
567        let mut cursor = body.walk();
568        for child in body.children(&mut cursor) {
569            if child.kind() == "field_declaration"
570                && let Some(name_node) = child.child_by_field_name("name")
571            {
572                let name = node_text(name_node, src).to_string();
573                if let Some(ty) = child.child_by_field_name("type")
574                    && is_callback_collection_type_rs(node_text(ty, src))
575                {
576                    callback_fields.push(name.clone());
577                }
578                names.push(name);
579            }
580        }
581    }
582    (names.len(), names, callback_fields)
583}
584
585/// Check if a type looks like a collection of callbacks: Vec<Box<dyn Fn*>>, Vec<fn(...)>.
586fn is_callback_collection_type_rs(ty: &str) -> bool {
587    if !ty.contains("Vec<") {
588        return false;
589    }
590    ty.contains("Fn(") || ty.contains("FnMut(") || ty.contains("FnOnce(") || ty.contains("fn(")
591}
592
593/// Collect field names checked for None/null in match/if-let patterns.
594fn collect_null_checks(body: Option<Node>, src: &[u8]) -> Vec<String> {
595    let Some(body) = body else { return vec![] };
596    let mut fields = Vec::new();
597    walk_null_checks_rs(body, src, &mut fields);
598    fields.sort();
599    fields.dedup();
600    fields
601}
602
603fn walk_null_checks_rs(node: Node, src: &[u8], fields: &mut Vec<String>) {
604    if node.kind() == "if_let_expression" {
605        // if let Some(x) = self.field { ... }
606        if let Some(pattern) = node.child_by_field_name("pattern")
607            && node_text(pattern, src).contains("Some")
608            && let Some(value) = node.child_by_field_name("value")
609        {
610            let vtext = node_text(value, src);
611            if let Some(f) = vtext.strip_prefix("self.") {
612                fields.push(f.to_string());
613            }
614        }
615    } else if node.kind() == "if_expression"
616        && let Some(cond) = node.child_by_field_name("condition")
617    {
618        let text = node_text(cond, src);
619        if text.contains("is_some") || text.contains("is_none") {
620            extract_null_checked_fields(text, fields);
621        }
622    }
623    let mut cursor = node.walk();
624    for child in node.children(&mut cursor) {
625        walk_null_checks_rs(child, src, fields);
626    }
627}
628
629/// Extract self.field names from a condition text containing null checks.
630fn extract_null_checked_fields(text: &str, fields: &mut Vec<String>) {
631    if !(text.contains("is_some") || text.contains("is_none") || text.contains("Some")) {
632        return;
633    }
634    for part in text.split("self.") {
635        if let Some(field) = part
636            .split(|c: char| !c.is_alphanumeric() && c != '_')
637            .next()
638            && !field.is_empty()
639            && field != "is_some"
640            && field != "is_none"
641        {
642            fields.push(field.to_string());
643        }
644    }
645}
646
647/// Extract the variable/field being dispatched on in a match expression.
648fn extract_switch_target(body: Option<Node>, src: &[u8]) -> Option<String> {
649    let body = body?;
650    find_match_target(body, src)
651}
652
653fn find_match_target(node: Node, src: &[u8]) -> Option<String> {
654    if node.kind() == "match_expression"
655        && let Some(value) = node.child_by_field_name("value")
656    {
657        return Some(node_text(value, src).to_string());
658    }
659    let mut cursor = node.walk();
660    for child in node.children(&mut cursor) {
661        if let Some(t) = find_match_target(child, src) {
662            return Some(t);
663        }
664    }
665    None
666}
667
668/// Count optional parameters (those with default values or Option<T> type).
669fn count_optional_params(node: Node, src: &[u8]) -> usize {
670    let Some(params) = node.child_by_field_name("parameters") else {
671        return 0;
672    };
673    let mut count = 0;
674    let mut cursor = params.walk();
675    for child in params.children(&mut cursor) {
676        if child.kind() == "parameter" {
677            let text = node_text(child, src);
678            if text.contains("Option<") {
679                count += 1;
680            }
681        }
682    }
683    count
684}
685
686/// Count self.method() calls in a function body (for Template Method detection).
687fn count_self_method_calls(body: Option<Node>, src: &[u8]) -> usize {
688    let Some(body) = body else { return 0 };
689    let mut count = 0;
690    walk_self_calls(body, src, &mut count);
691    count
692}
693
694fn walk_self_calls(node: Node, src: &[u8], count: &mut usize) {
695    if node.kind() == "call_expression"
696        && let Some(func) = node.child_by_field_name("function")
697        && node_text(func, src).starts_with("self.")
698    {
699        *count += 1;
700    }
701    let mut cursor = node.walk();
702    for child in node.children(&mut cursor) {
703        walk_self_calls(child, src, count);
704    }
705}
706
707/// Structural Observer detection: method body iterates a callback collection field and calls elements.
708/// Pattern: `for cb in &self.field { cb(...) }` or `self.field.iter().for_each(|cb| cb(...))`
709fn has_iterate_and_call(body: Option<Node>, src: &[u8], cb_fields: &[String]) -> bool {
710    let Some(body) = body else { return false };
711    for field in cb_fields {
712        let self_field = format!("self.{field}");
713        if walk_for_iterate_call(body, src, &self_field) {
714            return true;
715        }
716    }
717    false
718}
719
720fn walk_for_iterate_call(node: Node, src: &[u8], self_field: &str) -> bool {
721    // for x in &self.field { x(...) }
722    if node.kind() == "for_expression"
723        && let Some(value) = node.child_by_field_name("value")
724        && node_text(value, src).contains(self_field)
725        && let Some(loop_body) = node.child_by_field_name("body")
726        && has_call_expression(loop_body)
727    {
728        return true;
729    }
730    // self.field.iter().for_each(|cb| cb(...))
731    if node.kind() == "call_expression" {
732        let text = node_text(node, src);
733        if text.contains(self_field) && text.contains("for_each") {
734            return true;
735        }
736    }
737    let mut cursor = node.walk();
738    for child in node.children(&mut cursor) {
739        if walk_for_iterate_call(child, src, self_field) {
740            return true;
741        }
742    }
743    false
744}
745
746fn cognitive_complexity_rs(node: Node) -> usize {
747    let mut score = 0;
748    cc_walk_rs(node, 0, &mut score);
749    score
750}
751
752fn cc_walk_rs(node: Node, nesting: usize, score: &mut usize) {
753    match node.kind() {
754        "if_expression" => {
755            *score += 1 + nesting;
756            cc_children_rs(node, nesting + 1, score);
757            return;
758        }
759        "for_expression" | "while_expression" | "loop_expression" => {
760            *score += 1 + nesting;
761            cc_children_rs(node, nesting + 1, score);
762            return;
763        }
764        "match_expression" => {
765            *score += 1 + nesting;
766            cc_children_rs(node, nesting + 1, score);
767            return;
768        }
769        "else_clause" => {
770            *score += 1;
771        }
772        "binary_expression" => {
773            if let Some(op) = node.child_by_field_name("operator")
774                && (op.kind() == "&&" || op.kind() == "||")
775            {
776                *score += 1;
777            }
778        }
779        "closure_expression" => {
780            cc_children_rs(node, nesting + 1, score);
781            return;
782        }
783        _ => {}
784    }
785    cc_children_rs(node, nesting, score);
786}
787
788fn cc_children_rs(node: Node, nesting: usize, score: &mut usize) {
789    let mut cursor = node.walk();
790    for child in node.children(&mut cursor) {
791        cc_walk_rs(child, nesting, score);
792    }
793}
794
795fn collect_calls_rs(body: Option<tree_sitter::Node>, src: &[u8]) -> Vec<String> {
796    let Some(body) = body else { return Vec::new() };
797    let mut calls = Vec::new();
798    let mut cursor = body.walk();
799    visit_all(body, &mut cursor, &mut |n| {
800        if n.kind() == "call_expression"
801            && let Some(func) = n.child(0)
802        {
803            let name = node_text(func, src).to_string();
804            if !calls.contains(&name) {
805                calls.push(name);
806            }
807        }
808    });
809    calls
810}
811
812fn visit_all<F: FnMut(Node)>(node: Node, cursor: &mut tree_sitter::TreeCursor, f: &mut F) {
813    f(node);
814    if cursor.goto_first_child() {
815        loop {
816            visit_all(cursor.node(), cursor, f);
817            if !cursor.goto_next_sibling() {
818                break;
819            }
820        }
821        cursor.goto_parent();
822    }
823}
824
825fn collect_comments(root: Node, src: &[u8]) -> Vec<cha_core::CommentInfo> {
826    let mut comments = Vec::new();
827    let mut cursor = root.walk();
828    visit_all(root, &mut cursor, &mut |n| {
829        if n.kind().contains("comment") {
830            comments.push(cha_core::CommentInfo {
831                text: node_text(n, src).to_string(),
832                line: n.start_position().row + 1,
833            });
834        }
835    });
836    comments
837}
838
839fn has_call_expression(node: Node) -> bool {
840    if node.kind() == "call_expression" {
841        return true;
842    }
843    let mut cursor = node.walk();
844    for child in node.children(&mut cursor) {
845        if has_call_expression(child) {
846            return true;
847        }
848    }
849    false
850}