Skip to main content

imp_core/tools/scan/
kotlin.rs

1//! Kotlin tree-sitter extraction — classes, objects, interfaces, enums, functions, and properties.
2
3use tree_sitter::{Node, Parser};
4
5use super::types::*;
6
7pub fn parse(source: &str, file: &str, result: &mut ScanResult) {
8    let mut parser = Parser::new();
9    if parser
10        .set_language(&tree_sitter_kotlin_ng::LANGUAGE.into())
11        .is_err()
12    {
13        return;
14    }
15    let tree = match parser.parse(source, None) {
16        Some(t) => t,
17        None => return,
18    };
19    extract_kotlin(&tree.root_node(), source, file, result);
20}
21
22fn extract_kotlin(root: &Node, source: &str, file: &str, result: &mut ScanResult) {
23    let mut cursor = root.walk();
24    for child in root.named_children(&mut cursor) {
25        walk_declaration(&child, source, file, None, result);
26    }
27}
28
29fn walk_declaration(
30    node: &Node,
31    source: &str,
32    file: &str,
33    owner: Option<&str>,
34    result: &mut ScanResult,
35) {
36    match node.kind() {
37        "class_declaration" | "object_declaration" => {
38            extract_type(node, source, file, owner, result);
39        }
40        "function_declaration" => extract_function(node, source, file, owner, result),
41        "property_declaration" => extract_property(node, source, file, owner, result),
42        _ => {
43            let mut cursor = node.walk();
44            for child in node.named_children(&mut cursor) {
45                walk_declaration(&child, source, file, owner, result);
46            }
47        }
48    }
49}
50
51fn extract_type(
52    node: &Node,
53    source: &str,
54    file: &str,
55    owner: Option<&str>,
56    result: &mut ScanResult,
57) {
58    let Some(name_node) = find_child_kind(node, "type_identifier", source) else {
59        return;
60    };
61    let name = node_text(&name_node, source).to_string();
62    let qualified = qualify(owner, &name);
63    let kind = type_kind(node, source);
64    let visibility = visibility(node, source);
65    let fields = extract_constructor_fields(node, source);
66    let implements = extract_delegation_specifiers(node, source);
67
68    result.types.insert(
69        qualified.clone(),
70        TypeInfo {
71            name: qualified.clone(),
72            source: source_loc(file, node),
73            kind,
74            fields,
75            visibility,
76            implements,
77            ..Default::default()
78        },
79    );
80
81    let mut cursor = node.walk();
82    for child in node.named_children(&mut cursor) {
83        walk_declaration(&child, source, file, Some(&qualified), result);
84    }
85}
86
87fn extract_function(
88    node: &Node,
89    source: &str,
90    file: &str,
91    owner: Option<&str>,
92    result: &mut ScanResult,
93) {
94    let Some(name_node) = find_child_kind(node, "simple_identifier", source) else {
95        return;
96    };
97    let name = node_text(&name_node, source).to_string();
98    let qualified = qualify(owner, &name);
99    let signature = first_line(node_text(node, source));
100    let is_test = has_test_annotation(node, source) || name.starts_with("test");
101
102    result.functions.insert(
103        qualified,
104        FunctionInfo {
105            name: name.clone(),
106            source: source_loc(file, node),
107            signature,
108            visibility: visibility(node, source),
109            is_async: node_text(node, source).contains("suspend"),
110            is_test,
111        },
112    );
113
114    if let Some(owner) = owner {
115        if let Some(typedef) = result.types.get_mut(owner) {
116            if !typedef.methods.contains(&name) {
117                typedef.methods.push(name);
118            }
119        }
120    }
121}
122
123fn extract_property(
124    node: &Node,
125    source: &str,
126    file: &str,
127    owner: Option<&str>,
128    result: &mut ScanResult,
129) {
130    let Some(name_node) = find_child_kind(node, "variable_identifier", source) else {
131        return;
132    };
133    let name = node_text(&name_node, source).to_string();
134
135    if let Some(owner) = owner {
136        if let Some(typedef) = result.types.get_mut(owner) {
137            if !typedef.fields.iter().any(|field| field.name == name) {
138                typedef.fields.push(Field {
139                    name,
140                    type_name: find_type_text(node, source).unwrap_or_default(),
141                    optional: node_text(node, source).contains('?'),
142                });
143            }
144        }
145    } else {
146        let qualified = qualify(None, &name);
147        result.functions.insert(
148            qualified,
149            FunctionInfo {
150                name,
151                source: source_loc(file, node),
152                signature: first_line(node_text(node, source)),
153                visibility: visibility(node, source),
154                is_async: false,
155                is_test: false,
156            },
157        );
158    }
159}
160
161fn extract_constructor_fields(node: &Node, source: &str) -> Vec<Field> {
162    let mut fields = Vec::new();
163    collect_constructor_fields(node, source, &mut fields);
164    fields
165}
166
167fn collect_constructor_fields(node: &Node, source: &str, fields: &mut Vec<Field>) {
168    if matches!(node.kind(), "class_parameter" | "parameter") {
169        let text = node_text(node, source);
170        if text.contains("val ") || text.contains("var ") {
171            if let Some(name_node) = find_child_kind(node, "simple_identifier", source) {
172                fields.push(Field {
173                    name: node_text(&name_node, source).to_string(),
174                    type_name: find_type_text(node, source).unwrap_or_default(),
175                    optional: text.contains('?'),
176                });
177            }
178        }
179    }
180
181    let mut cursor = node.walk();
182    for child in node.named_children(&mut cursor) {
183        collect_constructor_fields(&child, source, fields);
184    }
185}
186
187fn extract_delegation_specifiers(node: &Node, source: &str) -> Vec<String> {
188    let mut implements = Vec::new();
189    collect_delegation_specifiers(node, source, &mut implements);
190    implements
191}
192
193fn collect_delegation_specifiers(node: &Node, source: &str, implements: &mut Vec<String>) {
194    if node.kind() == "delegation_specifier" {
195        let text = node_text(node, source).trim();
196        if !text.is_empty() {
197            implements.push(text.to_string());
198        }
199        return;
200    }
201
202    let mut cursor = node.walk();
203    for child in node.named_children(&mut cursor) {
204        collect_delegation_specifiers(&child, source, implements);
205    }
206}
207
208fn type_kind(node: &Node, source: &str) -> TypeKind {
209    let text = node_text(node, source);
210    if text.trim_start().starts_with("interface ") || text.contains(" interface ") {
211        TypeKind::Interface
212    } else if text.trim_start().starts_with("enum class ") || text.contains(" enum class ") {
213        TypeKind::Enum
214    } else {
215        TypeKind::Class
216    }
217}
218
219fn visibility(node: &Node, source: &str) -> Visibility {
220    let text = node_text(node, source);
221    if text.contains("private ") {
222        Visibility::Private
223    } else if text.contains("internal ") {
224        Visibility::Internal
225    } else {
226        Visibility::Public
227    }
228}
229
230fn find_type_text(node: &Node, source: &str) -> Option<String> {
231    find_child_kind(node, "type", source).map(|node| node_text(&node, source).trim().to_string())
232}
233
234fn find_child_kind<'a>(node: &Node<'a>, kind: &str, source: &str) -> Option<Node<'a>> {
235    if node.kind() == kind && !node_text(node, source).trim().is_empty() {
236        return Some(*node);
237    }
238    let mut cursor = node.walk();
239    for child in node.named_children(&mut cursor) {
240        if let Some(found) = find_child_kind(&child, kind, source) {
241            return Some(found);
242        }
243    }
244    None
245}
246
247fn has_test_annotation(node: &Node, source: &str) -> bool {
248    let text = node_text(node, source);
249    text.contains("@Test") || text.contains("@ParameterizedTest")
250}
251
252fn qualify(owner: Option<&str>, name: &str) -> String {
253    owner
254        .map(|owner| format!("{owner}::{name}"))
255        .unwrap_or_else(|| name.to_string())
256}
257
258fn first_line(text: &str) -> String {
259    text.lines().next().unwrap_or_default().trim().to_string()
260}
261
262fn node_text<'a>(node: &Node, source: &'a str) -> &'a str {
263    node.utf8_text(source.as_bytes()).unwrap_or("")
264}
265
266fn source_loc(file: &str, node: &Node) -> String {
267    format!("{}:{}", file, node.start_position().row + 1)
268}