1use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
5use std::path::Path;
6use tree_sitter::Node;
7
8pub struct Kotlin;
10
11impl Kotlin {
12 fn find_type_identifier(node: &Node, content: &str, out: &mut Vec<String>) {
14 let before = out.len();
15 if node.kind() == "type_identifier" {
16 out.push(content[node.byte_range()].to_string());
17 return;
18 }
19 let mut cursor = node.walk();
20 for child in node.children(&mut cursor) {
21 Self::find_type_identifier(&child, content, out);
22 if out.len() > before {
23 return;
24 }
25 }
26 }
27}
28
29impl Language for Kotlin {
30 fn name(&self) -> &'static str {
31 "Kotlin"
32 }
33 fn extensions(&self) -> &'static [&'static str] {
34 &["kt", "kts"]
35 }
36 fn grammar_name(&self) -> &'static str {
37 "kotlin"
38 }
39
40 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
41 Some(self)
42 }
43
44 fn signature_suffix(&self) -> &'static str {
45 " {}"
46 }
47
48 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
49 extract_kdoc(node, content)
50 }
51
52 fn refine_kind(
53 &self,
54 node: &Node,
55 _content: &str,
56 tag_kind: crate::SymbolKind,
57 ) -> crate::SymbolKind {
58 if node.kind() == "class_declaration" {
59 let mut cursor = node.walk();
62 for child in node.children(&mut cursor) {
63 match child.kind() {
64 "interface" => return crate::SymbolKind::Interface,
65 "enum" => return crate::SymbolKind::Enum,
66 "type_identifier" | "class_body" | "enum_class_body" => break,
68 _ => {}
69 }
70 }
71 }
72 tag_kind
73 }
74
75 fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
76 let mut implements = Vec::new();
77 for i in 0..node.child_count() {
78 if let Some(child) = node.child(i as u32)
79 && child.kind() == "delegation_specifier"
80 {
81 Self::find_type_identifier(&child, content, &mut implements);
82 }
83 }
84 crate::ImplementsInfo {
85 is_interface: false,
86 implements,
87 }
88 }
89
90 fn build_signature(&self, node: &Node, content: &str) -> String {
91 let name = match self.node_name(node, content) {
92 Some(n) => n,
93 None => {
94 return content[node.byte_range()]
95 .lines()
96 .next()
97 .unwrap_or("")
98 .trim()
99 .to_string();
100 }
101 };
102 match node.kind() {
103 "function_declaration" | "function_definition" => {
104 let params = node
105 .child_by_field_name("value_parameters")
106 .or_else(|| node.child_by_field_name("parameters"))
107 .map(|p| content[p.byte_range()].to_string())
108 .unwrap_or_else(|| "()".to_string());
109 let return_type = node
110 .child_by_field_name("type")
111 .map(|t| format!(": {}", content[t.byte_range()].trim()))
112 .unwrap_or_default();
113 format!("fun {}{}{}", name, params, return_type)
114 }
115 "class_declaration" => format!("class {}", name),
116 "object_declaration" => format!("object {}", name),
117 "type_alias" => {
118 let target = node
119 .child_by_field_name("type")
120 .map(|t| content[t.byte_range()].to_string())
121 .unwrap_or_default();
122 format!("typealias {} = {}", name, target)
123 }
124 _ => {
125 let text = &content[node.byte_range()];
126 text.lines().next().unwrap_or(text).trim().to_string()
127 }
128 }
129 }
130
131 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
132 if node.kind() != "import_header" {
133 return Vec::new();
134 }
135
136 let line = node.start_position().row + 1;
137
138 let mut cursor = node.walk();
140 for child in node.children(&mut cursor) {
141 if child.kind() == "identifier" || child.kind() == "user_type" {
142 let module = content[child.byte_range()].to_string();
143 let is_wildcard = content[node.byte_range()].contains(".*");
144 return vec![Import {
145 module,
146 names: Vec::new(),
147 alias: None,
148 is_wildcard,
149 is_relative: false,
150 line,
151 }];
152 }
153 }
154
155 Vec::new()
156 }
157
158 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
159 if import.is_wildcard {
161 format!("import {}.*", import.module)
162 } else {
163 format!("import {}", import.module)
164 }
165 }
166
167 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
168 let has_test_attr = symbol.attributes.iter().any(|a| a.contains("@Test"));
169 if has_test_attr {
170 return true;
171 }
172 match symbol.kind {
173 crate::SymbolKind::Class => {
174 symbol.name.starts_with("Test") || symbol.name.ends_with("Test")
175 }
176 _ => false,
177 }
178 }
179
180 fn test_file_globs(&self) -> &'static [&'static str] {
181 &[
182 "**/src/test/**/*.kt",
183 "**/Test*.kt",
184 "**/*Test.kt",
185 "**/*Tests.kt",
186 ]
187 }
188
189 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
190 node.child_by_field_name("class_body")
191 .or_else(|| node.child_by_field_name("body"))
192 }
193
194 fn analyze_container_body(
195 &self,
196 body_node: &Node,
197 content: &str,
198 inner_indent: &str,
199 ) -> Option<ContainerBody> {
200 crate::body::analyze_brace_body(body_node, content, inner_indent)
201 }
202
203 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
204 if let Some(name_node) = node.child_by_field_name("name") {
206 return Some(&content[name_node.byte_range()]);
207 }
208 for i in 0..node.child_count() {
210 if let Some(child) = node.child(i as u32)
211 && (child.kind() == "type_identifier" || child.kind() == "simple_identifier")
212 {
213 return Some(&content[child.byte_range()]);
214 }
215 }
216 None
217 }
218
219 fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
220 extract_kotlin_annotations(node, content)
221 }
222
223 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
224 let mut cursor = node.walk();
225 for child in node.children(&mut cursor) {
226 if child.kind() == "modifiers" {
227 let mods = &content[child.byte_range()];
228 if mods.contains("private") {
229 return Visibility::Private;
230 }
231 if mods.contains("protected") {
232 return Visibility::Protected;
233 }
234 if mods.contains("internal") {
235 return Visibility::Protected;
236 } if mods.contains("public") {
238 return Visibility::Public;
239 }
240 }
241 if child.kind() == "visibility_modifier" {
243 let vis = &content[child.byte_range()];
244 if vis == "private" {
245 return Visibility::Private;
246 }
247 if vis == "protected" {
248 return Visibility::Protected;
249 }
250 if vis == "internal" {
251 return Visibility::Protected;
252 }
253 if vis == "public" {
254 return Visibility::Public;
255 }
256 }
257 }
258 Visibility::Public
260 }
261
262 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
263 static RESOLVER: KotlinModuleResolver = KotlinModuleResolver;
264 Some(&RESOLVER)
265 }
266}
267
268impl LanguageSymbols for Kotlin {}
269
270pub struct KotlinModuleResolver;
279
280const KOTLIN_SRC_DIRS: &[&str] = &["src/main/kotlin", "src/test/kotlin", ""];
281
282impl ModuleResolver for KotlinModuleResolver {
283 fn workspace_config(&self, root: &Path) -> ResolverConfig {
284 ResolverConfig {
285 workspace_root: root.to_path_buf(),
286 path_mappings: Vec::new(),
287 search_roots: KOTLIN_SRC_DIRS.iter().map(|d| root.join(d)).collect(),
288 }
289 }
290
291 fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
292 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
293 if ext != "kt" && ext != "kts" {
294 return Vec::new();
295 }
296 for search_root in &cfg.search_roots {
297 if let Ok(rel) = file.strip_prefix(search_root) {
298 let rel_str = rel
299 .to_str()
300 .unwrap_or("")
301 .trim_end_matches(".kts")
302 .trim_end_matches(".kt")
303 .replace(['/', '\\'], ".");
304 if !rel_str.is_empty() {
305 return vec![ModuleId {
306 canonical_path: rel_str,
307 }];
308 }
309 }
310 }
311 Vec::new()
312 }
313
314 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
315 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
316 if ext != "kt" && ext != "kts" {
317 return Resolution::NotApplicable;
318 }
319 let raw = &spec.raw;
320 let path_part = raw.replace('.', "/");
321 let exported_name = raw.rsplit('.').next().unwrap_or(raw).to_string();
322 for search_root in &cfg.search_roots {
323 let candidate = search_root.join(format!("{}.kt", path_part));
324 if candidate.exists() {
325 return Resolution::Resolved(candidate, exported_name.clone());
326 }
327 let candidate = search_root.join(format!("{}.kts", path_part));
328 if candidate.exists() {
329 return Resolution::Resolved(candidate, exported_name.clone());
330 }
331 }
332 Resolution::NotFound
333 }
334}
335
336fn extract_kdoc(node: &Node, content: &str) -> Option<String> {
340 let mut prev = node.prev_sibling();
341 while let Some(sibling) = prev {
342 match sibling.kind() {
343 "multiline_comment" => {
344 let text = &content[sibling.byte_range()];
345 if text.starts_with("/**") {
346 let lines: Vec<&str> = text
348 .strip_prefix("/**")
349 .unwrap_or(text)
350 .strip_suffix("*/")
351 .unwrap_or(text)
352 .lines()
353 .map(|l| l.trim().strip_prefix("*").unwrap_or(l).trim())
354 .filter(|l| !l.is_empty())
355 .collect();
356 if !lines.is_empty() {
357 return Some(lines.join(" "));
358 }
359 }
360 return None;
361 }
362 "line_comment" => {
363 }
365 _ => return None,
366 }
367 prev = sibling.prev_sibling();
368 }
369 None
370}
371
372fn extract_kotlin_annotations(node: &Node, content: &str) -> Vec<String> {
375 let mut attrs = Vec::new();
376 let mut cursor = node.walk();
377 for child in node.children(&mut cursor) {
378 if child.kind() == "modifiers" {
379 let mut mod_cursor = child.walk();
380 for mod_child in child.children(&mut mod_cursor) {
381 if mod_child.kind() == "annotation" {
382 attrs.push(content[mod_child.byte_range()].to_string());
383 }
384 }
385 }
386 }
387 attrs
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use crate::validate_unused_kinds_audit;
394
395 #[test]
398 fn unused_node_kinds_audit() {
399 #[rustfmt::skip]
400 let documented_unused: &[&str] = &[
401 "annotated_lambda", "class_body", "class_modifier", "class_parameter", "constructor_delegation_call", "control_structure_body", "delegation_specifier", "function_body", "function_modifier", "function_type_parameters","function_value_parameters", "identifier", "import_alias", "import_list", "inheritance_modifier", "interpolated_expression", "interpolated_identifier", "lambda_parameters", "member_modifier", "modifiers", "multi_variable_declaration", "parameter_modifier", "parameter_modifiers", "parameter_with_optional_type", "platform_modifier", "primary_constructor", "property_modifier", "reification_modifier", "secondary_constructor", "statements", "visibility_modifier", "additive_expression", "as_expression", "check_expression", "comparison_expression", "directly_assignable_expression", "equality_expression", "indexing_expression", "infix_expression", "multiplicative_expression", "parenthesized_expression","postfix_expression", "prefix_expression", "range_expression", "spread_expression", "super_expression", "this_expression", "wildcard_import", "function_type", "not_nullable_type", "nullable_type", "parenthesized_type", "parenthesized_user_type", "receiver_type", "type_arguments", "type_constraint", "type_constraints", "type_modifiers", "type_parameter", "type_parameter_modifiers","type_parameters", "type_projection", "type_projection_modifiers", "type_test", "variance_modifier", "finally_block", "property_declaration",
481 "variable_declaration",
482 "if_expression",
484 "anonymous_function",
485 "when_entry",
486 "conjunction_expression",
487 "disjunction_expression",
488 "while_statement",
489 "do_while_statement",
490 "enum_class_body",
491 "for_statement",
492 "import_header",
493 "elvis_expression",
494 "jump_expression",
495 "when_expression",
496 "try_expression",
497 "lambda_literal",
498 "catch_block",
499 ];
500
501 validate_unused_kinds_audit(&Kotlin, documented_unused)
502 .expect("Kotlin unused node kinds audit failed");
503 }
504}