Skip to main content

comindly_core/lang/
go.rs

1//! Go syntax adapter (tree-sitter-go). A package is a directory; methods attach to receivers.
2
3use tree_sitter::Node;
4
5use super::{field_text, line, named_children, text, LangSpec, RawContract, Target};
6use crate::facts::{Args, Import, ImportItem, RefKind, TypeSource, ValueExpr};
7use crate::model::SymbolKind;
8
9pub struct Go;
10
11/// Router registration methods across net/http, gin, echo and chi.
12const ROUTE_METHODS: &[&str] = &[
13    "HandleFunc",
14    "Handle",
15    "GET",
16    "POST",
17    "PUT",
18    "PATCH",
19    "DELETE",
20    "Get",
21    "Post",
22    "Put",
23    "Patch",
24    "Delete",
25];
26
27impl LangSpec for Go {
28    fn receiver(&self) -> &'static str {
29        "self"
30    }
31
32    fn contract(&self, node: Node, src: &[u8]) -> Option<RawContract> {
33        // `api := r.Group("/api")` (gin, echo): a route group with a prefix.
34        if node.kind() == "short_var_declaration" {
35            let left = named_children(node.child_by_field_name("left")?);
36            let right = named_children(node.child_by_field_name("right")?);
37            let ([name], [call]) = (left.as_slice(), right.as_slice()) else {
38                return None;
39            };
40            if name.kind() != "identifier" || call.kind() != "call_expression" {
41                return None;
42            }
43            let ctor = path(call.child_by_field_name("function")?, src)?;
44            if ctor.last().map(String::as_str) != Some("Group") {
45                return None;
46            }
47            return Some(RawContract::Construct {
48                target: Target::Var(text(*name, src).to_string()),
49                ctor,
50                args: args(call.child_by_field_name("arguments")?, src),
51            });
52        }
53        if node.kind() != "call_expression" {
54            return None;
55        }
56        let f = node.child_by_field_name("function")?;
57        let p = path(f, src)?;
58        let (method, receiver) = p.split_last()?;
59        let list = node.child_by_field_name("arguments")?;
60        let args = args(list, src);
61        if receiver == ["http"] && !method.starts_with("Handle") {
62            return Some(RawContract::Op {
63                receiver: receiver.to_vec(),
64                method: method.clone(),
65                args,
66            });
67        }
68        if !ROUTE_METHODS.contains(&method.as_str()) || args.positional.len() < 2 {
69            return None;
70        }
71        let Some(ValueExpr::Lit(pattern)) = args.positional.first().cloned().flatten() else {
72            return None;
73        };
74        let (verb, route) = match pattern.split_once(' ') {
75            Some((v, r)) if v.chars().all(|c| c.is_ascii_uppercase()) => (v.to_string(), r),
76            _ if method.starts_with("Handle") => ("ANY".to_string(), pattern.as_str()),
77            _ => (method.to_uppercase(), pattern.as_str()),
78        };
79        if !route.starts_with('/') {
80            return None;
81        }
82        let handler = named_children(list)
83            .last()
84            .filter(|h| h.kind() != "func_literal")
85            .and_then(|h| path(*h, src))
86            .unwrap_or_default();
87        let (prefix, receiver) = route_scope(node, receiver.to_vec(), src);
88        Some(RawContract::Handle {
89            receiver,
90            method: verb,
91            path: ValueExpr::Lit(format!("{prefix}{route}")),
92            handler,
93        })
94    }
95
96    fn value(&self, node: Node, src: &[u8]) -> Option<(String, ValueExpr)> {
97        if !matches!(node.kind(), "const_spec" | "var_spec") {
98            return None;
99        }
100        let name = field_text(node, "name", src)?.to_string();
101        let v = named_children(node.child_by_field_name("value")?)
102            .into_iter()
103            .next()?;
104        Some((name, value(v, src)?))
105    }
106
107    fn def(&self, node: Node, src: &[u8], _in_class: bool) -> Option<(SymbolKind, String)> {
108        let kind = match node.kind() {
109            "function_declaration" => SymbolKind::Function,
110            "method_declaration" | "method_elem" => SymbolKind::Method,
111            "type_spec" => match node.child_by_field_name("type")?.kind() {
112                "struct_type" => SymbolKind::Struct,
113                "interface_type" => SymbolKind::Interface,
114                _ => SymbolKind::TypeAlias,
115            },
116            "type_alias" => SymbolKind::TypeAlias,
117            _ => return None,
118        };
119        Some((kind, field_text(node, "name", src)?.to_string()))
120    }
121
122    fn owner(&self, def: Node, src: &[u8]) -> Option<String> {
123        if def.kind() != "method_declaration" {
124            return None;
125        }
126        let recv = named_children(def.child_by_field_name("receiver")?)
127            .into_iter()
128            .find(|p| p.kind() == "parameter_declaration")?;
129        type_path(recv.child_by_field_name("type")?, src)?.pop()
130    }
131
132    fn variable(&self, node: Node, src: &[u8]) -> Option<String> {
133        matches!(node.kind(), "var_spec" | "const_spec")
134            .then(|| field_text(node, "name", src).map(str::to_string))?
135    }
136
137    fn import(&self, node: Node, src: &[u8]) -> Option<Vec<Import>> {
138        if node.kind() != "import_declaration" {
139            return None;
140        }
141        let mut specs = Vec::new();
142        for c in named_children(node) {
143            match c.kind() {
144                "import_spec" => specs.push(c),
145                "import_spec_list" => specs.extend(
146                    named_children(c)
147                        .into_iter()
148                        .filter(|s| s.kind() == "import_spec"),
149                ),
150                _ => {}
151            }
152        }
153        let imports = specs
154            .into_iter()
155            .filter_map(|s| {
156                let module = field_text(s, "path", src)?
157                    .trim_matches(|c| c == '"' || c == '`')
158                    .to_string();
159                let item = match s
160                    .child_by_field_name("name")
161                    .map(|n| (n.kind(), text(n, src)))
162                {
163                    Some(("dot", _)) => ImportItem::All,
164                    Some(("blank_identifier", _)) => ImportItem::Module { alias: None },
165                    Some((_, alias)) => ImportItem::Module {
166                        alias: Some(alias.to_string()),
167                    },
168                    None => ImportItem::Module {
169                        alias: Some(default_name(&module).to_string()),
170                    },
171                };
172                Some(Import {
173                    module,
174                    level: 0,
175                    items: vec![item],
176                    line: line(s),
177                    file: None,
178                })
179            })
180            .collect();
181        Some(imports)
182    }
183
184    fn reference(&self, node: Node, src: &[u8]) -> Option<(RefKind, Vec<String>)> {
185        match node.kind() {
186            "call_expression" => Some((
187                RefKind::Call,
188                path(node.child_by_field_name("function")?, src)?,
189            )),
190            "composite_literal" => Some((
191                RefKind::New,
192                type_path(node.child_by_field_name("type")?, src)?,
193            )),
194            _ => None,
195        }
196    }
197
198    fn assignment(&self, node: Node, src: &[u8]) -> Option<(Target, TypeSource)> {
199        match node.kind() {
200            "short_var_declaration" => {
201                let left = named_children(node.child_by_field_name("left")?)
202                    .into_iter()
203                    .next()?;
204                if left.kind() != "identifier" {
205                    return None;
206                }
207                let right = named_children(node.child_by_field_name("right")?)
208                    .into_iter()
209                    .next()?;
210                Some((
211                    Target::Var(text(left, src).to_string()),
212                    value_type(right, src)?,
213                ))
214            }
215            "var_spec" => {
216                let name = field_text(node, "name", src)?.to_string();
217                if let Some(t) = node
218                    .child_by_field_name("type")
219                    .and_then(|t| type_path(t, src))
220                {
221                    return Some((Target::Var(name), TypeSource::Annotation(t)));
222                }
223                let v = named_children(node.child_by_field_name("value")?)
224                    .into_iter()
225                    .next()?;
226                Some((Target::Var(name), value_type(v, src)?))
227            }
228            "field_declaration" => Some((
229                Target::Attr(field_text(node, "name", src)?.to_string()),
230                TypeSource::Annotation(type_path(node.child_by_field_name("type")?, src)?),
231            )),
232            _ => None,
233        }
234    }
235
236    fn bases(&self, def: Node, src: &[u8]) -> Vec<(RefKind, Vec<String>)> {
237        let Some(ty) = (def.kind() == "type_spec")
238            .then(|| def.child_by_field_name("type"))
239            .flatten()
240        else {
241            return Vec::new();
242        };
243        let mut out = Vec::new();
244        let mut stack = vec![ty];
245        while let Some(n) = stack.pop() {
246            match n.kind() {
247                // An embedded field (type only, no name) promotes the embedded type's methods.
248                "field_declaration" if n.child_by_field_name("name").is_none() => {
249                    if let Some(p) = n
250                        .child_by_field_name("type")
251                        .and_then(|t| type_path(t, src))
252                    {
253                        out.push((RefKind::Inherits, p));
254                    }
255                }
256                "type_elem" => out.extend(
257                    named_children(n)
258                        .into_iter()
259                        .filter_map(|t| type_path(t, src))
260                        .map(|p| (RefKind::Inherits, p)),
261                ),
262                "struct_type" | "interface_type" | "field_declaration_list" => {
263                    stack.extend(named_children(n))
264                }
265                _ => {}
266            }
267        }
268        out
269    }
270
271    fn returns(&self, def: Node, src: &[u8]) -> Option<Vec<String>> {
272        let r = def.child_by_field_name("result")?;
273        if r.kind() == "parameter_list" {
274            let first = named_children(r)
275                .into_iter()
276                .find(|p| p.kind() == "parameter_declaration")?;
277            return type_path(first.child_by_field_name("type")?, src);
278        }
279        type_path(r, src)
280    }
281
282    fn params(&self, def: Node, src: &[u8]) -> Vec<(String, Vec<String>, bool)> {
283        let lists = ["receiver", "parameters"]
284            .into_iter()
285            .filter_map(|f| def.child_by_field_name(f));
286        let mut out = Vec::new();
287        for list in lists {
288            for p in named_children(list)
289                .into_iter()
290                .filter(|p| p.kind() == "parameter_declaration")
291            {
292                let Some(ty) = p
293                    .child_by_field_name("type")
294                    .and_then(|t| type_path(t, src))
295                else {
296                    continue;
297                };
298                let mut cur = p.walk();
299                for n in p.children_by_field_name("name", &mut cur) {
300                    out.push((text(n, src).to_string(), ty.clone(), false));
301                }
302            }
303        }
304        out
305    }
306}
307
308/// Package name an import binds by default: the last path segment, skipping `/vN`.
309fn default_name(module: &str) -> &str {
310    let mut segs = module.rsplit('/');
311    let last = segs.next().unwrap_or(module);
312    let is_version =
313        last.len() > 1 && last.starts_with('v') && last[1..].chars().all(|c| c.is_ascii_digit());
314    let name = if is_version {
315        segs.next().unwrap_or(last)
316    } else {
317        last
318    };
319    // gopkg.in/yaml.v3 → yaml
320    name.split('.').next().unwrap_or(name)
321}
322
323/// Type of an initializer: a call (its return type), a composite literal, or `&T{}`.
324fn value_type(v: Node, src: &[u8]) -> Option<TypeSource> {
325    match v.kind() {
326        "call_expression" => Some(TypeSource::Construct(path(
327            v.child_by_field_name("function")?,
328            src,
329        )?)),
330        "composite_literal" => Some(TypeSource::Construct(type_path(
331            v.child_by_field_name("type")?,
332            src,
333        )?)),
334        "unary_expression" => value_type(v.child_by_field_name("operand")?, src),
335        "identifier" => Some(TypeSource::Alias {
336            name: text(v, src).to_string(),
337            scope: String::new(),
338        }),
339        _ => None,
340    }
341}
342
343/// chi's `r.Route("/api", func(r chi.Router) { r.Get(...) })`: the prefixes of the enclosing
344/// `Route` closures whose parameter is this receiver, and the outermost receiver.
345fn route_scope(node: Node, mut receiver: Vec<String>, src: &[u8]) -> (String, Vec<String>) {
346    let mut prefix = String::new();
347    let mut cur = node.parent();
348    while let Some(n) = cur {
349        cur = n.parent();
350        if n.kind() != "func_literal" {
351            continue;
352        }
353        let param = n
354            .child_by_field_name("parameters")
355            .and_then(|ps| named_children(ps).into_iter().next())
356            .and_then(|p| p.child_by_field_name("name"))
357            .map(|i| text(i, src).to_string());
358        let call = n
359            .parent()
360            .filter(|a| a.kind() == "argument_list")
361            .and_then(|a| a.parent())
362            .filter(|c| c.kind() == "call_expression");
363        let (Some(param), Some(call)) = (param, call) else {
364            continue;
365        };
366        if receiver != [param] {
367            continue;
368        }
369        let Some(p) = call
370            .child_by_field_name("function")
371            .and_then(|f| path(f, src))
372        else {
373            continue;
374        };
375        let Some((m, outer)) = p.split_last() else {
376            continue;
377        };
378        let lit = call
379            .child_by_field_name("arguments")
380            .and_then(|a| named_children(a).into_iter().next())
381            .and_then(|a| value(a, src));
382        if let (true, Some(ValueExpr::Lit(l))) = (m == "Route", lit) {
383            prefix = format!("{l}{prefix}");
384            receiver = outer.to_vec();
385        }
386    }
387    (prefix, receiver)
388}
389
390fn value(node: Node, src: &[u8]) -> Option<ValueExpr> {
391    match node.kind() {
392        "interpreted_string_literal" | "raw_string_literal" => Some(ValueExpr::Lit(
393            text(node, src)
394                .trim_matches(|c| c == '"' || c == '`')
395                .to_string(),
396        )),
397        "identifier" | "selector_expression" => path(node, src).map(ValueExpr::Name),
398        "binary_expression" if field_text(node, "operator", src) == Some("+") => {
399            Some(ValueExpr::Join {
400                sep: String::new(),
401                parts: [
402                    node.child_by_field_name("left"),
403                    node.child_by_field_name("right"),
404                ]
405                .into_iter()
406                .map(|n| {
407                    n.and_then(|n| value(n, src))
408                        .unwrap_or(ValueExpr::Name(Vec::new()))
409                })
410                .collect(),
411            })
412        }
413        "call_expression" => {
414            let func = path(node.child_by_field_name("function")?, src)?;
415            let a = args(node.child_by_field_name("arguments")?, src);
416            Some(ValueExpr::Call {
417                func,
418                args: a
419                    .positional
420                    .into_iter()
421                    .map(|v| v.unwrap_or(ValueExpr::Name(Vec::new())))
422                    .collect(),
423                kwargs: Vec::new(),
424            })
425        }
426        "parenthesized_expression" => value(*named_children(node).first()?, src),
427        _ => None,
428    }
429}
430
431fn args(list: Node, src: &[u8]) -> Args {
432    let mut out = Args::default();
433    for a in named_children(list) {
434        if a.kind() == "comment" {
435            continue;
436        }
437        out.positional.push(value(a, src));
438    }
439    out
440}
441
442fn path(node: Node, src: &[u8]) -> Option<Vec<String>> {
443    match node.kind() {
444        "identifier" | "field_identifier" | "type_identifier" | "package_identifier" => {
445            Some(vec![text(node, src).to_string()])
446        }
447        "selector_expression" => {
448            let mut p = path(node.child_by_field_name("operand")?, src)?;
449            p.push(field_text(node, "field", src)?.to_string());
450            Some(p)
451        }
452        _ => None,
453    }
454}
455
456fn type_path(node: Node, src: &[u8]) -> Option<Vec<String>> {
457    match node.kind() {
458        "qualified_type" => Some(vec![
459            field_text(node, "package", src)?.to_string(),
460            field_text(node, "name", src)?.to_string(),
461        ]),
462        "pointer_type" | "parenthesized_type" => {
463            type_path(named_children(node).into_iter().next()?, src)
464        }
465        "generic_type" => type_path(node.child_by_field_name("type")?, src),
466        _ => path(node, src),
467    }
468}
469
470#[cfg(test)]
471mod tests {
472    use super::default_name;
473
474    #[test]
475    fn go_default_import_names() {
476        assert_eq!(default_name("github.com/acme/golib/store"), "store");
477        assert_eq!(default_name("github.com/jackc/pgx/v5"), "pgx");
478        assert_eq!(default_name("gopkg.in/yaml.v3"), "yaml");
479        assert_eq!(default_name("fmt"), "fmt");
480    }
481}