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