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