1use 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}