1use sqry_core::ast::{Scope, ScopeId, link_nested_scopes};
12use sqry_core::plugin::{
13 LanguageMetadata, LanguagePlugin,
14 error::{ParseError, ScopeError},
15};
16use std::path::Path;
17use streaming_iterator::StreamingIterator;
18use tree_sitter::{Language, Parser, Query, QueryCursor, Tree};
19
20pub mod relations;
21
22pub struct JavaPlugin {
47 graph_builder: relations::JavaGraphBuilder,
48}
49
50impl JavaPlugin {
51 #[must_use]
52 pub fn new() -> Self {
53 Self {
54 graph_builder: relations::JavaGraphBuilder::default(),
55 }
56 }
57}
58
59impl Default for JavaPlugin {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl LanguagePlugin for JavaPlugin {
66 fn metadata(&self) -> LanguageMetadata {
67 LanguageMetadata {
68 id: "java",
69 name: "Java",
70 version: env!("CARGO_PKG_VERSION"),
71 author: "Verivus Pty Ltd",
72 description: "Java language support for sqry - enterprise code search",
73 tree_sitter_version: "0.23",
74 }
75 }
76
77 fn extensions(&self) -> &'static [&'static str] {
78 &["java"]
79 }
80
81 fn language(&self) -> Language {
82 tree_sitter_java::LANGUAGE.into()
83 }
84
85 fn parse_ast(&self, content: &[u8]) -> Result<Tree, ParseError> {
86 let mut parser = Parser::new();
87 let language = self.language();
88
89 parser.set_language(&language).map_err(|e| {
90 ParseError::LanguageSetFailed(format!("Failed to set Java language: {e}"))
91 })?;
92
93 parser
94 .parse(content, None)
95 .ok_or(ParseError::TreeSitterFailed)
96 }
97
98 fn extract_scopes(
99 &self,
100 tree: &Tree,
101 content: &[u8],
102 file_path: &Path,
103 ) -> Result<Vec<Scope>, ScopeError> {
104 Self::extract_java_scopes(tree, content, file_path)
105 }
106 fn graph_builder(&self) -> Option<&dyn sqry_core::graph::GraphBuilder> {
107 Some(&self.graph_builder)
108 }
109}
110
111impl JavaPlugin {
112 fn scope_query_source() -> &'static str {
114 r"
115; Class declarations with body
116(class_declaration
117 name: (identifier) @class.name
118 body: (class_body)) @class.type
119
120; Interface declarations with body
121(interface_declaration
122 name: (identifier) @interface.name
123 body: (interface_body)) @interface.type
124
125; Enum declarations with body
126(enum_declaration
127 name: (identifier) @enum.name
128 body: (enum_body)) @enum.type
129
130; Method declarations (both concrete and abstract)
131(method_declaration
132 name: (identifier) @method.name) @method.type
133
134; Constructor declarations with body
135(constructor_declaration
136 name: (identifier) @constructor.name
137 body: (constructor_body)) @constructor.type
138
139; Record declarations (Java 14+)
140(record_declaration
141 name: (identifier) @record.name
142 body: (class_body)) @record.type
143
144; Compact constructor declarations (used in records)
145(compact_constructor_declaration
146 name: (identifier) @constructor.name
147 body: (block)) @constructor.type
148"
149 }
150
151 fn extract_java_scopes(
153 tree: &Tree,
154 content: &[u8],
155 file_path: &Path,
156 ) -> Result<Vec<Scope>, ScopeError> {
157 let language = tree_sitter_java::LANGUAGE.into();
158 let scope_query = Self::scope_query_source();
159
160 let query = Query::new(&language, scope_query).map_err(|e| {
161 ScopeError::QueryCompilationFailed(format!("Failed to compile Java scope query: {e}"))
162 })?;
163
164 let mut cursor = QueryCursor::new();
165 let mut matches = cursor.matches(&query, tree.root_node(), content);
166 let mut scopes = Vec::new();
167
168 while let Some(m) = matches.next() {
169 let mut scope_type = None;
171 let mut scope_name = None;
172 let mut scope_node = None;
173
174 for capture in m.captures {
175 let capture_name = query.capture_names()[capture.index as usize];
176 let capture_extension = std::path::Path::new(capture_name)
177 .extension()
178 .and_then(|ext| ext.to_str());
179 if capture_extension.is_some_and(|ext| ext.eq_ignore_ascii_case("type")) {
180 scope_type = Some(capture_name.trim_end_matches(".type"));
181 scope_node = Some(capture.node);
182 } else if capture_extension.is_some_and(|ext| ext.eq_ignore_ascii_case("name")) {
183 scope_name = capture.node.utf8_text(content).ok();
184 }
185 }
186
187 if let (Some(stype), Some(sname), Some(node)) = (scope_type, scope_name, scope_node) {
188 let start_pos = node.start_position();
189 let end_pos = node.end_position();
190
191 scopes.push(Scope {
192 id: ScopeId::new(0), name: sname.to_string(),
194 scope_type: stype.to_string(),
195 file_path: file_path.to_path_buf(),
196 start_line: start_pos.row + 1,
197 start_column: start_pos.column,
198 end_line: end_pos.row + 1,
199 end_column: end_pos.column,
200 parent_id: None,
201 });
202 }
203 }
204
205 scopes.sort_by(|a, b| {
207 a.start_line
208 .cmp(&b.start_line)
209 .then(a.start_column.cmp(&b.start_column))
210 });
211
212 link_nested_scopes(&mut scopes);
213
214 Ok(scopes)
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_metadata() {
224 let plugin = JavaPlugin::default();
225 let metadata = plugin.metadata();
226
227 assert_eq!(metadata.id, "java");
228 assert_eq!(metadata.name, "Java");
229 assert_eq!(metadata.version, env!("CARGO_PKG_VERSION"));
230 assert_eq!(metadata.author, "Verivus Pty Ltd");
231 assert_eq!(metadata.tree_sitter_version, "0.23");
232 }
233
234 #[test]
235 fn test_extensions() {
236 let plugin = JavaPlugin::default();
237 let extensions = plugin.extensions();
238
239 assert_eq!(extensions.len(), 1);
240 assert_eq!(extensions[0], "java");
241 }
242
243 #[test]
244 fn test_graph_builder_returns_some() {
245 let plugin = JavaPlugin::default();
246 assert!(
247 plugin.graph_builder().is_some(),
248 "JavaPlugin::graph_builder() should return Some"
249 );
250 }
251
252 #[test]
253 fn test_language() {
254 let plugin = JavaPlugin::default();
255 let language = plugin.language();
256
257 assert!(language.abi_version() > 0);
259 }
260
261 #[test]
262 fn test_parse_ast_simple() {
263 let plugin = JavaPlugin::default();
264 let source = b"class HelloWorld {}";
265
266 let tree = plugin.parse_ast(source).unwrap();
267 assert!(!tree.root_node().has_error());
268 }
269
270 #[test]
271 fn test_plugin_is_send_sync() {
272 fn assert_send_sync<T: Send + Sync>() {}
273 assert_send_sync::<JavaPlugin>();
274 }
275
276 #[test]
277 fn test_extract_scopes_class() {
278 let plugin = JavaPlugin::default();
279 let source = b"public class MyClass {
280 public void myMethod() {
281 System.out.println(\"Hello\");
282 }
283}";
284 let tree = plugin.parse_ast(source).unwrap();
285 let scopes = plugin
286 .extract_scopes(&tree, source, Path::new("Test.java"))
287 .unwrap();
288
289 assert_eq!(scopes.len(), 2, "Expected 2 scopes, got {scopes:?}");
291
292 let class_scope = scopes.iter().find(|s| s.scope_type == "class");
294 assert!(class_scope.is_some(), "Should have class scope");
295 assert_eq!(class_scope.unwrap().name, "MyClass");
296
297 let method_scope = scopes.iter().find(|s| s.scope_type == "method");
299 assert!(method_scope.is_some(), "Should have method scope");
300 assert_eq!(method_scope.unwrap().name, "myMethod");
301 }
302
303 #[test]
304 fn test_extract_scopes_interface() {
305 let plugin = JavaPlugin::default();
306 let source = b"public interface MyInterface {
307 void doSomething();
308}";
309 let tree = plugin.parse_ast(source).unwrap();
310 let scopes = plugin
311 .extract_scopes(&tree, source, Path::new("Test.java"))
312 .unwrap();
313
314 assert!(!scopes.is_empty(), "Expected at least 1 scope");
316
317 let interface_scope = scopes.iter().find(|s| s.scope_type == "interface");
318 assert!(interface_scope.is_some(), "Should have interface scope");
319 assert_eq!(interface_scope.unwrap().name, "MyInterface");
320 }
321
322 #[test]
323 fn test_extract_scopes_enum() {
324 let plugin = JavaPlugin::default();
325 let source = b"public enum Color {
326 RED, GREEN, BLUE
327}";
328 let tree = plugin.parse_ast(source).unwrap();
329 let scopes = plugin
330 .extract_scopes(&tree, source, Path::new("Test.java"))
331 .unwrap();
332
333 assert!(!scopes.is_empty(), "Expected at least 1 scope");
334
335 let enum_scope = scopes.iter().find(|s| s.scope_type == "enum");
336 assert!(enum_scope.is_some(), "Should have enum scope");
337 assert_eq!(enum_scope.unwrap().name, "Color");
338 }
339
340 #[test]
341 fn test_extract_scopes_nested() {
342 let plugin = JavaPlugin::default();
343 let source = b"public class Outer {
344 public class Inner {
345 public void innerMethod() {}
346 }
347}";
348 let tree = plugin.parse_ast(source).unwrap();
349 let scopes = plugin
350 .extract_scopes(&tree, source, Path::new("Test.java"))
351 .unwrap();
352
353 assert_eq!(scopes.len(), 3, "Expected 3 scopes, got {scopes:?}");
355
356 let outer = scopes.iter().find(|s| s.name == "Outer");
358 assert!(outer.is_some());
359 assert!(outer.unwrap().parent_id.is_none());
360
361 let inner = scopes.iter().find(|s| s.name == "Inner");
363 assert!(inner.is_some());
364 assert!(inner.unwrap().parent_id.is_some());
365
366 let method = scopes.iter().find(|s| s.name == "innerMethod");
368 assert!(method.is_some());
369 assert!(method.unwrap().parent_id.is_some());
370 }
371
372 #[test]
373 fn test_extract_scopes_abstract_methods() {
374 let plugin = JavaPlugin::default();
375 let source = b"public abstract class Shape {
376 public abstract void draw();
377 public abstract double area();
378}";
379 let tree = plugin.parse_ast(source).unwrap();
380 let scopes = plugin
381 .extract_scopes(&tree, source, Path::new("Shape.java"))
382 .unwrap();
383
384 assert_eq!(scopes.len(), 3, "Expected 3 scopes, got {scopes:?}");
386
387 let class = scopes.iter().find(|s| s.name == "Shape");
389 assert!(class.is_some(), "Missing 'Shape' class scope");
390 assert_eq!(class.unwrap().scope_type, "class");
391
392 let draw = scopes.iter().find(|s| s.name == "draw");
394 assert!(draw.is_some(), "Missing 'draw' abstract method scope");
395 assert_eq!(draw.unwrap().scope_type, "method");
396
397 let area = scopes.iter().find(|s| s.name == "area");
398 assert!(area.is_some(), "Missing 'area' abstract method scope");
399 assert_eq!(area.unwrap().scope_type, "method");
400 }
401
402 #[test]
403 fn test_extract_scopes_interface_methods() {
404 let plugin = JavaPlugin::default();
405 let source = b"public interface Drawable {
406 void draw();
407 default void init() { System.out.println(\"init\"); }
408}";
409 let tree = plugin.parse_ast(source).unwrap();
410 let scopes = plugin
411 .extract_scopes(&tree, source, Path::new("Drawable.java"))
412 .unwrap();
413
414 assert_eq!(scopes.len(), 3, "Expected 3 scopes, got {scopes:?}");
416
417 let iface = scopes.iter().find(|s| s.name == "Drawable");
419 assert!(iface.is_some(), "Missing 'Drawable' interface scope");
420 assert_eq!(iface.unwrap().scope_type, "interface");
421
422 let draw = scopes.iter().find(|s| s.name == "draw");
424 assert!(draw.is_some(), "Missing 'draw' method scope");
425 assert_eq!(draw.unwrap().scope_type, "method");
426
427 let init = scopes.iter().find(|s| s.name == "init");
429 assert!(init.is_some(), "Missing 'init' default method scope");
430 assert_eq!(init.unwrap().scope_type, "method");
431 }
432
433 #[test]
434 fn test_extract_scopes_record() {
435 let plugin = JavaPlugin::default();
436 let source = b"public record Point(int x, int y) {
437 public double distance() {
438 return Math.sqrt(x * x + y * y);
439 }
440}";
441 let tree = plugin.parse_ast(source).unwrap();
442 let scopes = plugin
443 .extract_scopes(&tree, source, Path::new("Point.java"))
444 .unwrap();
445
446 assert_eq!(scopes.len(), 2, "Expected 2 scopes, got {scopes:?}");
448
449 let record = scopes.iter().find(|s| s.name == "Point");
451 assert!(record.is_some(), "Missing 'Point' record scope");
452 assert_eq!(record.unwrap().scope_type, "record");
453
454 let method = scopes.iter().find(|s| s.name == "distance");
456 assert!(method.is_some(), "Missing 'distance' method scope");
457 assert_eq!(method.unwrap().scope_type, "method");
458 }
459}