Skip to main content

comindly_core/lang/
kotlin.rs

1//! Kotlin syntax adapter (tree-sitter-kotlin-ng). Shares the JVM package namespace with Java.
2
3use tree_sitter::Node;
4
5use super::{field_text, line, named_children, spring, text, LangSpec, RawContract, Target};
6use crate::facts::{Import, ImportItem, RefKind, TypeSource, ValueExpr};
7use crate::model::SymbolKind;
8
9pub struct Kotlin;
10
11impl LangSpec for Kotlin {
12    fn receiver(&self) -> &'static str {
13        "this"
14    }
15
16    fn contract(&self, node: Node, src: &[u8]) -> Option<RawContract> {
17        if let Some(r) = spring::route(node, src) {
18            return Some(r);
19        }
20        if node.kind() != "call_expression" {
21            return None;
22        }
23        let kids = named_children(node);
24        let callee = kids
25            .first()
26            .filter(|k| k.kind() == "navigation_expression")?;
27        let parts = named_children(*callee);
28        if parts.last().is_some_and(|l| text(*l, src) == "uri") {
29            let inner = parts.first().filter(|p| p.kind() == "call_expression")?;
30            let inner_nav = named_children(*inner)
31                .into_iter()
32                .find(|k| k.kind() == "navigation_expression")?;
33            let verb = spring::webclient_verb(text(*named_children(inner_nav).last()?, src))?;
34            let list = kids.iter().find(|k| k.kind() == "value_arguments")?;
35            return Some(RawContract::Op {
36                receiver: vec![spring::WEBCLIENT.into()],
37                method: verb,
38                args: spring::args(*list, src),
39            });
40        }
41        let mut receiver = path(*callee, src)?;
42        let method = receiver.pop()?;
43        if !spring::REST_TEMPLATE.iter().any(|(m, _)| *m == method) {
44            return None;
45        }
46        let list = kids.iter().find(|k| k.kind() == "value_arguments")?;
47        Some(RawContract::Op {
48            receiver,
49            method,
50            args: spring::args(*list, src),
51        })
52    }
53
54    fn value(&self, node: Node, src: &[u8]) -> Option<(String, ValueExpr)> {
55        if node.kind() != "property_declaration" {
56            return None;
57        }
58        let name = property_name(node, src)?;
59        let init = named_children(node).into_iter().last()?;
60        Some((name, spring::value(init, src)?))
61    }
62
63    fn package(&self, root: Node, src: &[u8]) -> Option<String> {
64        let header = named_children(root)
65            .into_iter()
66            .find(|n| n.kind() == "package_header")?;
67        named_children(header)
68            .into_iter()
69            .find(|n| n.kind() == "qualified_identifier")
70            .map(|n| text(n, src).to_string())
71    }
72
73    fn def(&self, node: Node, src: &[u8], in_class: bool) -> Option<(SymbolKind, String)> {
74        let kind = match node.kind() {
75            "class_declaration" if has_token(node, "interface") => SymbolKind::Interface,
76            "class_declaration" | "object_declaration" => SymbolKind::Class,
77            "function_declaration" if in_class => SymbolKind::Method,
78            "function_declaration" => SymbolKind::Function,
79            "enum_entry" => {
80                let name = named_children(node)
81                    .into_iter()
82                    .find(|n| n.kind() == "identifier")?;
83                return Some((SymbolKind::Constant, text(name, src).to_string()));
84            }
85            // `@A @B(x) public annotation class Name` (no body) misparses as an expression.
86            "infix_expression" => {
87                let ids: Vec<&str> = named_children(node)
88                    .into_iter()
89                    .filter(|n| n.kind() == "identifier")
90                    .map(|n| text(n, src))
91                    .collect();
92                let at = ids.iter().position(|t| *t == "class")?;
93                return Some((SymbolKind::Class, ids.get(at + 1)?.to_string()));
94            }
95            // The grammar wraps some valid classes in ERROR; recover `class Name …`.
96            "ERROR" if has_token(node, "class") || has_token(node, "object") => {
97                let name = named_children(node)
98                    .into_iter()
99                    .find(|n| n.kind() == "identifier")?;
100                return Some((SymbolKind::Class, text(name, src).to_string()));
101            }
102            _ => return None,
103        };
104        Some((kind, field_text(node, "name", src)?.to_string()))
105    }
106
107    fn variable(&self, node: Node, src: &[u8]) -> Option<String> {
108        (node.kind() == "property_declaration").then(|| property_name(node, src))?
109    }
110
111    fn import(&self, node: Node, src: &[u8]) -> Option<Vec<Import>> {
112        if node.kind() != "import" {
113            return None;
114        }
115        let kids = named_children(node);
116        let full = kids
117            .iter()
118            .find(|n| n.kind() == "qualified_identifier")
119            .map(|n| text(*n, src).to_string())?;
120        let alias = kids
121            .iter()
122            .rev()
123            .find(|n| n.kind() == "identifier")
124            .map(|n| text(*n, src).to_string());
125        let (module, items) = if has_token(node, "*") {
126            (full, vec![ImportItem::All])
127        } else {
128            let (m, name) = full.rsplit_once('.')?;
129            (
130                m.to_string(),
131                vec![ImportItem::Name {
132                    name: name.to_string(),
133                    alias,
134                }],
135            )
136        };
137        Some(vec![Import {
138            module,
139            level: 0,
140            items,
141            line: line(node),
142            file: None,
143        }])
144    }
145
146    fn reference(&self, node: Node, src: &[u8]) -> Option<(RefKind, Vec<String>)> {
147        if node.kind() != "call_expression" {
148            return None;
149        }
150        Some((
151            RefKind::Call,
152            path(named_children(node).into_iter().next()?, src)?,
153        ))
154    }
155
156    fn assignment(&self, node: Node, src: &[u8]) -> Option<(Target, TypeSource)> {
157        match node.kind() {
158            "property_declaration" => {
159                let name = property_name(node, src)?;
160                let in_class = node.parent().is_some_and(|p| p.kind() == "class_body");
161                let target = if in_class {
162                    Target::Attr(name)
163                } else {
164                    Target::Var(name)
165                };
166                let decl = named_children(node)
167                    .into_iter()
168                    .find(|n| n.kind() == "variable_declaration")?;
169                if let Some(t) = named_children(decl)
170                    .into_iter()
171                    .find_map(|t| type_path(t, src))
172                {
173                    return Some((target, TypeSource::Annotation(t)));
174                }
175                let value = named_children(node)
176                    .into_iter()
177                    .find(|n| n.kind() == "call_expression")?;
178                let callee = path(named_children(value).into_iter().next()?, src)?;
179                Some((target, TypeSource::Construct(callee)))
180            }
181            // `class S(private val repo: Repo)` declares a property.
182            "class_parameter" if has_token(node, "val") || has_token(node, "var") => {
183                let kids = named_children(node);
184                let name = kids.iter().find(|n| n.kind() == "identifier")?;
185                let ty = kids.iter().find_map(|t| type_path(*t, src))?;
186                Some((
187                    Target::Attr(text(*name, src).to_string()),
188                    TypeSource::Annotation(ty),
189                ))
190            }
191            _ => None,
192        }
193    }
194
195    fn bases(&self, def: Node, src: &[u8]) -> Vec<(RefKind, Vec<String>)> {
196        let Some(specs) = named_children(def)
197            .into_iter()
198            .find(|n| n.kind() == "delegation_specifiers")
199        else {
200            return Vec::new();
201        };
202        named_children(specs)
203            .into_iter()
204            .filter_map(|spec| {
205                let inner = named_children(spec).into_iter().next()?;
206                match inner.kind() {
207                    "constructor_invocation" => Some((
208                        RefKind::Inherits,
209                        type_path(named_children(inner).into_iter().next()?, src)?,
210                    )),
211                    _ => Some((RefKind::Implements, type_path(inner, src)?)),
212                }
213            })
214            .collect()
215    }
216
217    fn returns(&self, def: Node, src: &[u8]) -> Option<Vec<String>> {
218        let kids = named_children(def);
219        let params = kids
220            .iter()
221            .position(|n| n.kind() == "function_value_parameters")?;
222        kids[params + 1..]
223            .iter()
224            .take_while(|n| n.kind() != "function_body")
225            .find_map(|t| type_path(*t, src))
226    }
227
228    fn params(&self, def: Node, src: &[u8]) -> Vec<(String, Vec<String>, bool)> {
229        let Some(ps) = named_children(def)
230            .into_iter()
231            .find(|n| n.kind() == "function_value_parameters")
232        else {
233            return Vec::new();
234        };
235        named_children(ps)
236            .into_iter()
237            .filter(|p| p.kind() == "parameter")
238            .filter_map(|p| {
239                let kids = named_children(p);
240                let name = kids.iter().find(|n| n.kind() == "identifier")?;
241                let ty = kids.iter().find_map(|t| type_path(*t, src))?;
242                Some((text(*name, src).to_string(), ty, false))
243            })
244            .collect()
245    }
246}
247
248fn has_token(node: Node, kind: &str) -> bool {
249    (0..node.child_count())
250        .filter_map(|i| node.child(i))
251        .any(|c| c.kind() == kind)
252}
253
254fn property_name(node: Node, src: &[u8]) -> Option<String> {
255    let decl = named_children(node)
256        .into_iter()
257        .find(|n| n.kind() == "variable_declaration")?;
258    named_children(decl)
259        .into_iter()
260        .find(|n| n.kind() == "identifier")
261        .map(|n| text(n, src).to_string())
262}
263
264fn path(node: Node, src: &[u8]) -> Option<Vec<String>> {
265    match node.kind() {
266        "identifier" => Some(vec![text(node, src).to_string()]),
267        "this_expression" => Some(vec!["this".into()]),
268        "navigation_expression" => {
269            let kids = named_children(node);
270            let mut p = path(*kids.first()?, src)?;
271            let last = kids.last()?;
272            if last.kind() != "identifier" {
273                return None;
274            }
275            p.push(text(*last, src).to_string());
276            Some(p)
277        }
278        _ => None,
279    }
280}
281
282fn type_path(node: Node, src: &[u8]) -> Option<Vec<String>> {
283    match node.kind() {
284        "user_type" => {
285            let ids: Vec<String> = named_children(node)
286                .into_iter()
287                .filter(|n| n.kind() == "identifier")
288                .map(|n| text(n, src).to_string())
289                .collect();
290            (!ids.is_empty()).then_some(ids)
291        }
292        "nullable_type" | "type" | "parenthesized_type" => named_children(node)
293            .into_iter()
294            .find_map(|t| type_path(t, src)),
295        _ => None,
296    }
297}