Skip to main content

imp_core/tools/scan/
go.rs

1//! Go tree-sitter extraction — structs, interfaces, functions, methods.
2//! Ported from uu-manifest's Go language adapter.
3
4use tree_sitter::{Node, Parser};
5
6use super::types::*;
7
8pub fn parse(source: &str, file: &str, result: &mut ScanResult) {
9    let mut parser = Parser::new();
10    if parser
11        .set_language(&tree_sitter_go::LANGUAGE.into())
12        .is_err()
13    {
14        return;
15    }
16    let tree = match parser.parse(source, None) {
17        Some(t) => t,
18        None => return,
19    };
20    let is_test = file.ends_with("_test.go");
21    extract_go(&tree.root_node(), source, file, is_test, result);
22}
23
24fn extract_go(root: &Node, source: &str, file: &str, is_test: bool, result: &mut ScanResult) {
25    let mut cursor = root.walk();
26    for child in root.named_children(&mut cursor) {
27        match child.kind() {
28            "type_declaration" => extract_type_decl(&child, source, file, result),
29            "function_declaration" => extract_function(&child, source, file, is_test, result),
30            "method_declaration" => extract_method(&child, source, file, is_test, result),
31            _ => {}
32        }
33    }
34}
35
36fn extract_type_decl(node: &Node, source: &str, file: &str, result: &mut ScanResult) {
37    let mut cursor = node.walk();
38    for child in node.named_children(&mut cursor) {
39        if child.kind() == "type_spec" {
40            let name = match child.child_by_field_name("name") {
41                Some(n) => node_text(&n, source).to_string(),
42                None => continue,
43            };
44            let vis = go_visibility(&name);
45            let type_node = match child.child_by_field_name("type") {
46                Some(n) => n,
47                None => continue,
48            };
49
50            match type_node.kind() {
51                "struct_type" => {
52                    let fields = extract_struct_fields(&type_node, source);
53                    result.types.insert(
54                        name.clone(),
55                        TypeInfo {
56                            name,
57                            source: source_loc(file, &child),
58                            kind: TypeKind::Struct,
59                            fields,
60                            visibility: vis,
61                            ..Default::default()
62                        },
63                    );
64                }
65                "interface_type" => {
66                    let methods = extract_interface_methods(&type_node, source);
67                    result.types.insert(
68                        name.clone(),
69                        TypeInfo {
70                            name,
71                            source: source_loc(file, &child),
72                            kind: TypeKind::Interface,
73                            methods,
74                            visibility: vis,
75                            ..Default::default()
76                        },
77                    );
78                }
79                _ => {
80                    result.types.insert(
81                        name.clone(),
82                        TypeInfo {
83                            name,
84                            source: source_loc(file, &child),
85                            kind: TypeKind::TypeAlias,
86                            visibility: vis,
87                            ..Default::default()
88                        },
89                    );
90                }
91            }
92        }
93    }
94}
95
96fn extract_struct_fields(node: &Node, source: &str) -> Vec<Field> {
97    let mut fields = Vec::new();
98    let mut cursor = node.walk();
99    for child in node.named_children(&mut cursor) {
100        if child.kind() == "field_declaration_list" {
101            let mut list_cursor = child.walk();
102            for field_node in child.named_children(&mut list_cursor) {
103                if field_node.kind() == "field_declaration" {
104                    extract_single_field(&field_node, source, &mut fields);
105                }
106            }
107        } else if child.kind() == "field_declaration" {
108            extract_single_field(&child, source, &mut fields);
109        }
110    }
111    fields
112}
113
114fn extract_single_field(field_node: &Node, source: &str, fields: &mut Vec<Field>) {
115    let type_name = field_node
116        .child_by_field_name("type")
117        .map(|t| node_text(&t, source).to_string())
118        .unwrap_or_default();
119    let mut inner = field_node.walk();
120    for name_child in field_node.named_children(&mut inner) {
121        if name_child.kind() == "field_identifier" {
122            let name = node_text(&name_child, source).to_string();
123            let optional = type_name.starts_with('*');
124            fields.push(Field {
125                name,
126                type_name: type_name.clone(),
127                optional,
128            });
129        }
130    }
131}
132
133fn extract_interface_methods(node: &Node, source: &str) -> Vec<String> {
134    let mut methods = Vec::new();
135    let mut cursor = node.walk();
136    for child in node.named_children(&mut cursor) {
137        if child.kind() == "method_spec" || child.kind() == "method_elem" {
138            if let Some(name_node) = child.child_by_field_name("name") {
139                methods.push(node_text(&name_node, source).to_string());
140            }
141        }
142    }
143    methods
144}
145
146fn extract_function(
147    node: &Node,
148    source: &str,
149    file: &str,
150    is_test_file: bool,
151    result: &mut ScanResult,
152) {
153    let name = match node.child_by_field_name("name") {
154        Some(n) => node_text(&n, source).to_string(),
155        None => return,
156    };
157    let vis = go_visibility(&name);
158    let is_test = is_test_file && name.starts_with("Test");
159    let sig = build_fn_signature(node, source, &name);
160
161    result.functions.insert(
162        name.clone(),
163        FunctionInfo {
164            name,
165            source: source_loc(file, node),
166            signature: sig,
167            visibility: vis,
168            is_async: false,
169            is_test,
170        },
171    );
172}
173
174fn extract_method(
175    node: &Node,
176    source: &str,
177    file: &str,
178    is_test_file: bool,
179    result: &mut ScanResult,
180) {
181    let name = match node.child_by_field_name("name") {
182        Some(n) => node_text(&n, source).to_string(),
183        None => return,
184    };
185    let receiver_type = extract_receiver_type(node, source);
186    let vis = go_visibility(&name);
187    let is_test = is_test_file && name.starts_with("Test");
188    let sig = build_fn_signature(node, source, &name);
189
190    let qualified = if receiver_type.is_empty() {
191        name.clone()
192    } else {
193        format!("{receiver_type}::{name}")
194    };
195    result.functions.insert(
196        qualified,
197        FunctionInfo {
198            name: name.clone(),
199            source: source_loc(file, node),
200            signature: sig,
201            visibility: vis,
202            is_async: false,
203            is_test,
204        },
205    );
206
207    if !receiver_type.is_empty() {
208        if let Some(typedef) = result.types.get_mut(&receiver_type) {
209            if !typedef.methods.contains(&name) {
210                typedef.methods.push(name);
211            }
212        }
213    }
214}
215
216fn extract_receiver_type(node: &Node, source: &str) -> String {
217    let receiver = match node.child_by_field_name("receiver") {
218        Some(r) => r,
219        None => return String::new(),
220    };
221    let mut cursor = receiver.walk();
222    for child in receiver.named_children(&mut cursor) {
223        if child.kind() == "parameter_declaration" {
224            if let Some(type_node) = child.child_by_field_name("type") {
225                let text = node_text(&type_node, source);
226                return text.trim_start_matches('*').to_string();
227            }
228        }
229    }
230    String::new()
231}
232
233fn build_fn_signature(node: &Node, source: &str, name: &str) -> String {
234    let params = node
235        .child_by_field_name("parameters")
236        .map(|n| node_text(&n, source))
237        .unwrap_or("()");
238    let ret = node
239        .child_by_field_name("result")
240        .map(|n| format!(" {}", node_text(&n, source)))
241        .unwrap_or_default();
242    format!("func {name}{params}{ret}")
243}
244
245fn go_visibility(name: &str) -> Visibility {
246    if name.starts_with(|c: char| c.is_uppercase()) {
247        Visibility::Public
248    } else {
249        Visibility::Private
250    }
251}
252
253fn node_text<'a>(node: &Node, source: &'a str) -> &'a str {
254    &source[node.byte_range()]
255}
256
257fn source_loc(file: &str, node: &Node) -> String {
258    format!("{}:{}", file, node.start_position().row + 1)
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    fn parse_go_str(source: &str) -> ScanResult {
266        let mut result = ScanResult::default();
267        parse(source, "main.go", &mut result);
268        result
269    }
270
271    #[test]
272    fn struct_with_fields() {
273        let r = parse_go_str(
274            r#"
275package main
276type User struct {
277    Name  string
278    Email string
279    Age   int
280}
281"#,
282        );
283        let t = &r.types["User"];
284        assert_eq!(t.kind, TypeKind::Struct);
285        assert_eq!(t.visibility, Visibility::Public);
286        assert_eq!(t.fields.len(), 3);
287    }
288
289    #[test]
290    fn unexported_is_private() {
291        let r = parse_go_str("package main\ntype config struct { debug bool }");
292        assert_eq!(r.types["config"].visibility, Visibility::Private);
293    }
294
295    #[test]
296    fn interface_methods() {
297        let r = parse_go_str(
298            r#"
299package main
300type Repository interface {
301    Get(id string) error
302    Save(item Item) error
303}
304"#,
305        );
306        let t = &r.types["Repository"];
307        assert_eq!(t.kind, TypeKind::Interface);
308        assert!(t.methods.contains(&"Get".to_string()));
309        assert!(t.methods.contains(&"Save".to_string()));
310    }
311
312    #[test]
313    fn method_adds_to_type() {
314        let r = parse_go_str(
315            r#"
316package main
317type Server struct { Port int }
318func (s *Server) Start() error { return nil }
319"#,
320        );
321        let t = &r.types["Server"];
322        assert!(t.methods.contains(&"Start".to_string()));
323        assert!(r.functions.contains_key("Server::Start"));
324    }
325
326    #[test]
327    fn pointer_field_optional() {
328        let r = parse_go_str(
329            "package main\ntype Config struct {\n    Name string\n    Parent *Config\n}",
330        );
331        let t = &r.types["Config"];
332        assert!(!t.fields[0].optional);
333        assert!(t.fields[1].optional);
334    }
335}