Skip to main content

cha_parser/
python.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 PythonParser;
10
11impl LanguageParser for PythonParser {
12    fn language_name(&self) -> &str {
13        "python"
14    }
15
16    fn parse(&self, file: &SourceFile) -> Option<SourceModel> {
17        let mut parser = Parser::new();
18        parser
19            .set_language(&tree_sitter_python::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 functions = Vec::new();
26        let mut classes = Vec::new();
27        let mut imports = Vec::new();
28        let mut type_aliases = Vec::new();
29
30        let imports_map = crate::python_imports::build(root, src);
31        collect_top_level(
32            root,
33            src,
34            &imports_map,
35            &mut functions,
36            &mut classes,
37            &mut imports,
38            &mut type_aliases,
39        );
40
41        Some(SourceModel {
42            language: "python".into(),
43            total_lines: file.line_count(),
44            functions,
45            classes,
46            imports,
47            comments: collect_comments(root, src),
48            type_aliases,
49        })
50    }
51}
52
53fn push_definition(
54    node: Node,
55    src: &[u8],
56    imports_map: &crate::type_ref::ImportsMap,
57    functions: &mut Vec<FunctionInfo>,
58    classes: &mut Vec<ClassInfo>,
59) {
60    match node.kind() {
61        "function_definition" => {
62            if let Some(f) = extract_function(node, src, imports_map) {
63                functions.push(f);
64            }
65        }
66        "class_definition" => {
67            if let Some(c) = extract_class(node, src, imports_map, functions) {
68                classes.push(c);
69            }
70        }
71        _ => {}
72    }
73}
74
75fn collect_top_level(
76    node: Node,
77    src: &[u8],
78    imports_map: &crate::type_ref::ImportsMap,
79    functions: &mut Vec<FunctionInfo>,
80    classes: &mut Vec<ClassInfo>,
81    imports: &mut Vec<ImportInfo>,
82    type_aliases: &mut Vec<(String, String)>,
83) {
84    let mut cursor = node.walk();
85    for child in node.children(&mut cursor) {
86        match child.kind() {
87            "function_definition" | "class_definition" => {
88                push_definition(child, src, imports_map, functions, classes);
89            }
90            "import_statement" => collect_import(child, src, imports),
91            "import_from_statement" => collect_import_from(child, src, imports),
92            "type_alias_statement" => collect_type_alias_statement(child, src, type_aliases),
93            "expression_statement" => collect_typed_alias_assignment(child, src, type_aliases),
94            "decorated_definition" => {
95                let mut inner = child.walk();
96                for c in child.children(&mut inner) {
97                    push_definition(c, src, imports_map, functions, classes);
98                }
99            }
100            _ => {}
101        }
102    }
103}
104
105fn collect_type_alias_statement(node: Node, src: &[u8], out: &mut Vec<(String, String)>) {
106    if let Some(pair) = crate::type_aliases::python_statement(node, src) {
107        out.push(pair);
108    }
109}
110
111fn collect_typed_alias_assignment(node: Node, src: &[u8], out: &mut Vec<(String, String)>) {
112    if let Some(pair) = crate::type_aliases::python_assignment(node, src) {
113        out.push(pair);
114    }
115}
116
117fn extract_function(
118    node: Node,
119    src: &[u8],
120    imports_map: &crate::type_ref::ImportsMap,
121) -> Option<FunctionInfo> {
122    let name_node = node.child_by_field_name("name")?;
123    let name = node_text(name_node, src).to_string();
124    let name_col = name_node.start_position().column;
125    let name_end_col = name_node.end_position().column;
126    let start_line = node.start_position().row + 1;
127    let end_line = node.end_position().row + 1;
128    let body = node.child_by_field_name("body");
129    let params = node.child_by_field_name("parameters");
130    let (param_count, param_types, param_names) = params
131        .map(|p| extract_params(p, src, imports_map))
132        .unwrap_or((0, vec![], vec![]));
133
134    Some(FunctionInfo {
135        name,
136        start_line,
137        end_line,
138        name_col,
139        name_end_col,
140        line_count: end_line - start_line + 1,
141        complexity: count_complexity(node),
142        body_hash: body.map(hash_ast_structure),
143        is_exported: true,
144        parameter_count: param_count,
145        parameter_types: param_types,
146        parameter_names: param_names,
147        chain_depth: body.map(max_chain_depth).unwrap_or(0),
148        switch_arms: body.map(count_match_arms).unwrap_or(0),
149        switch_arm_values: body
150            .map(|b| collect_py_arm_values(b, src))
151            .unwrap_or_default(),
152        external_refs: body
153            .map(|b| collect_external_refs(b, src))
154            .unwrap_or_default(),
155        is_delegating: body.map(|b| check_delegating(b, src)).unwrap_or(false),
156        comment_lines: count_comment_lines(node, src),
157        referenced_fields: body.map(|b| collect_self_refs(b, src)).unwrap_or_default(),
158        null_check_fields: body
159            .map(|b| collect_none_checks(b, src))
160            .unwrap_or_default(),
161        switch_dispatch_target: body.and_then(|b| extract_match_target_py(b, src)),
162        optional_param_count: params.map(count_optional).unwrap_or(0),
163        called_functions: body.map(|b| collect_calls_py(b, src)).unwrap_or_default(),
164        cognitive_complexity: body.map(cognitive_complexity_py).unwrap_or(0),
165        return_type: node
166            .child_by_field_name("return_type")
167            .map(|rt| crate::type_ref::resolve(node_text(rt, src), imports_map)),
168    })
169}
170
171fn find_method_def(child: Node) -> Option<Node> {
172    if child.kind() == "function_definition" {
173        return Some(child);
174    }
175    if child.kind() == "decorated_definition" {
176        let mut inner = child.walk();
177        return child
178            .children(&mut inner)
179            .find(|c| c.kind() == "function_definition");
180    }
181    None
182}
183
184fn extract_parent_name(node: Node, src: &[u8]) -> Option<String> {
185    node.child_by_field_name("superclasses").and_then(|sc| {
186        let mut c = sc.walk();
187        sc.children(&mut c)
188            .find(|n| n.kind() != "(" && n.kind() != ")" && n.kind() != ",")
189            .map(|n| node_text(n, src).to_string())
190    })
191}
192
193fn has_listener_name(name: &str) -> bool {
194    name.contains("listener")
195        || name.contains("handler")
196        || name.contains("callback")
197        || name.contains("observer")
198}
199
200fn process_method(
201    func_node: Node,
202    f: &mut FunctionInfo,
203    src: &[u8],
204    field_names: &mut Vec<String>,
205) -> (bool, bool, bool, usize) {
206    let method_name = &f.name;
207    let mut has_behavior = false;
208    let mut is_override = false;
209    let mut is_notify = false;
210    if method_name == "__init__" {
211        collect_init_fields(func_node, src, field_names);
212    } else {
213        has_behavior = true;
214    }
215    let sc = func_node
216        .child_by_field_name("body")
217        .map(|b| count_self_calls(b, src))
218        .unwrap_or(0);
219    if method_name.starts_with("__") && method_name.ends_with("__") && method_name != "__init__" {
220        is_override = true;
221    }
222    if method_name.contains("notify") || method_name.contains("emit") {
223        is_notify = true;
224    }
225    f.is_exported = !method_name.starts_with('_');
226    (has_behavior, is_override, is_notify, sc)
227}
228
229struct ClassScan {
230    methods: Vec<FunctionInfo>,
231    field_names: Vec<String>,
232    delegating_count: usize,
233    has_behavior: bool,
234    override_count: usize,
235    self_call_count: usize,
236    has_notify_method: bool,
237}
238
239fn scan_class_methods(
240    body: Node,
241    src: &[u8],
242    imports_map: &crate::type_ref::ImportsMap,
243) -> ClassScan {
244    let mut s = ClassScan {
245        methods: Vec::new(),
246        field_names: Vec::new(),
247        delegating_count: 0,
248        has_behavior: false,
249        override_count: 0,
250        self_call_count: 0,
251        has_notify_method: false,
252    };
253    let mut cursor = body.walk();
254    for child in body.children(&mut cursor) {
255        let Some(func_node) = find_method_def(child) else {
256            continue;
257        };
258        let Some(mut f) = extract_function(func_node, src, imports_map) else {
259            continue;
260        };
261        if f.is_delegating {
262            s.delegating_count += 1;
263        }
264        let (behav, over, notify, sc) = process_method(func_node, &mut f, src, &mut s.field_names);
265        s.has_behavior |= behav;
266        if over {
267            s.override_count += 1;
268        }
269        if notify {
270            s.has_notify_method = true;
271        }
272        s.self_call_count += sc;
273        s.methods.push(f);
274    }
275    s
276}
277
278fn extract_class(
279    node: Node,
280    src: &[u8],
281    imports_map: &crate::type_ref::ImportsMap,
282    top_functions: &mut Vec<FunctionInfo>,
283) -> Option<ClassInfo> {
284    let name_node = node.child_by_field_name("name")?;
285    let name = node_text(name_node, src).to_string();
286    let name_col = name_node.start_position().column;
287    let name_end_col = name_node.end_position().column;
288    let start_line = node.start_position().row + 1;
289    let end_line = node.end_position().row + 1;
290    let body = node.child_by_field_name("body")?;
291    let s = scan_class_methods(body, src, imports_map);
292    let method_count = s.methods.len();
293    top_functions.extend(s.methods);
294
295    Some(ClassInfo {
296        name,
297        start_line,
298        end_line,
299        name_col,
300        name_end_col,
301        line_count: end_line - start_line + 1,
302        method_count,
303        is_exported: true,
304        delegating_method_count: s.delegating_count,
305        field_count: s.field_names.len(),
306        has_listener_field: s.field_names.iter().any(|n| has_listener_name(n)),
307        field_names: s.field_names,
308        field_types: Vec::new(),
309        has_behavior: s.has_behavior,
310        is_interface: has_only_pass_or_ellipsis(body, src),
311        parent_name: extract_parent_name(node, src),
312        override_count: s.override_count,
313        self_call_count: s.self_call_count,
314        has_notify_method: s.has_notify_method,
315    })
316}
317
318// --- imports ---
319
320fn collect_import(node: Node, src: &[u8], imports: &mut Vec<ImportInfo>) {
321    let line = node.start_position().row + 1;
322    let col = node.start_position().column;
323    let mut cursor = node.walk();
324    for child in node.children(&mut cursor) {
325        if child.kind() == "dotted_name" || child.kind() == "aliased_import" {
326            let text = node_text(child, src);
327            imports.push(ImportInfo {
328                source: text.to_string(),
329                line,
330                col,
331                ..Default::default()
332            });
333        }
334    }
335}
336
337fn collect_import_from(node: Node, src: &[u8], imports: &mut Vec<ImportInfo>) {
338    let line = node.start_position().row + 1;
339    let col = node.start_position().column;
340    let module = node
341        .child_by_field_name("module_name")
342        .map(|n| node_text(n, src).to_string())
343        .unwrap_or_default();
344    let mut cursor = node.walk();
345    let mut has_names = false;
346    for child in node.children(&mut cursor) {
347        if child.kind() == "dotted_name" || child.kind() == "aliased_import" {
348            let n = node_text(child, src).to_string();
349            if n != module {
350                imports.push(ImportInfo {
351                    source: format!("{module}.{n}"),
352                    line,
353                    col,
354                    ..Default::default()
355                });
356                has_names = true;
357            }
358        }
359    }
360    if !has_names {
361        imports.push(ImportInfo {
362            source: module,
363            line,
364            col,
365            ..Default::default()
366        });
367    }
368}
369
370// --- helpers ---
371
372fn node_text<'a>(node: Node, src: &'a [u8]) -> &'a str {
373    node.utf8_text(src).unwrap_or("")
374}
375
376fn count_complexity(node: Node) -> usize {
377    let mut complexity = 1usize;
378    let mut cursor = node.walk();
379    visit_all(node, &mut cursor, &mut |n| {
380        match n.kind() {
381            "if_statement"
382            | "elif_clause"
383            | "for_statement"
384            | "while_statement"
385            | "except_clause"
386            | "with_statement"
387            | "assert_statement"
388            | "conditional_expression"
389            | "boolean_operator"
390            | "list_comprehension"
391            | "set_comprehension"
392            | "dictionary_comprehension"
393            | "generator_expression" => {
394                complexity += 1;
395            }
396            "match_statement" => {} // match itself doesn't add, cases do
397            "case_clause" => {
398                complexity += 1;
399            }
400            _ => {}
401        }
402    });
403    complexity
404}
405
406fn hash_ast_structure(node: Node) -> u64 {
407    let mut hasher = DefaultHasher::new();
408    hash_node(node, &mut hasher);
409    hasher.finish()
410}
411
412fn hash_node(node: Node, hasher: &mut DefaultHasher) {
413    node.kind().hash(hasher);
414    let mut cursor = node.walk();
415    for child in node.children(&mut cursor) {
416        hash_node(child, hasher);
417    }
418}
419
420fn max_chain_depth(node: Node) -> usize {
421    let mut max = 0usize;
422    let mut cursor = node.walk();
423    visit_all(node, &mut cursor, &mut |n| {
424        if n.kind() == "attribute" {
425            let depth = chain_len(n);
426            if depth > max {
427                max = depth;
428            }
429        }
430    });
431    max
432}
433
434fn chain_len(node: Node) -> usize {
435    let mut depth = 0usize;
436    let mut current = node;
437    while current.kind() == "attribute" || current.kind() == "call" {
438        if current.kind() == "attribute" {
439            depth += 1;
440        }
441        if let Some(obj) = current.child(0) {
442            current = obj;
443        } else {
444            break;
445        }
446    }
447    depth
448}
449
450fn collect_py_arm_values(body: Node, src: &[u8]) -> Vec<cha_core::ArmValue> {
451    let mut out = Vec::new();
452    crate::switch_arms::walk_arms(body, src, &mut out, &|n| n.kind() == "case_clause");
453    out
454}
455
456fn count_match_arms(node: Node) -> usize {
457    let mut count = 0usize;
458    let mut cursor = node.walk();
459    visit_all(node, &mut cursor, &mut |n| {
460        if n.kind() == "case_clause" {
461            count += 1;
462        }
463    });
464    count
465}
466
467fn collect_external_refs(node: Node, src: &[u8]) -> Vec<String> {
468    let mut refs = Vec::new();
469    let mut cursor = node.walk();
470    visit_all(node, &mut cursor, &mut |n| {
471        if n.kind() != "attribute" {
472            return;
473        }
474        let Some(obj) = n.child(0) else { return };
475        let text = node_text(obj, src);
476        if text != "self"
477            && !text.is_empty()
478            && text.starts_with(|c: char| c.is_lowercase())
479            && !refs.contains(&text.to_string())
480        {
481            refs.push(text.to_string());
482        }
483    });
484    refs
485}
486
487fn unwrap_single_call(body: Node) -> Option<Node> {
488    let mut c = body.walk();
489    let stmts: Vec<Node> = body
490        .children(&mut c)
491        .filter(|n| !n.is_extra() && n.kind() != "pass_statement" && n.kind() != "comment")
492        .collect();
493    if stmts.len() != 1 {
494        return None;
495    }
496    let stmt = stmts[0];
497    match stmt.kind() {
498        "return_statement" => stmt.child(1).filter(|v| v.kind() == "call"),
499        "expression_statement" => stmt.child(0).filter(|v| v.kind() == "call"),
500        _ => None,
501    }
502}
503
504fn check_delegating(body: Node, src: &[u8]) -> bool {
505    let Some(func) = unwrap_single_call(body).and_then(|c| c.child(0)) else {
506        return false;
507    };
508    let text = node_text(func, src);
509    text.contains('.') && !text.starts_with("self.")
510}
511
512fn count_comment_lines(node: Node, src: &[u8]) -> usize {
513    let mut count = 0usize;
514    let mut cursor = node.walk();
515    visit_all(node, &mut cursor, &mut |n| {
516        if n.kind() == "comment" {
517            count += 1;
518        } else if n.kind() == "string" || n.kind() == "expression_statement" {
519            // docstrings
520            let text = node_text(n, src);
521            if text.starts_with("\"\"\"") || text.starts_with("'''") {
522                count += text.lines().count();
523            }
524        }
525    });
526    count
527}
528
529fn collect_self_refs(body: Node, src: &[u8]) -> Vec<String> {
530    let mut refs = Vec::new();
531    let mut cursor = body.walk();
532    visit_all(body, &mut cursor, &mut |n| {
533        if n.kind() != "attribute" {
534            return;
535        }
536        let is_self = n.child(0).is_some_and(|o| node_text(o, src) == "self");
537        if !is_self {
538            return;
539        }
540        if let Some(attr) = n.child_by_field_name("attribute") {
541            let name = node_text(attr, src).to_string();
542            if !refs.contains(&name) {
543                refs.push(name);
544            }
545        }
546    });
547    refs
548}
549
550fn collect_none_checks(body: Node, src: &[u8]) -> Vec<String> {
551    let mut fields = Vec::new();
552    let mut cursor = body.walk();
553    visit_all(body, &mut cursor, &mut |n| {
554        if n.kind() != "comparison_operator" {
555            return;
556        }
557        let text = node_text(n, src);
558        if !text.contains("is None") && !text.contains("is not None") && !text.contains("== None") {
559            return;
560        }
561        if let Some(left) = n.child(0) {
562            let name = node_text(left, src).to_string();
563            if !fields.contains(&name) {
564                fields.push(name);
565            }
566        }
567    });
568    fields
569}
570
571fn is_self_or_cls(name: &str) -> bool {
572    name == "self" || name == "cls"
573}
574
575fn param_name_and_type(child: Node, src: &[u8]) -> Option<(String, String)> {
576    match child.kind() {
577        "identifier" => {
578            let name = node_text(child, src);
579            (!is_self_or_cls(name)).then(|| (name.to_string(), "Any".to_string()))
580        }
581        "typed_parameter" | "default_parameter" | "typed_default_parameter" => {
582            let name = child
583                .child_by_field_name("name")
584                .or_else(|| child.child(0))
585                .map(|n| node_text(n, src))
586                .unwrap_or("");
587            if is_self_or_cls(name) {
588                return None;
589            }
590            let ty = child
591                .child_by_field_name("type")
592                .map(|n| node_text(n, src).to_string())
593                .unwrap_or_else(|| "Any".to_string());
594            Some((name.to_string(), ty))
595        }
596        "list_splat_pattern" | "dictionary_splat_pattern" => {
597            Some(("*".to_string(), "Any".to_string()))
598        }
599        _ => None,
600    }
601}
602
603fn extract_params(
604    params_node: Node,
605    src: &[u8],
606    imports_map: &crate::type_ref::ImportsMap,
607) -> (usize, Vec<cha_core::TypeRef>, Vec<String>) {
608    let mut count = 0usize;
609    let mut types = Vec::new();
610    let mut names = Vec::new();
611    let mut cursor = params_node.walk();
612    for child in params_node.children(&mut cursor) {
613        if let Some((name, ty)) = param_name_and_type(child, src) {
614            count += 1;
615            types.push(crate::type_ref::resolve(ty, imports_map));
616            names.push(name.to_string());
617        }
618    }
619    (count, types, names)
620}
621
622fn count_optional(params_node: Node) -> usize {
623    let mut count = 0usize;
624    let mut cursor = params_node.walk();
625    for child in params_node.children(&mut cursor) {
626        if child.kind() == "default_parameter" || child.kind() == "typed_default_parameter" {
627            count += 1;
628        }
629    }
630    count
631}
632
633fn collect_init_fields(func_node: Node, src: &[u8], fields: &mut Vec<String>) {
634    let Some(body) = func_node.child_by_field_name("body") else {
635        return;
636    };
637    let mut cursor = body.walk();
638    visit_all(body, &mut cursor, &mut |n| {
639        if n.kind() != "assignment" {
640            return;
641        }
642        let Some(left) = n.child_by_field_name("left") else {
643            return;
644        };
645        if left.kind() != "attribute" {
646            return;
647        }
648        let is_self = left.child(0).is_some_and(|o| node_text(o, src) == "self");
649        if !is_self {
650            return;
651        }
652        if let Some(attr) = left.child_by_field_name("attribute") {
653            let name = node_text(attr, src).to_string();
654            if !fields.contains(&name) {
655                fields.push(name);
656            }
657        }
658    });
659}
660
661fn count_self_calls(body: Node, src: &[u8]) -> usize {
662    let mut count = 0;
663    let mut cursor = body.walk();
664    visit_all(body, &mut cursor, &mut |n| {
665        if n.kind() != "call" {
666            return;
667        }
668        let is_self_call = n
669            .child(0)
670            .filter(|f| f.kind() == "attribute")
671            .and_then(|f| f.child(0))
672            .is_some_and(|obj| node_text(obj, src) == "self");
673        if is_self_call {
674            count += 1;
675        }
676    });
677    count
678}
679
680fn is_stub_body(node: Node, src: &[u8]) -> bool {
681    node.child_by_field_name("body")
682        .is_none_or(|b| has_only_pass_or_ellipsis(b, src))
683}
684
685fn has_only_pass_or_ellipsis(body: Node, src: &[u8]) -> bool {
686    let mut cursor = body.walk();
687    for child in body.children(&mut cursor) {
688        let ok = match child.kind() {
689            "pass_statement" | "ellipsis" | "comment" => true,
690            "expression_statement" => child.child(0).is_none_or(|expr| {
691                let text = node_text(expr, src);
692                text == "..." || text.starts_with("\"\"\"") || text.starts_with("'''")
693            }),
694            "function_definition" => is_stub_body(child, src),
695            "decorated_definition" => {
696                let mut inner = child.walk();
697                child
698                    .children(&mut inner)
699                    .filter(|c| c.kind() == "function_definition")
700                    .all(|c| is_stub_body(c, src))
701            }
702            _ => false,
703        };
704        if !ok {
705            return false;
706        }
707    }
708    true
709}
710
711fn cognitive_complexity_py(node: tree_sitter::Node) -> usize {
712    let mut score = 0;
713    cc_walk_py(node, 0, &mut score);
714    score
715}
716
717fn cc_walk_py(node: tree_sitter::Node, nesting: usize, score: &mut usize) {
718    match node.kind() {
719        "if_statement" => {
720            *score += 1 + nesting;
721            cc_children_py(node, nesting + 1, score);
722            return;
723        }
724        "for_statement" | "while_statement" => {
725            *score += 1 + nesting;
726            cc_children_py(node, nesting + 1, score);
727            return;
728        }
729        "match_statement" => {
730            *score += 1 + nesting;
731            cc_children_py(node, nesting + 1, score);
732            return;
733        }
734        "elif_clause" | "else_clause" => {
735            *score += 1;
736        }
737        "boolean_operator" => {
738            *score += 1;
739        }
740        "except_clause" => {
741            *score += 1 + nesting;
742            cc_children_py(node, nesting + 1, score);
743            return;
744        }
745        "lambda" => {
746            cc_children_py(node, nesting + 1, score);
747            return;
748        }
749        _ => {}
750    }
751    cc_children_py(node, nesting, score);
752}
753
754fn cc_children_py(node: tree_sitter::Node, nesting: usize, score: &mut usize) {
755    let mut cursor = node.walk();
756    for child in node.children(&mut cursor) {
757        cc_walk_py(child, nesting, score);
758    }
759}
760
761fn extract_match_target_py(body: tree_sitter::Node, src: &[u8]) -> Option<String> {
762    let mut target = None;
763    let mut cursor = body.walk();
764    visit_all(body, &mut cursor, &mut |n| {
765        if n.kind() == "match_statement"
766            && target.is_none()
767            && let Some(subj) = n.child_by_field_name("subject")
768        {
769            target = Some(node_text(subj, src).to_string());
770        }
771    });
772    target
773}
774
775fn collect_calls_py(body: tree_sitter::Node, src: &[u8]) -> Vec<String> {
776    let mut calls = Vec::new();
777    let mut cursor = body.walk();
778    visit_all(body, &mut cursor, &mut |n| {
779        if n.kind() == "call"
780            && let Some(func) = n.child(0)
781        {
782            let name = node_text(func, src).to_string();
783            if !calls.contains(&name) {
784                calls.push(name);
785            }
786        }
787    });
788    calls
789}
790
791fn collect_comments(root: Node, src: &[u8]) -> Vec<cha_core::CommentInfo> {
792    let mut comments = Vec::new();
793    let mut cursor = root.walk();
794    visit_all(root, &mut cursor, &mut |n| {
795        if n.kind().contains("comment") {
796            comments.push(cha_core::CommentInfo {
797                text: node_text(n, src).to_string(),
798                line: n.start_position().row + 1,
799            });
800        }
801    });
802    comments
803}
804
805fn visit_all<F: FnMut(Node)>(node: Node, cursor: &mut tree_sitter::TreeCursor, f: &mut F) {
806    f(node);
807    if cursor.goto_first_child() {
808        loop {
809            let child_node = cursor.node();
810            let mut child_cursor = child_node.walk();
811            visit_all(child_node, &mut child_cursor, f);
812            if !cursor.goto_next_sibling() {
813                break;
814            }
815        }
816        cursor.goto_parent();
817    }
818}