use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
use std::path::Path;
use tree_sitter::Node;
pub struct Kotlin;
impl Kotlin {
fn find_type_identifier(node: &Node, content: &str, out: &mut Vec<String>) {
let before = out.len();
if node.kind() == "type_identifier" {
out.push(content[node.byte_range()].to_string());
return;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
Self::find_type_identifier(&child, content, out);
if out.len() > before {
return;
}
}
}
}
impl Language for Kotlin {
fn name(&self) -> &'static str {
"Kotlin"
}
fn extensions(&self) -> &'static [&'static str] {
&["kt", "kts"]
}
fn grammar_name(&self) -> &'static str {
"kotlin"
}
fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
Some(self)
}
fn signature_suffix(&self) -> &'static str {
" {}"
}
fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
extract_kdoc(node, content)
}
fn refine_kind(
&self,
node: &Node,
_content: &str,
tag_kind: crate::SymbolKind,
) -> crate::SymbolKind {
if node.kind() == "class_declaration" {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"interface" => return crate::SymbolKind::Interface,
"enum" => return crate::SymbolKind::Enum,
"type_identifier" | "class_body" | "enum_class_body" => break,
_ => {}
}
}
}
tag_kind
}
fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
let mut implements = Vec::new();
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32)
&& child.kind() == "delegation_specifier"
{
Self::find_type_identifier(&child, content, &mut implements);
}
}
crate::ImplementsInfo {
is_interface: false,
implements,
}
}
fn build_signature(&self, node: &Node, content: &str) -> String {
let name = match self.node_name(node, content) {
Some(n) => n,
None => {
return content[node.byte_range()]
.lines()
.next()
.unwrap_or("")
.trim()
.to_string();
}
};
match node.kind() {
"function_declaration" | "function_definition" => {
let params = node
.child_by_field_name("value_parameters")
.or_else(|| node.child_by_field_name("parameters"))
.map(|p| content[p.byte_range()].to_string())
.unwrap_or_else(|| "()".to_string());
let return_type = node
.child_by_field_name("type")
.map(|t| format!(": {}", content[t.byte_range()].trim()))
.unwrap_or_default();
format!("fun {}{}{}", name, params, return_type)
}
"class_declaration" => format!("class {}", name),
"object_declaration" => format!("object {}", name),
"type_alias" => {
let target = node
.child_by_field_name("type")
.map(|t| content[t.byte_range()].to_string())
.unwrap_or_default();
format!("typealias {} = {}", name, target)
}
_ => {
let text = &content[node.byte_range()];
text.lines().next().unwrap_or(text).trim().to_string()
}
}
}
fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
if node.kind() != "import_header" {
return Vec::new();
}
let line = node.start_position().row + 1;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "identifier" || child.kind() == "user_type" {
let module = content[child.byte_range()].to_string();
let is_wildcard = content[node.byte_range()].contains(".*");
return vec![Import {
module,
names: Vec::new(),
alias: None,
is_wildcard,
is_relative: false,
line,
}];
}
}
Vec::new()
}
fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
if import.is_wildcard {
format!("import {}.*", import.module)
} else {
format!("import {}", import.module)
}
}
fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
let has_test_attr = symbol.attributes.iter().any(|a| a.contains("@Test"));
if has_test_attr {
return true;
}
match symbol.kind {
crate::SymbolKind::Class => {
symbol.name.starts_with("Test") || symbol.name.ends_with("Test")
}
_ => false,
}
}
fn test_file_globs(&self) -> &'static [&'static str] {
&[
"**/src/test/**/*.kt",
"**/Test*.kt",
"**/*Test.kt",
"**/*Tests.kt",
]
}
fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
node.child_by_field_name("class_body")
.or_else(|| node.child_by_field_name("body"))
}
fn analyze_container_body(
&self,
body_node: &Node,
content: &str,
inner_indent: &str,
) -> Option<ContainerBody> {
crate::body::analyze_brace_body(body_node, content, inner_indent)
}
fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
if let Some(name_node) = node.child_by_field_name("name") {
return Some(&content[name_node.byte_range()]);
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32)
&& (child.kind() == "type_identifier" || child.kind() == "simple_identifier")
{
return Some(&content[child.byte_range()]);
}
}
None
}
fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
extract_kotlin_annotations(node, content)
}
fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "modifiers" {
let mods = &content[child.byte_range()];
if mods.contains("private") {
return Visibility::Private;
}
if mods.contains("protected") {
return Visibility::Protected;
}
if mods.contains("internal") {
return Visibility::Protected;
} if mods.contains("public") {
return Visibility::Public;
}
}
if child.kind() == "visibility_modifier" {
let vis = &content[child.byte_range()];
if vis == "private" {
return Visibility::Private;
}
if vis == "protected" {
return Visibility::Protected;
}
if vis == "internal" {
return Visibility::Protected;
}
if vis == "public" {
return Visibility::Public;
}
}
}
Visibility::Public
}
fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
static RESOLVER: KotlinModuleResolver = KotlinModuleResolver;
Some(&RESOLVER)
}
}
impl LanguageSymbols for Kotlin {}
pub struct KotlinModuleResolver;
const KOTLIN_SRC_DIRS: &[&str] = &["src/main/kotlin", "src/test/kotlin", ""];
impl ModuleResolver for KotlinModuleResolver {
fn workspace_config(&self, root: &Path) -> ResolverConfig {
ResolverConfig {
workspace_root: root.to_path_buf(),
path_mappings: Vec::new(),
search_roots: KOTLIN_SRC_DIRS.iter().map(|d| root.join(d)).collect(),
}
}
fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
if ext != "kt" && ext != "kts" {
return Vec::new();
}
for search_root in &cfg.search_roots {
if let Ok(rel) = file.strip_prefix(search_root) {
let rel_str = rel
.to_str()
.unwrap_or("")
.trim_end_matches(".kts")
.trim_end_matches(".kt")
.replace(['/', '\\'], ".");
if !rel_str.is_empty() {
return vec![ModuleId {
canonical_path: rel_str,
}];
}
}
}
Vec::new()
}
fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
if ext != "kt" && ext != "kts" {
return Resolution::NotApplicable;
}
let raw = &spec.raw;
let path_part = raw.replace('.', "/");
let exported_name = raw.rsplit('.').next().unwrap_or(raw).to_string();
for search_root in &cfg.search_roots {
let candidate = search_root.join(format!("{}.kt", path_part));
if candidate.exists() {
return Resolution::Resolved(candidate, exported_name.clone());
}
let candidate = search_root.join(format!("{}.kts", path_part));
if candidate.exists() {
return Resolution::Resolved(candidate, exported_name.clone());
}
}
Resolution::NotFound
}
}
fn extract_kdoc(node: &Node, content: &str) -> Option<String> {
let mut prev = node.prev_sibling();
while let Some(sibling) = prev {
match sibling.kind() {
"multiline_comment" => {
let text = &content[sibling.byte_range()];
if text.starts_with("/**") {
let lines: Vec<&str> = text
.strip_prefix("/**")
.unwrap_or(text)
.strip_suffix("*/")
.unwrap_or(text)
.lines()
.map(|l| l.trim().strip_prefix("*").unwrap_or(l).trim())
.filter(|l| !l.is_empty())
.collect();
if !lines.is_empty() {
return Some(lines.join(" "));
}
}
return None;
}
"line_comment" => {
}
_ => return None,
}
prev = sibling.prev_sibling();
}
None
}
fn extract_kotlin_annotations(node: &Node, content: &str) -> Vec<String> {
let mut attrs = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "modifiers" {
let mut mod_cursor = child.walk();
for mod_child in child.children(&mut mod_cursor) {
if mod_child.kind() == "annotation" {
attrs.push(content[mod_child.byte_range()].to_string());
}
}
}
}
attrs
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validate_unused_kinds_audit;
#[test]
fn unused_node_kinds_audit() {
#[rustfmt::skip]
let documented_unused: &[&str] = &[
"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",
"variable_declaration",
"if_expression",
"anonymous_function",
"when_entry",
"conjunction_expression",
"disjunction_expression",
"while_statement",
"do_while_statement",
"enum_class_body",
"for_statement",
"import_header",
"elvis_expression",
"jump_expression",
"when_expression",
"try_expression",
"lambda_literal",
"catch_block",
];
validate_unused_kinds_audit(&Kotlin, documented_unused)
.expect("Kotlin unused node kinds audit failed");
}
}