Skip to main content

sqry_lang_kotlin/
lib.rs

1//! Kotlin language plugin for sqry
2//!
3//! Implements the `LanguagePlugin` trait for Kotlin, providing:
4//! - AST parsing with tree-sitter
5//! - Scope extraction
6//! - Relation extraction (call graph, imports, exports, return types)
7//!
8//! # Supported Features
9//!
10//! - Classes (regular, data, enum, value, sealed)
11//! - Objects (singleton declarations, companion objects)
12//! - Interfaces (including functional interfaces)
13//! - Functions (regular, suspend, extension, inline)
14//! - Properties (val/var with getters/setters)
15//! - Generic type parameters
16//! - Visibility modifiers (public, private, protected, internal)
17//! - Inheritance modifiers (open, abstract, final, override, sealed)
18
19use sqry_core::ast::{Scope, ScopeId, link_nested_scopes};
20use sqry_core::plugin::{
21    LanguageMetadata, LanguagePlugin,
22    error::{ParseError, ScopeError},
23};
24use std::path::Path;
25use streaming_iterator::StreamingIterator;
26use tree_sitter::{Language, Parser, Query, QueryCursor, Tree};
27
28const PLUGIN_ID: &str = "kotlin";
29
30pub mod relations;
31
32/// Kotlin language plugin
33///
34/// Provides language support for Kotlin source files (.kt, .kts).
35///
36/// # Example
37///
38/// ```
39/// use sqry_lang_kotlin::KotlinPlugin;
40/// use sqry_core::plugin::LanguagePlugin;
41///
42/// let plugin = KotlinPlugin::default();
43/// let metadata = plugin.metadata();
44/// assert_eq!(metadata.id, "kotlin");
45/// assert_eq!(metadata.name, "Kotlin");
46/// ```
47pub struct KotlinPlugin {
48    graph_builder: relations::KotlinGraphBuilder,
49}
50
51impl KotlinPlugin {
52    #[must_use]
53    pub fn new() -> Self {
54        Self {
55            graph_builder: relations::KotlinGraphBuilder::new(),
56        }
57    }
58}
59
60impl Default for KotlinPlugin {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl LanguagePlugin for KotlinPlugin {
67    fn metadata(&self) -> LanguageMetadata {
68        LanguageMetadata {
69            id: PLUGIN_ID,
70            name: "Kotlin",
71            version: env!("CARGO_PKG_VERSION"),
72            author: "Verivus Pty Ltd.",
73            description: "Kotlin language support for sqry",
74            tree_sitter_version: "0.25",
75        }
76    }
77
78    fn extensions(&self) -> &'static [&'static str] {
79        &["kt", "kts"]
80    }
81
82    fn language(&self) -> Language {
83        tree_sitter_kotlin_sqry::language()
84    }
85
86    fn parse_ast(&self, content: &[u8]) -> Result<Tree, ParseError> {
87        let mut parser = Parser::new();
88        let language = self.language();
89
90        parser.set_language(&language).map_err(|e| {
91            ParseError::LanguageSetFailed(format!("Failed to set Kotlin language: {e}"))
92        })?;
93
94        parser
95            .parse(content, None)
96            .ok_or(ParseError::TreeSitterFailed)
97    }
98
99    fn extract_scopes(
100        &self,
101        tree: &Tree,
102        content: &[u8],
103        file_path: &Path,
104    ) -> Result<Vec<Scope>, ScopeError> {
105        Self::extract_kotlin_scopes(tree, content, file_path)
106    }
107
108    fn graph_builder(&self) -> Option<&dyn sqry_core::graph::GraphBuilder> {
109        Some(&self.graph_builder)
110    }
111}
112
113impl KotlinPlugin {
114    /// Extract scope information from Kotlin code
115    fn extract_kotlin_scopes(
116        tree: &Tree,
117        content: &[u8],
118        file_path: &Path,
119    ) -> Result<Vec<Scope>, ScopeError> {
120        let root_node = tree.root_node();
121        let language = tree_sitter_kotlin_sqry::language();
122
123        let scope_query = Self::scope_query_source();
124
125        let query = Query::new(&language, scope_query)
126            .map_err(|e| ScopeError::QueryCompilationFailed(e.to_string()))?;
127
128        let mut scopes = Vec::new();
129        let mut cursor = QueryCursor::new();
130        let mut query_matches = cursor.matches(&query, root_node, content);
131
132        while let Some(m) = query_matches.next() {
133            let mut scope_type = None;
134            let mut scope_name = None;
135            let mut scope_start = None;
136            let mut scope_end = None;
137
138            for capture in m.captures {
139                let capture_name = query.capture_names()[capture.index as usize];
140                let node = capture.node;
141
142                if let Some((prefix, suffix)) = capture_name.rsplit_once('.') {
143                    match suffix {
144                        "type" => {
145                            scope_type = Some(prefix.to_string());
146                            scope_start = Some(node.start_position());
147                            scope_end = Some(node.end_position());
148                        }
149                        "name" => {
150                            scope_name = node
151                                .utf8_text(content)
152                                .ok()
153                                .map(std::string::ToString::to_string);
154                        }
155                        _ => {}
156                    }
157                }
158            }
159
160            if let (Some(stype), Some(sname), Some(start), Some(end)) =
161                (scope_type, scope_name, scope_start, scope_end)
162            {
163                let scope = Scope {
164                    id: ScopeId::new(0),
165                    scope_type: stype,
166                    name: sname,
167                    file_path: file_path.to_path_buf(),
168                    start_line: start.row + 1,
169                    start_column: start.column,
170                    end_line: end.row + 1,
171                    end_column: end.column,
172                    parent_id: None,
173                };
174                scopes.push(scope);
175            }
176        }
177
178        scopes.sort_by_key(|s| (s.start_line, s.start_column));
179
180        link_nested_scopes(&mut scopes);
181
182        Ok(scopes)
183    }
184
185    /// Returns tree-sitter query source for scope extraction
186    fn scope_query_source() -> &'static str {
187        r"
188; Function scopes
189(function_declaration
190  (simple_identifier) @function.name
191) @function.type
192
193; Class scopes
194(class_declaration
195  (type_identifier) @class.name
196) @class.type
197
198; Object scopes
199(object_declaration
200  (type_identifier) @object.name
201) @object.type
202"
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use std::path::PathBuf;
210
211    #[test]
212    fn test_metadata() {
213        let plugin = KotlinPlugin::default();
214        let metadata = plugin.metadata();
215
216        assert_eq!(metadata.id, "kotlin");
217        assert_eq!(metadata.name, "Kotlin");
218        assert_eq!(metadata.author, "Verivus Pty Ltd.");
219    }
220
221    #[test]
222    fn test_extensions() {
223        let plugin = KotlinPlugin::default();
224        let extensions = plugin.extensions();
225
226        assert_eq!(extensions.len(), 2);
227        assert!(extensions.contains(&"kt"));
228        assert!(extensions.contains(&"kts"));
229    }
230
231    #[test]
232    fn test_language() {
233        let plugin = KotlinPlugin::default();
234        let language = plugin.language();
235
236        assert!(language.abi_version() > 0);
237    }
238
239    #[test]
240    fn test_parse_ast_simple() {
241        let plugin = KotlinPlugin::default();
242        let source = b"fun main() {}";
243
244        let tree = plugin.parse_ast(source).unwrap();
245        assert!(!tree.root_node().has_error());
246    }
247
248    #[test]
249    fn test_extract_scopes_simple() {
250        let plugin = KotlinPlugin::default();
251        let source = b"class User { fun getName() = \"Alice\" }\nfun topLevel() {}";
252        let file = PathBuf::from("test.kt");
253
254        let tree = plugin.parse_ast(source).unwrap();
255        let scopes = plugin.extract_scopes(&tree, source, &file).unwrap();
256
257        assert!(scopes.iter().any(|s| s.name == "User"));
258        assert!(scopes.iter().any(|s| s.name == "getName"));
259        assert!(scopes.iter().any(|s| s.name == "topLevel"));
260    }
261}