Skip to main content

sqry_lang_java/
lib.rs

1//! Java language plugin for sqry
2//!
3//! Implements the `LanguagePlugin` trait for Java, providing:
4//! - AST parsing with tree-sitter
5//! - Scope extraction
6//! - Relation extraction via `JavaGraphBuilder` (calls, imports, exports, OOP edges)
7//!
8//! This plugin enables semantic code search for Java codebases, the #1 priority
9//! language for enterprise adoption (90%+ Fortune 500 companies).
10
11use 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
22/// Java language plugin
23///
24/// Provides language support for Java source files (.java).
25///
26/// # Supported Constructs
27///
28/// - Classes (`class Foo`)
29/// - Interfaces (`interface Bar`)
30/// - Methods (instance, static, constructors)
31/// - Enums (`enum Color`)
32/// - Fields (instance and static fields)
33/// - Constants (`static final` fields)
34///
35/// # Example
36///
37/// ```
38/// use sqry_lang_java::JavaPlugin;
39/// use sqry_core::plugin::LanguagePlugin;
40///
41/// let plugin = JavaPlugin::default();
42/// let metadata = plugin.metadata();
43/// assert_eq!(metadata.id, "java");
44/// assert_eq!(metadata.name, "Java");
45/// ```
46pub 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    /// Tree-sitter query source for Java scope extraction
113    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    /// Extract scopes from Java source code using tree-sitter queries
152    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            // Find the type and name captures
170            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), // Will be assigned by link_nested_scopes
193                    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        // Sort by position and link nested scopes
206        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        // Just verify we can get a language (ABI version should be non-zero)
258        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        // Should find class and method scopes
290        assert_eq!(scopes.len(), 2, "Expected 2 scopes, got {scopes:?}");
291
292        // Find class scope
293        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        // Find method scope
298        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        // Interface should be captured (method without body won't be)
315        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        // Should find outer class, inner class, and method
354        assert_eq!(scopes.len(), 3, "Expected 3 scopes, got {scopes:?}");
355
356        // Outer class should have no parent
357        let outer = scopes.iter().find(|s| s.name == "Outer");
358        assert!(outer.is_some());
359        assert!(outer.unwrap().parent_id.is_none());
360
361        // Inner class should have Outer as parent
362        let inner = scopes.iter().find(|s| s.name == "Inner");
363        assert!(inner.is_some());
364        assert!(inner.unwrap().parent_id.is_some());
365
366        // Method should have Inner as parent
367        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        // Should find abstract class and both abstract methods
385        assert_eq!(scopes.len(), 3, "Expected 3 scopes, got {scopes:?}");
386
387        // Abstract class should be present
388        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        // Abstract methods should be present
393        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        // Should find interface and both methods (abstract and default)
415        assert_eq!(scopes.len(), 3, "Expected 3 scopes, got {scopes:?}");
416
417        // Interface should be present
418        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        // Abstract interface method should be present
423        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        // Default method should be present
428        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        // Should find record and method
447        assert_eq!(scopes.len(), 2, "Expected 2 scopes, got {scopes:?}");
448
449        // Record should be present
450        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        // Method in record should be present
455        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}