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