Skip to main content

cha_parser/
golang.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 GolangParser;
10
11impl LanguageParser for GolangParser {
12    fn language_name(&self) -> &str {
13        "go"
14    }
15
16    fn parse(&self, file: &SourceFile) -> Option<SourceModel> {
17        let mut parser = Parser::new();
18        parser.set_language(&tree_sitter_go::LANGUAGE.into()).ok()?;
19        let tree = parser.parse(&file.content, None)?;
20        let root = tree.root_node();
21        let src = file.content.as_bytes();
22
23        let mut functions = Vec::new();
24        let mut classes = Vec::new();
25        let mut imports = Vec::new();
26
27        collect_top_level(root, src, &mut functions, &mut classes, &mut imports);
28
29        Some(SourceModel {
30            language: "go".into(),
31            total_lines: file.line_count(),
32            functions,
33            classes,
34            imports,
35            comments: collect_comments(root, src),
36            type_aliases: vec![], // TODO(parser): extract type aliases from 'type X = Y' declarations
37        })
38    }
39}
40
41fn collect_top_level(
42    root: Node,
43    src: &[u8],
44    functions: &mut Vec<FunctionInfo>,
45    classes: &mut Vec<ClassInfo>,
46    imports: &mut Vec<ImportInfo>,
47) {
48    let mut cursor = root.walk();
49    for child in root.children(&mut cursor) {
50        match child.kind() {
51            "function_declaration" | "method_declaration" => {
52                if let Some(f) = extract_function(child, src) {
53                    functions.push(f);
54                }
55            }
56            "type_declaration" => extract_type_decl(child, src, classes),
57            "import_declaration" => collect_imports(child, src, imports),
58            _ => {}
59        }
60    }
61}
62
63fn extract_function(node: Node, src: &[u8]) -> Option<FunctionInfo> {
64    let name_node = node.child_by_field_name("name")?;
65    let name = node_text(name_node, src).to_string();
66    let name_col = name_node.start_position().column;
67    let name_end_col = name_node.end_position().column;
68    let start_line = node.start_position().row + 1;
69    let end_line = node.end_position().row + 1;
70    let body = node.child_by_field_name("body");
71    let params = node.child_by_field_name("parameters");
72    let (param_count, param_types) = params
73        .map(|p| extract_params(p, src))
74        .unwrap_or((0, vec![]));
75    let is_exported = name.starts_with(|c: char| c.is_uppercase());
76
77    Some(FunctionInfo {
78        name,
79        start_line,
80        end_line,
81        name_col,
82        name_end_col,
83        line_count: end_line - start_line + 1,
84        complexity: count_complexity(node),
85        body_hash: body.map(hash_ast),
86        is_exported,
87        parameter_count: param_count,
88        parameter_types: param_types,
89        chain_depth: body.map(max_chain_depth).unwrap_or(0),
90        switch_arms: body.map(count_case_clauses).unwrap_or(0),
91        external_refs: body
92            .map(|b| collect_external_refs(b, src))
93            .unwrap_or_default(),
94        is_delegating: body.map(|b| check_delegating(b, src)).unwrap_or(false),
95        comment_lines: count_comment_lines(node, src),
96        referenced_fields: body
97            .map(|b| collect_field_refs_go(b, src))
98            .unwrap_or_default(),
99        null_check_fields: body.map(|b| collect_nil_checks(b, src)).unwrap_or_default(),
100        switch_dispatch_target: body.and_then(|b| extract_switch_target_go(b, src)),
101        optional_param_count: 0,
102        called_functions: collect_calls(body, src),
103        cognitive_complexity: body.map(|b| cognitive_complexity_go(b)).unwrap_or(0),
104    })
105}
106
107fn extract_type_decl(node: Node, src: &[u8], classes: &mut Vec<ClassInfo>) {
108    let mut cursor = node.walk();
109    for child in node.children(&mut cursor) {
110        if child.kind() == "type_spec"
111            && let Some(c) = extract_struct(child, src)
112        {
113            classes.push(c);
114        }
115    }
116}
117
118fn extract_struct(node: Node, src: &[u8]) -> Option<ClassInfo> {
119    let name_node = node.child_by_field_name("name")?;
120    let name = node_text(name_node, src).to_string();
121    let name_col = name_node.start_position().column;
122    let name_end_col = name_node.end_position().column;
123    let type_node = node.child_by_field_name("type")?;
124    if type_node.kind() != "struct_type" && type_node.kind() != "interface_type" {
125        return None;
126    }
127    let is_interface = type_node.kind() == "interface_type";
128    let start_line = node.start_position().row + 1;
129    let end_line = node.end_position().row + 1;
130    let field_count = count_struct_fields(type_node);
131    let is_exported = name.starts_with(|c: char| c.is_uppercase());
132
133    Some(ClassInfo {
134        name,
135        start_line,
136        end_line,
137        name_col,
138        name_end_col,
139        line_count: end_line - start_line + 1,
140        method_count: 0,
141        is_exported,
142        delegating_method_count: 0,
143        field_count,
144        field_names: Vec::new(),
145        field_types: Vec::new(),
146        has_behavior: false,
147        is_interface,
148        parent_name: None,
149        override_count: 0,
150        self_call_count: 0,
151        has_listener_field: false,
152        has_notify_method: false,
153    })
154}
155
156fn count_struct_fields(node: Node) -> usize {
157    let mut count = 0;
158    let mut cursor = node.walk();
159    visit_all(node, &mut cursor, &mut |n| {
160        if n.kind() == "field_declaration" {
161            count += 1;
162        }
163    });
164    count
165}
166
167fn collect_imports(node: Node, src: &[u8], imports: &mut Vec<ImportInfo>) {
168    let mut cursor = node.walk();
169    visit_all(node, &mut cursor, &mut |n| {
170        if n.kind() == "import_spec" {
171            let line = n.start_position().row + 1;
172            let col = n.start_position().column;
173            let path_node = n.child_by_field_name("path").unwrap_or(n);
174            let text = node_text(path_node, src).trim_matches('"').to_string();
175            if !text.is_empty() {
176                imports.push(ImportInfo {
177                    source: text,
178                    line,
179                    col,
180                    ..Default::default()
181                });
182            }
183        }
184    });
185}
186
187fn extract_params(params: Node, src: &[u8]) -> (usize, Vec<String>) {
188    let mut count = 0;
189    let mut types = Vec::new();
190    let mut cursor = params.walk();
191    for child in params.children(&mut cursor) {
192        if child.kind() == "parameter_declaration" {
193            let ty = child
194                .child_by_field_name("type")
195                .map(|t| node_text(t, src).to_string())
196                .unwrap_or_else(|| "any".into());
197            // Count names in this declaration (e.g. `a, b int` = 2 params)
198            let mut inner = child.walk();
199            let names: usize = child
200                .children(&mut inner)
201                .filter(|c| c.kind() == "identifier")
202                .count()
203                .max(1);
204            for _ in 0..names {
205                count += 1;
206                types.push(ty.clone());
207            }
208        }
209    }
210    (count, types)
211}
212
213fn count_complexity(node: Node) -> usize {
214    let mut c = 1usize;
215    let mut cursor = node.walk();
216    visit_all(node, &mut cursor, &mut |n| match n.kind() {
217        "if_statement" | "for_statement" | "expression_case" | "default_case" | "type_case"
218        | "select_statement" | "go_statement" => c += 1,
219        "binary_expression" => {
220            if let Some(op) = n.child_by_field_name("operator") {
221                let kind = op.kind();
222                if kind == "&&" || kind == "||" {
223                    c += 1;
224                }
225            }
226        }
227        _ => {}
228    });
229    c
230}
231
232fn max_chain_depth(node: Node) -> usize {
233    let mut max = 0;
234    let mut cursor = node.walk();
235    visit_all(node, &mut cursor, &mut |n| {
236        if n.kind() == "selector_expression" {
237            let d = chain_len(n);
238            if d > max {
239                max = d;
240            }
241        }
242    });
243    max
244}
245
246fn chain_len(node: Node) -> usize {
247    let mut depth = 0;
248    let mut current = node;
249    while current.kind() == "selector_expression" || current.kind() == "call_expression" {
250        if current.kind() == "selector_expression" {
251            depth += 1;
252        }
253        match current.child(0) {
254            Some(c) => current = c,
255            None => break,
256        }
257    }
258    depth
259}
260
261fn count_case_clauses(node: Node) -> usize {
262    let mut count = 0;
263    let mut cursor = node.walk();
264    visit_all(node, &mut cursor, &mut |n| {
265        if n.kind() == "expression_case" || n.kind() == "default_case" || n.kind() == "type_case" {
266            count += 1;
267        }
268    });
269    count
270}
271
272fn collect_external_refs(node: Node, src: &[u8]) -> Vec<String> {
273    let mut refs = Vec::new();
274    let mut cursor = node.walk();
275    visit_all(node, &mut cursor, &mut |n| {
276        if n.kind() == "selector_expression"
277            && let Some(obj) = n.child(0)
278            && obj.kind() == "identifier"
279        {
280            let text = node_text(obj, src).to_string();
281            if !refs.contains(&text) {
282                refs.push(text);
283            }
284        }
285    });
286    refs
287}
288
289fn check_delegating(body: Node, src: &[u8]) -> bool {
290    let mut cursor = body.walk();
291    let stmts: Vec<Node> = body
292        .children(&mut cursor)
293        .filter(|n| n.kind() != "{" && n.kind() != "}" && n.kind() != "comment")
294        .collect();
295    if stmts.len() != 1 {
296        return false;
297    }
298    let stmt = stmts[0];
299    let call = match stmt.kind() {
300        "return_statement" => stmt.child(1).filter(|c| c.kind() == "call_expression"),
301        "expression_statement" => stmt.child(0).filter(|c| c.kind() == "call_expression"),
302        _ => None,
303    };
304    call.and_then(|c| c.child(0))
305        .is_some_and(|f| node_text(f, src).contains('.'))
306}
307
308fn count_comment_lines(node: Node, src: &[u8]) -> usize {
309    let mut count = 0;
310    let mut cursor = node.walk();
311    visit_all(node, &mut cursor, &mut |n| {
312        if n.kind() == "comment" {
313            count += node_text(n, src).lines().count();
314        }
315    });
316    count
317}
318
319fn collect_nil_checks(body: Node, src: &[u8]) -> Vec<String> {
320    let mut fields = Vec::new();
321    let mut cursor = body.walk();
322    visit_all(body, &mut cursor, &mut |n| {
323        if n.kind() != "binary_expression" {
324            return;
325        }
326        let text = node_text(n, src);
327        if !text.contains("nil") {
328            return;
329        }
330        if let Some(left) = n.child(0) {
331            let name = node_text(left, src).to_string();
332            if !fields.contains(&name) {
333                fields.push(name);
334            }
335        }
336    });
337    fields
338}
339
340fn hash_ast(node: Node) -> u64 {
341    let mut hasher = DefaultHasher::new();
342    hash_node(node, &mut hasher);
343    hasher.finish()
344}
345
346fn hash_node(node: Node, hasher: &mut DefaultHasher) {
347    node.kind().hash(hasher);
348    let mut cursor = node.walk();
349    for child in node.children(&mut cursor) {
350        hash_node(child, hasher);
351    }
352}
353
354fn cognitive_complexity_go(node: Node) -> usize {
355    let mut score = 0;
356    cc_walk(node, 0, &mut score);
357    score
358}
359
360fn cc_walk(node: Node, nesting: usize, score: &mut usize) {
361    match node.kind() {
362        "if_statement" => {
363            *score += 1 + nesting;
364            cc_children(node, nesting + 1, score);
365            return;
366        }
367        "for_statement" => {
368            *score += 1 + nesting;
369            cc_children(node, nesting + 1, score);
370            return;
371        }
372        "expression_switch_statement" | "type_switch_statement" | "select_statement" => {
373            *score += 1 + nesting;
374            cc_children(node, nesting + 1, score);
375            return;
376        }
377        "else_clause" => {
378            *score += 1; // no nesting increment for else
379        }
380        "binary_expression" => {
381            if let Some(op) = node.child_by_field_name("operator")
382                && (op.kind() == "&&" || op.kind() == "||")
383            {
384                *score += 1;
385            }
386        }
387        _ => {}
388    }
389    cc_children(node, nesting, score);
390}
391
392fn cc_children(node: Node, nesting: usize, score: &mut usize) {
393    let mut cursor = node.walk();
394    for child in node.children(&mut cursor) {
395        cc_walk(child, nesting, score);
396    }
397}
398
399fn collect_field_refs_go(body: Node, src: &[u8]) -> Vec<String> {
400    let mut refs = Vec::new();
401    let mut cursor = body.walk();
402    visit_all(body, &mut cursor, &mut |n| {
403        if n.kind() == "selector_expression"
404            && let Some(field) = n.child_by_field_name("field")
405        {
406            let name = node_text(field, src).to_string();
407            if !refs.contains(&name) {
408                refs.push(name);
409            }
410        }
411    });
412    refs
413}
414
415fn extract_switch_target_go(body: Node, src: &[u8]) -> Option<String> {
416    let mut target = None;
417    let mut cursor = body.walk();
418    visit_all(body, &mut cursor, &mut |n| {
419        if (n.kind() == "expression_switch_statement" || n.kind() == "type_switch_statement")
420            && target.is_none()
421            && let Some(val) = n.child_by_field_name("value")
422        {
423            target = Some(node_text(val, src).to_string());
424        }
425    });
426    target
427}
428
429fn collect_calls(body: Option<Node>, src: &[u8]) -> Vec<String> {
430    let Some(body) = body else { return Vec::new() };
431    let mut calls = Vec::new();
432    let mut cursor = body.walk();
433    visit_all(body, &mut cursor, &mut |n| {
434        if n.kind() == "call_expression"
435            && let Some(func) = n.child(0)
436        {
437            let name = node_text(func, src).to_string();
438            if !calls.contains(&name) {
439                calls.push(name);
440            }
441        }
442    });
443    calls
444}
445
446fn node_text<'a>(node: Node, src: &'a [u8]) -> &'a str {
447    node.utf8_text(src).unwrap_or("")
448}
449
450fn collect_comments(root: Node, src: &[u8]) -> Vec<cha_core::CommentInfo> {
451    let mut comments = Vec::new();
452    let mut cursor = root.walk();
453    visit_all(root, &mut cursor, &mut |n| {
454        if n.kind().contains("comment") {
455            comments.push(cha_core::CommentInfo {
456                text: node_text(n, src).to_string(),
457                line: n.start_position().row + 1,
458            });
459        }
460    });
461    comments
462}
463
464fn visit_all<F: FnMut(Node)>(node: Node, cursor: &mut tree_sitter::TreeCursor, f: &mut F) {
465    f(node);
466    if cursor.goto_first_child() {
467        loop {
468            let child_node = cursor.node();
469            let mut child_cursor = child_node.walk();
470            visit_all(child_node, &mut child_cursor, f);
471            if !cursor.goto_next_sibling() {
472                break;
473            }
474        }
475        cursor.goto_parent();
476    }
477}