Skip to main content

cha_parser/
c_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 CParser;
10pub struct CppParser;
11
12impl LanguageParser for CParser {
13    fn language_name(&self) -> &str {
14        "c"
15    }
16    fn parse(&self, file: &SourceFile) -> Option<SourceModel> {
17        parse_c_like(file, "c", &tree_sitter_c::LANGUAGE.into())
18    }
19}
20
21impl LanguageParser for CppParser {
22    fn language_name(&self) -> &str {
23        "cpp"
24    }
25    fn parse(&self, file: &SourceFile) -> Option<SourceModel> {
26        parse_c_like(file, "cpp", &tree_sitter_cpp::LANGUAGE.into())
27    }
28}
29
30fn parse_c_like(
31    file: &SourceFile,
32    lang: &str,
33    language: &tree_sitter::Language,
34) -> Option<SourceModel> {
35    let mut parser = Parser::new();
36    parser.set_language(language).ok()?;
37    let tree = parser.parse(&file.content, None)?;
38    let root = tree.root_node();
39    let src = file.content.as_bytes();
40
41    let mut functions = Vec::new();
42    let mut classes = Vec::new();
43    let mut imports = Vec::new();
44    let mut type_aliases = Vec::new();
45
46    let imports_map = crate::c_imports::build(root, src);
47    collect_top_level(
48        root,
49        src,
50        &imports_map,
51        &mut functions,
52        &mut classes,
53        &mut imports,
54        &mut type_aliases,
55    );
56
57    // (C OOP method attribution moved to `cha-cli::c_oop_enrich`, which has
58    // cross-file visibility — a function in `foo.c` can be attributed to a
59    // struct declared in `foo.h`.)
60
61    if is_header_file(file) {
62        for f in &mut functions {
63            f.is_exported = true;
64        }
65    }
66
67    Some(SourceModel {
68        language: lang.into(),
69        total_lines: file.line_count(),
70        functions,
71        classes,
72        imports,
73        comments: collect_comments(root, src),
74        type_aliases,
75    })
76}
77
78fn is_header_file(file: &SourceFile) -> bool {
79    file.path
80        .extension()
81        .is_some_and(|e| e == "h" || e == "hxx" || e == "hpp")
82}
83
84// cha:ignore cognitive_complexity
85// cha:ignore high_complexity
86fn collect_top_level(
87    root: Node,
88    src: &[u8],
89    imports_map: &crate::type_ref::ImportsMap,
90    functions: &mut Vec<FunctionInfo>,
91    classes: &mut Vec<ClassInfo>,
92    imports: &mut Vec<ImportInfo>,
93    type_aliases: &mut Vec<(String, String)>,
94) {
95    let mut cursor = root.walk();
96    for child in root.children(&mut cursor) {
97        match child.kind() {
98            "function_definition" => {
99                handle_function_definition(child, src, imports_map, functions, classes);
100            }
101            "declaration" => {
102                // Header-style function declarations (`void foo(int);` — no
103                // body) surface as `declaration` nodes in tree-sitter-c. Pick
104                // out the function_declarator variants and treat them as
105                // functions so `.h` file contents contribute to the project
106                // API surface / method attribution. Non-function
107                // `declaration` (globals, typedefs, extern vars) have no
108                // function_declarator child and are skipped.
109                if has_function_declarator(child)
110                    && let Some(f) = extract_function(child, src, imports_map)
111                {
112                    functions.push(f);
113                }
114            }
115            "struct_specifier" | "class_specifier" => {
116                if let Some(c) = extract_class(child, src) {
117                    classes.push(c);
118                }
119            }
120            "type_definition" => {
121                extract_typedef_struct(child, src, classes, type_aliases);
122            }
123            "preproc_include" => {
124                if let Some(imp) = extract_include(child, src) {
125                    imports.push(imp);
126                }
127            }
128            // C++ nesting constructs: recurse into them so inner
129            // functions/classes land at the top of `functions`/`classes`.
130            // `namespace { ... }` and `extern "C" { ... }` wrap their
131            // content in a `body` field; `template <...>` applies to its
132            // following sibling node directly (no body field). We just
133            // recurse into the whole subtree either way.
134            "namespace_definition" | "linkage_specification" | "template_declaration" => {
135                collect_top_level(
136                    child,
137                    src,
138                    imports_map,
139                    functions,
140                    classes,
141                    imports,
142                    type_aliases,
143                );
144            }
145            _ => {
146                if child.child_count() > 0 {
147                    collect_top_level(
148                        child,
149                        src,
150                        imports_map,
151                        functions,
152                        classes,
153                        imports,
154                        type_aliases,
155                    );
156                }
157            }
158        }
159    }
160}
161
162/// Process a `function_definition` node. Handles two C-specific edge
163/// cases (the `class MACRO Name {...}` macro-class pattern and plain
164/// functions) plus C++ out-of-class method attribution.
165fn handle_function_definition(
166    node: Node,
167    src: &[u8],
168    imports_map: &crate::type_ref::ImportsMap,
169    functions: &mut Vec<FunctionInfo>,
170    classes: &mut Vec<ClassInfo>,
171) {
172    if let Some(c) = try_extract_macro_class(node, src) {
173        classes.push(c);
174        return;
175    }
176    let Some(f) = extract_function(node, src, imports_map) else {
177        return;
178    };
179    if let Some(q) = crate::cpp::extract_class_qualifier(node, src) {
180        crate::cpp::attach_to_class(&q, classes);
181    }
182    functions.push(f);
183}
184
185fn extract_typedef_struct(
186    node: Node,
187    src: &[u8],
188    classes: &mut Vec<ClassInfo>,
189    type_aliases: &mut Vec<(String, String)>,
190) {
191    let found_struct = register_typedef_struct_children(node, src, classes, type_aliases);
192    if !found_struct {
193        register_simple_typedef(node, src, type_aliases);
194    }
195}
196
197fn register_typedef_struct_children(
198    node: Node,
199    src: &[u8],
200    classes: &mut Vec<ClassInfo>,
201    type_aliases: &mut Vec<(String, String)>,
202) -> bool {
203    let mut found_struct = false;
204    let mut inner = node.walk();
205    for sub in node.children(&mut inner) {
206        if sub.kind() != "struct_specifier" && sub.kind() != "class_specifier" {
207            continue;
208        }
209        found_struct = true;
210        register_single_typedef_struct(node, sub, src, classes, type_aliases);
211    }
212    found_struct
213}
214
215fn register_single_typedef_struct(
216    typedef: Node,
217    sub: Node,
218    src: &[u8],
219    classes: &mut Vec<ClassInfo>,
220    type_aliases: &mut Vec<(String, String)>,
221) {
222    let Some(mut c) = extract_class(sub, src) else {
223        return;
224    };
225    let original_name = c.name.clone();
226    if c.name.is_empty()
227        && let Some(decl) = typedef.child_by_field_name("declarator")
228    {
229        c.name = node_text(decl, src).to_string();
230    }
231    if !original_name.is_empty()
232        && let Some(decl) = typedef.child_by_field_name("declarator")
233    {
234        let alias = node_text(decl, src).to_string();
235        if alias != original_name {
236            type_aliases.push((alias, original_name));
237        }
238    }
239    if !c.name.is_empty() {
240        classes.push(c);
241    }
242}
243
244/// `typedef uint32_t tag_t;` — simple alias, no struct body.
245fn register_simple_typedef(node: Node, src: &[u8], type_aliases: &mut Vec<(String, String)>) {
246    let alias = extract_typedef_alias(node, src);
247    let original = node
248        .child_by_field_name("type")
249        .map(|t| node_text(t, src).trim().to_string())
250        .unwrap_or_default();
251    if !alias.is_empty() && alias != original {
252        type_aliases.push((alias, original));
253    }
254}
255
256/// Find the new type name in a `typedef <something> <name>;`. Tree-sitter-c
257/// sometimes puts the name behind the `declarator` field; other grammars
258/// (typedef of enum/union without body) emit a plain `type_identifier` as
259/// a top-level child. Try the field first, then the first type_identifier.
260fn extract_typedef_alias(node: Node, src: &[u8]) -> String {
261    if let Some(decl) = node.child_by_field_name("declarator") {
262        return node_text(decl, src).trim().to_string();
263    }
264    let mut cursor = node.walk();
265    for child in node.children(&mut cursor) {
266        if child.kind() == "type_identifier" {
267            return node_text(child, src).trim().to_string();
268        }
269    }
270    String::new()
271}
272
273/// Detect `class MACRO ClassName { ... };` misparse.
274/// tree-sitter sees this as function_definition with class_specifier return type.
275fn try_extract_macro_class(node: Node, src: &[u8]) -> Option<ClassInfo> {
276    let mut has_class_spec = false;
277    let mut cursor = node.walk();
278    for child in node.children(&mut cursor) {
279        if child.kind() == "class_specifier" || child.kind() == "struct_specifier" {
280            has_class_spec = true;
281        }
282    }
283    if !has_class_spec {
284        return None;
285    }
286    // The real class name is the "identifier" child (what tree-sitter thinks is the func name)
287    let name_node = node
288        .child_by_field_name("declarator")
289        .filter(|d| d.kind() == "identifier")?;
290    let name = node_text(name_node, src).to_string();
291    let name_col = name_node.start_position().column;
292    let name_end_col = name_node.end_position().column;
293    let body = node.child_by_field_name("body")?;
294    let start_line = node.start_position().row + 1;
295    let end_line = node.end_position().row + 1;
296    let method_count = count_methods(body);
297    let (field_names, field_types, first_field_type) = extract_field_info(body, src);
298
299    // Find parent from the class_specifier's base_class_clause if present
300    let parent_name = first_field_type;
301
302    Some(ClassInfo {
303        name,
304        start_line,
305        end_line,
306        name_col,
307        name_end_col,
308        line_count: end_line - start_line + 1,
309        method_count,
310        is_exported: true,
311        delegating_method_count: 0,
312        field_count: field_names.len(),
313        field_names,
314        field_types,
315        has_behavior: method_count > 0,
316        is_interface: false,
317        parent_name,
318        override_count: 0,
319        self_call_count: 0,
320        has_listener_field: false,
321        has_notify_method: false,
322    })
323}
324
325fn extract_function(
326    node: Node,
327    src: &[u8],
328    imports_map: &crate::type_ref::ImportsMap,
329) -> Option<FunctionInfo> {
330    let declarator = node.child_by_field_name("declarator")?;
331    let name_node = find_func_name_node(declarator)?;
332    let name = node_text(name_node, src).to_string();
333    let name_col = name_node.start_position().column;
334    let name_end_col = name_node.end_position().column;
335    let start_line = node.start_position().row + 1;
336    let end_line = node.end_position().row + 1;
337    let body = node.child_by_field_name("body");
338    let (param_count, param_types, param_names) = extract_params(declarator, src, imports_map);
339    let is_static = has_storage_class(node, src, "static");
340
341    Some(FunctionInfo {
342        name,
343        start_line,
344        end_line,
345        name_col,
346        name_end_col,
347        line_count: end_line - start_line + 1,
348        complexity: count_complexity(node),
349        body_hash: body.map(hash_ast),
350        is_exported: !is_static,
351        parameter_count: param_count,
352        parameter_types: param_types,
353        parameter_names: param_names,
354        chain_depth: body.map(max_chain_depth).unwrap_or(0),
355        switch_arms: body.map(count_case_labels).unwrap_or(0),
356        switch_arm_values: body
357            .map(|b| collect_c_arm_values(b, src))
358            .unwrap_or_default(),
359        external_refs: body
360            .map(|b| collect_external_refs_c(b, src))
361            .unwrap_or_default(),
362        is_delegating: body.map(|b| check_delegating_c(b, src)).unwrap_or(false),
363        comment_lines: count_comment_lines(node, src),
364        referenced_fields: body
365            .map(|b| collect_field_refs_c(b, src))
366            .unwrap_or_default(),
367        null_check_fields: body
368            .map(|b| collect_null_checks_c(b, src))
369            .unwrap_or_default(),
370        switch_dispatch_target: body.and_then(|b| extract_switch_target_c(b, src)),
371        optional_param_count: 0,
372        called_functions: body.map(|b| collect_calls_c(b, src)).unwrap_or_default(),
373        cognitive_complexity: body.map(cognitive_complexity_c).unwrap_or(0),
374        return_type: extract_c_return_type(node, src, imports_map),
375    })
376}
377
378/// The C function's return type lives in the `type` field of the
379/// `function_definition` node. Pointer return types are indicated by the
380/// declarator having a `pointer_declarator`; prefix the type with ` *`
381/// so the `raw` text mirrors the written form.
382fn extract_c_return_type(
383    node: Node,
384    src: &[u8],
385    imports_map: &crate::type_ref::ImportsMap,
386) -> Option<cha_core::TypeRef> {
387    let ty = node.child_by_field_name("type")?;
388    let base = node_text(ty, src).trim().to_string();
389    let is_ptr = node
390        .child_by_field_name("declarator")
391        .is_some_and(|d| d.kind() == "pointer_declarator");
392    let raw = if is_ptr { format!("{base} *") } else { base };
393    Some(crate::type_ref::resolve(raw, imports_map))
394}
395
396/// Does this `declaration` node actually declare a function (as opposed
397/// to a variable / typedef / extern)? tree-sitter-c wraps function
398/// prototypes in `declaration` with a `function_declarator` descendant.
399fn has_function_declarator(node: Node) -> bool {
400    node.child_by_field_name("declarator")
401        .is_some_and(has_function_declarator_inside)
402}
403
404fn has_function_declarator_inside(node: Node) -> bool {
405    if node.kind() == "function_declarator" {
406        return true;
407    }
408    // Pointer return types wrap the declarator: `int *foo(...)` produces
409    // `pointer_declarator { function_declarator { ... } }`. Unwrap.
410    if let Some(inner) = node.child_by_field_name("declarator") {
411        return has_function_declarator_inside(inner);
412    }
413    false
414}
415
416/// Check if a declaration node has a specific storage class specifier (e.g. "static").
417fn has_storage_class(node: Node, src: &[u8], keyword: &str) -> bool {
418    for i in 0..node.child_count() {
419        if let Some(child) = node.child(i)
420            && child.kind() == "storage_class_specifier"
421            && node_text(child, src) == keyword
422        {
423            return true;
424        }
425    }
426    false
427}
428
429fn find_func_name_node(declarator: Node) -> Option<Node> {
430    // Plain C: declarator chain bottoms out in `identifier`. C++ adds:
431    // `field_identifier` (in-class member), `destructor_name`,
432    // `operator_name`, and `qualified_identifier` (out-of-class / global).
433    match declarator.kind() {
434        "identifier" | "field_identifier" | "destructor_name" | "operator_name" => {
435            return Some(declarator);
436        }
437        "qualified_identifier" => return crate::cpp::qualified_identifier_leaf(declarator),
438        _ => {}
439    }
440    // `pointer_declarator` has a `declarator` field; `reference_declarator`
441    // does not — its named children are the sub-declarator positionally.
442    // Fall back to the first named child if the field is absent.
443    let next = declarator
444        .child_by_field_name("declarator")
445        .or_else(|| first_named_child(declarator));
446    next.and_then(find_func_name_node)
447}
448
449fn first_named_child(node: Node) -> Option<Node> {
450    let mut c = node.walk();
451    node.children(&mut c).find(|n| n.is_named())
452}
453
454fn extract_params(
455    declarator: Node,
456    src: &[u8],
457    imports_map: &crate::type_ref::ImportsMap,
458) -> (usize, Vec<cha_core::TypeRef>, Vec<String>) {
459    let params = match declarator.child_by_field_name("parameters") {
460        Some(p) => p,
461        None => return (0, vec![], vec![]),
462    };
463    let mut count = 0;
464    let mut types = Vec::new();
465    let mut names = Vec::new();
466    let mut cursor = params.walk();
467    for child in params.children(&mut cursor) {
468        if child.kind() == "parameter_declaration" {
469            count += 1;
470            let base = child
471                .child_by_field_name("type")
472                .map(|t| node_text(t, src).to_string())
473                .unwrap_or_else(|| "int".into());
474            let decl = child.child_by_field_name("declarator");
475            let is_ptr = decl.is_some_and(|d| d.kind() == "pointer_declarator");
476            let raw = if is_ptr { format!("{base} *") } else { base };
477            types.push(crate::type_ref::resolve(raw, imports_map));
478            names.push(
479                decl.map(|d| crate::cpp::c_param_name(d, src))
480                    .unwrap_or_default(),
481            );
482        }
483    }
484    (count, types, names)
485}
486
487fn extract_class(node: Node, src: &[u8]) -> Option<ClassInfo> {
488    let (name, name_col, name_end_col) =
489        crate::cpp::class_name_triple(node.child_by_field_name("name"), src);
490    let start_line = node.start_position().row + 1;
491    let end_line = node.end_position().row + 1;
492    let body = node.child_by_field_name("body");
493    let method_count = body.map(count_methods).unwrap_or(0);
494    let (field_names, field_types, first_field_type) =
495        body.map(|b| extract_field_info(b, src)).unwrap_or_default();
496
497    // C++ inheritance (`class Derived : public Base`) takes precedence
498    // over the "first field looks like an embedded base struct" C heuristic.
499    // `crate::cpp::extract_cpp_base` returns `None` for plain C
500    // struct_specifier (no base_class_clause child), so the old behaviour
501    // is preserved where real inheritance syntax isn't present.
502    let parent_name = crate::cpp::extract_cpp_base(node, src).or(first_field_type);
503
504    Some(ClassInfo {
505        name,
506        start_line,
507        end_line,
508        name_col,
509        name_end_col,
510        line_count: end_line - start_line + 1,
511        method_count,
512        is_exported: true,
513        delegating_method_count: 0,
514        field_count: field_names.len(),
515        field_names,
516        field_types,
517        has_behavior: method_count > 0,
518        is_interface: false,
519        parent_name,
520        override_count: 0,
521        self_call_count: 0,
522        has_listener_field: false,
523        has_notify_method: false,
524    })
525}
526
527fn extract_field_info(body: Node, src: &[u8]) -> (Vec<String>, Vec<String>, Option<String>) {
528    let mut names = Vec::new();
529    let mut types = Vec::new();
530    let mut first_type = None;
531    let mut cursor = body.walk();
532    for child in body.children(&mut cursor) {
533        if child.kind() == "field_declaration" {
534            if let Some(decl) = child.child_by_field_name("declarator") {
535                names.push(node_text(decl, src).to_string());
536            }
537            let ty = child
538                .child_by_field_name("type")
539                .map(|t| node_text(t, src).to_string());
540            if first_type.is_none() {
541                first_type = ty.clone();
542            }
543            types.push(ty.unwrap_or_default());
544        }
545    }
546    (names, types, first_type)
547}
548
549fn count_methods(body: Node) -> usize {
550    let mut count = 0;
551    let mut cursor = body.walk();
552    for child in body.children(&mut cursor) {
553        if child.kind() == "function_definition" || child.kind() == "declaration" {
554            count += 1;
555        }
556    }
557    count
558}
559
560fn extract_include(node: Node, src: &[u8]) -> Option<ImportInfo> {
561    let path = node.child_by_field_name("path")?;
562    let text = node_text(path, src)
563        .trim_matches(|c| c == '"' || c == '<' || c == '>')
564        .to_string();
565    Some(ImportInfo {
566        source: text,
567        line: node.start_position().row + 1,
568        col: node.start_position().column,
569        ..Default::default()
570    })
571}
572
573fn count_complexity(node: Node) -> usize {
574    let mut c = 1usize;
575    let mut cursor = node.walk();
576    visit_all(node, &mut cursor, &mut |n| match n.kind() {
577        "if_statement"
578        | "for_statement"
579        | "while_statement"
580        | "do_statement"
581        | "case_statement"
582        | "catch_clause"
583        | "conditional_expression" => c += 1,
584        "binary_expression" => {
585            if let Some(op) = n.child_by_field_name("operator") {
586                let kind = op.kind();
587                if kind == "&&" || kind == "||" {
588                    c += 1;
589                }
590            }
591        }
592        _ => {}
593    });
594    c
595}
596
597fn max_chain_depth(node: Node) -> usize {
598    let mut max = 0;
599    let mut cursor = node.walk();
600    visit_all(node, &mut cursor, &mut |n| {
601        if n.kind() == "field_expression" {
602            let d = chain_len(n);
603            if d > max {
604                max = d;
605            }
606        }
607    });
608    max
609}
610
611fn chain_len(node: Node) -> usize {
612    let mut depth = 0;
613    let mut current = node;
614    while current.kind() == "field_expression" || current.kind() == "call_expression" {
615        if current.kind() == "field_expression" {
616            depth += 1;
617        }
618        match current.child(0) {
619            Some(c) => current = c,
620            None => break,
621        }
622    }
623    depth
624}
625
626fn collect_c_arm_values(body: Node, src: &[u8]) -> Vec<cha_core::ArmValue> {
627    let mut out = Vec::new();
628    crate::switch_arms::walk_arms(body, src, &mut out, &|n| n.kind() == "case_statement");
629    out
630}
631
632fn count_case_labels(node: Node) -> usize {
633    let mut count = 0;
634    let mut cursor = node.walk();
635    visit_all(node, &mut cursor, &mut |n| {
636        if n.kind() == "case_statement" {
637            count += 1;
638        }
639    });
640    count
641}
642
643fn cognitive_complexity_c(node: tree_sitter::Node) -> usize {
644    let mut score = 0;
645    cc_walk_c(node, 0, &mut score);
646    score
647}
648
649fn cc_walk_c(node: tree_sitter::Node, nesting: usize, score: &mut usize) {
650    match node.kind() {
651        "if_statement" => {
652            *score += 1 + nesting;
653            cc_children_c(node, nesting + 1, score);
654            return;
655        }
656        "for_statement" | "while_statement" | "do_statement" => {
657            *score += 1 + nesting;
658            cc_children_c(node, nesting + 1, score);
659            return;
660        }
661        "switch_statement" => {
662            *score += 1 + nesting;
663            cc_children_c(node, nesting + 1, score);
664            return;
665        }
666        "else_clause" => {
667            *score += 1;
668        }
669        "binary_expression" => {
670            if let Some(op) = node.child_by_field_name("operator")
671                && (op.kind() == "&&" || op.kind() == "||")
672            {
673                *score += 1;
674            }
675        }
676        "catch_clause" => {
677            *score += 1 + nesting;
678            cc_children_c(node, nesting + 1, score);
679            return;
680        }
681        _ => {}
682    }
683    cc_children_c(node, nesting, score);
684}
685
686fn cc_children_c(node: tree_sitter::Node, nesting: usize, score: &mut usize) {
687    let mut cursor = node.walk();
688    for child in node.children(&mut cursor) {
689        cc_walk_c(child, nesting, score);
690    }
691}
692
693fn collect_external_refs_c(body: Node, src: &[u8]) -> Vec<String> {
694    let mut refs = Vec::new();
695    let mut cursor = body.walk();
696    visit_all(body, &mut cursor, &mut |n| {
697        if n.kind() == "field_expression"
698            && let Some(obj) = n.child(0)
699            && obj.kind() == "identifier"
700        {
701            let name = node_text(obj, src).to_string();
702            if !refs.contains(&name) {
703                refs.push(name);
704            }
705        }
706    });
707    refs
708}
709
710fn check_delegating_c(body: Node, src: &[u8]) -> bool {
711    let mut cursor = body.walk();
712    let stmts: Vec<Node> = body
713        .children(&mut cursor)
714        .filter(|n| n.kind() != "{" && n.kind() != "}" && !n.kind().contains("comment"))
715        .collect();
716    if stmts.len() != 1 {
717        return false;
718    }
719    let stmt = stmts[0];
720    let call = match stmt.kind() {
721        "return_statement" => stmt.child(1).filter(|c| c.kind() == "call_expression"),
722        "expression_statement" => stmt.child(0).filter(|c| c.kind() == "call_expression"),
723        _ => None,
724    };
725    call.and_then(|c| c.child(0))
726        .is_some_and(|f| node_text(f, src).contains('.') || node_text(f, src).contains("->"))
727}
728
729fn collect_field_refs_c(body: Node, src: &[u8]) -> Vec<String> {
730    let mut refs = Vec::new();
731    let mut cursor = body.walk();
732    visit_all(body, &mut cursor, &mut |n| {
733        if n.kind() == "field_expression"
734            && let Some(field) = n.child_by_field_name("field")
735        {
736            let name = node_text(field, src).to_string();
737            if !refs.contains(&name) {
738                refs.push(name);
739            }
740        }
741    });
742    refs
743}
744
745fn collect_null_checks_c(body: Node, src: &[u8]) -> Vec<String> {
746    let mut fields = Vec::new();
747    let mut cursor = body.walk();
748    visit_all(body, &mut cursor, &mut |n| {
749        if n.kind() == "binary_expression" {
750            let text = node_text(n, src);
751            if (text.contains("NULL") || text.contains("nullptr"))
752                && let Some(left) = n.child(0)
753            {
754                let name = node_text(left, src).to_string();
755                if !fields.contains(&name) {
756                    fields.push(name);
757                }
758            }
759        }
760    });
761    fields
762}
763
764fn extract_switch_target_c(body: Node, src: &[u8]) -> Option<String> {
765    let mut cursor = body.walk();
766    let mut target = None;
767    visit_all(body, &mut cursor, &mut |n| {
768        if n.kind() == "switch_statement"
769            && target.is_none()
770            && let Some(cond) = n.child_by_field_name("condition")
771        {
772            target = Some(node_text(cond, src).trim_matches(['(', ')']).to_string());
773        }
774    });
775    target
776}
777
778fn collect_calls_c(body: Node, src: &[u8]) -> Vec<String> {
779    let mut calls = Vec::new();
780    let mut cursor = body.walk();
781    visit_all(body, &mut cursor, &mut |n| {
782        if n.kind() == "call_expression"
783            && let Some(func) = n.child(0)
784        {
785            let name = node_text(func, src).to_string();
786            if !calls.contains(&name) {
787                calls.push(name);
788            }
789        }
790    });
791    calls
792}
793
794fn count_comment_lines(node: Node, src: &[u8]) -> usize {
795    let mut count = 0;
796    let mut cursor = node.walk();
797    visit_all(node, &mut cursor, &mut |n| {
798        if n.kind() == "comment" {
799            count += node_text(n, src).lines().count();
800        }
801    });
802    count
803}
804
805fn hash_ast(node: Node) -> u64 {
806    let mut hasher = DefaultHasher::new();
807    hash_node(node, &mut hasher);
808    hasher.finish()
809}
810
811fn hash_node(node: Node, hasher: &mut DefaultHasher) {
812    node.kind().hash(hasher);
813    let mut cursor = node.walk();
814    for child in node.children(&mut cursor) {
815        hash_node(child, hasher);
816    }
817}
818
819fn node_text<'a>(node: Node, src: &'a [u8]) -> &'a str {
820    node.utf8_text(src).unwrap_or("")
821}
822
823fn collect_comments(root: Node, src: &[u8]) -> Vec<cha_core::CommentInfo> {
824    let mut comments = Vec::new();
825    let mut cursor = root.walk();
826    visit_all(root, &mut cursor, &mut |n| {
827        if n.kind().contains("comment") {
828            comments.push(cha_core::CommentInfo {
829                text: node_text(n, src).to_string(),
830                line: n.start_position().row + 1,
831            });
832        }
833    });
834    comments
835}
836
837fn visit_all<F: FnMut(Node)>(node: Node, cursor: &mut tree_sitter::TreeCursor, f: &mut F) {
838    f(node);
839    if cursor.goto_first_child() {
840        loop {
841            let child_node = cursor.node();
842            let mut child_cursor = child_node.walk();
843            visit_all(child_node, &mut child_cursor, f);
844            if !cursor.goto_next_sibling() {
845                break;
846            }
847        }
848        cursor.goto_parent();
849    }
850}