Skip to content

Commit eae4107

Browse files
committed
feat(indexer): add Java language support
- Enable Java parsing by registering file extension - Add Java parser dependency
1 parent 7a6264d commit eae4107

5 files changed

Lines changed: 326 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ tree-sitter-javascript = "0.23.1"
5757
tree-sitter-json = "0.24.8"
5858
tree-sitter-go = "0.23.4"
5959
tree-sitter-cpp = "0.23.4"
60+
tree-sitter-java = "0.23.5"
6061
tree-sitter-bash = "0.25.0"
6162
tree-sitter-css = "0.23.2"
6263
tree-sitter-lua = "0.2.0"

src/indexer/file_utils.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ impl FileUtils {
100100
"ts" | "tsx" => Some("typescript"),
101101
// Go
102102
"go" => Some("go"),
103+
// Java
104+
"java" => Some("java"),
103105
// C++
104106
"cpp" | "cc" | "cxx" | "c" | "h" | "hpp" => Some("cpp"),
105107
// PHP

src/indexer/languages/java.rs

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
// Copyright 2025 Muvon Un Limited
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! Java language implementation for the indexer
16+
17+
use crate::indexer::languages::Language;
18+
use tree_sitter::Node;
19+
20+
pub struct Java {}
21+
22+
impl Language for Java {
23+
fn name(&self) -> &'static str {
24+
"java"
25+
}
26+
27+
fn get_ts_language(&self) -> tree_sitter::Language {
28+
tree_sitter_java::LANGUAGE.into()
29+
}
30+
31+
fn get_meaningful_kinds(&self) -> Vec<&'static str> {
32+
vec![
33+
"class_declaration",
34+
"interface_declaration",
35+
"enum_declaration",
36+
"method_declaration",
37+
"constructor_declaration",
38+
"field_declaration",
39+
"annotation_type_declaration",
40+
"record_declaration", // Java 14+
41+
"import_declaration",
42+
"package_declaration",
43+
// Lambda expressions and method references for modern Java
44+
"lambda_expression",
45+
"method_reference",
46+
]
47+
}
48+
49+
fn extract_symbols(&self, node: Node, contents: &str) -> Vec<String> {
50+
let mut symbols = Vec::new();
51+
52+
match node.kind() {
53+
"class_declaration"
54+
| "interface_declaration"
55+
| "enum_declaration"
56+
| "annotation_type_declaration"
57+
| "record_declaration" => {
58+
// Extract class/interface/enum/annotation/record name
59+
for child in node.children(&mut node.walk()) {
60+
if child.kind() == "identifier" {
61+
if let Ok(name) = child.utf8_text(contents.as_bytes()) {
62+
symbols.push(name.to_string());
63+
break;
64+
}
65+
}
66+
}
67+
}
68+
"method_declaration" | "constructor_declaration" => {
69+
// Extract method or constructor name
70+
for child in node.children(&mut node.walk()) {
71+
if child.kind() == "identifier" {
72+
if let Ok(name) = child.utf8_text(contents.as_bytes()) {
73+
symbols.push(name.to_string());
74+
break;
75+
}
76+
}
77+
}
78+
}
79+
"field_declaration" => {
80+
// Extract field names (can be multiple in one declaration)
81+
let mut cursor = node.walk();
82+
if cursor.goto_first_child() {
83+
loop {
84+
let child = cursor.node();
85+
if child.kind() == "variable_declarator" {
86+
// Look for identifier in variable_declarator
87+
for var_child in child.children(&mut child.walk()) {
88+
if var_child.kind() == "identifier" {
89+
if let Ok(name) = var_child.utf8_text(contents.as_bytes()) {
90+
symbols.push(name.to_string());
91+
}
92+
break;
93+
}
94+
}
95+
}
96+
if !cursor.goto_next_sibling() {
97+
break;
98+
}
99+
}
100+
}
101+
}
102+
"import_declaration" => {
103+
// Extract import path
104+
if let Ok(import_text) = node.utf8_text(contents.as_bytes()) {
105+
// Clean up import statement to extract the actual import path
106+
let import_path = import_text
107+
.trim()
108+
.strip_prefix("import")
109+
.unwrap_or(import_text)
110+
.strip_prefix("static")
111+
.unwrap_or(import_text.strip_prefix("import").unwrap_or(import_text))
112+
.trim()
113+
.trim_end_matches(';')
114+
.trim();
115+
if !import_path.is_empty() {
116+
symbols.push(import_path.to_string());
117+
}
118+
}
119+
}
120+
"package_declaration" => {
121+
// Extract package name
122+
for child in node.children(&mut node.walk()) {
123+
if child.kind() == "scoped_identifier" || child.kind() == "identifier" {
124+
if let Ok(package_name) = child.utf8_text(contents.as_bytes()) {
125+
symbols.push(package_name.to_string());
126+
break;
127+
}
128+
}
129+
}
130+
}
131+
"lambda_expression" => {
132+
// For lambda expressions, we might want to extract parameter names or mark as lambda
133+
symbols.push("<lambda>".to_string());
134+
}
135+
"method_reference" => {
136+
// For method references, extract the referenced method if possible
137+
if let Ok(method_ref) = node.utf8_text(contents.as_bytes()) {
138+
symbols.push(method_ref.to_string());
139+
}
140+
}
141+
_ => {}
142+
}
143+
144+
symbols
145+
}
146+
147+
fn extract_imports_exports(&self, node: Node, contents: &str) -> (Vec<String>, Vec<String>) {
148+
let mut imports = Vec::new();
149+
let mut exports = Vec::new();
150+
151+
match node.kind() {
152+
"import_declaration" => {
153+
if let Ok(import_text) = node.utf8_text(contents.as_bytes()) {
154+
// Clean up import statement
155+
let import_path = import_text
156+
.trim()
157+
.strip_prefix("import")
158+
.unwrap_or(import_text)
159+
.strip_prefix("static")
160+
.unwrap_or(import_text.strip_prefix("import").unwrap_or(import_text))
161+
.trim()
162+
.trim_end_matches(';')
163+
.trim();
164+
if !import_path.is_empty() {
165+
imports.push(import_path.to_string());
166+
}
167+
}
168+
}
169+
"package_declaration" => {
170+
// Package declaration defines the current module's namespace
171+
for child in node.children(&mut node.walk()) {
172+
if child.kind() == "scoped_identifier" || child.kind() == "identifier" {
173+
if let Ok(package_name) = child.utf8_text(contents.as_bytes()) {
174+
exports.push(format!("package:{}", package_name));
175+
break;
176+
}
177+
}
178+
}
179+
}
180+
"class_declaration"
181+
| "interface_declaration"
182+
| "enum_declaration"
183+
| "annotation_type_declaration"
184+
| "record_declaration" => {
185+
// Check if this is a public declaration (exported)
186+
let mut is_public = false;
187+
let mut type_name = String::new();
188+
189+
for child in node.children(&mut node.walk()) {
190+
if child.kind() == "modifiers" {
191+
if let Ok(modifiers_text) = child.utf8_text(contents.as_bytes()) {
192+
if modifiers_text.contains("public") {
193+
is_public = true;
194+
}
195+
}
196+
} else if child.kind() == "identifier" {
197+
if let Ok(name) = child.utf8_text(contents.as_bytes()) {
198+
type_name = name.to_string();
199+
}
200+
}
201+
}
202+
203+
if is_public && !type_name.is_empty() {
204+
let type_kind = match node.kind() {
205+
"class_declaration" => "class",
206+
"interface_declaration" => "interface",
207+
"enum_declaration" => "enum",
208+
"annotation_type_declaration" => "annotation",
209+
"record_declaration" => "record",
210+
_ => "type",
211+
};
212+
exports.push(format!("{}:{}", type_kind, type_name));
213+
}
214+
}
215+
"method_declaration" => {
216+
// Check if this is a public method (exported)
217+
let mut is_public = false;
218+
let mut method_name = String::new();
219+
220+
for child in node.children(&mut node.walk()) {
221+
if child.kind() == "modifiers" {
222+
if let Ok(modifiers_text) = child.utf8_text(contents.as_bytes()) {
223+
if modifiers_text.contains("public") {
224+
is_public = true;
225+
}
226+
}
227+
} else if child.kind() == "identifier" {
228+
if let Ok(name) = child.utf8_text(contents.as_bytes()) {
229+
method_name = name.to_string();
230+
}
231+
}
232+
}
233+
234+
if is_public && !method_name.is_empty() {
235+
exports.push(format!("method:{}", method_name));
236+
}
237+
}
238+
_ => {}
239+
}
240+
241+
(imports, exports)
242+
}
243+
244+
fn are_node_types_equivalent(&self, type1: &str, type2: &str) -> bool {
245+
// Java-specific equivalences
246+
match (type1, type2) {
247+
// Methods and constructors are similar in structure
248+
("method_declaration", "constructor_declaration")
249+
| ("constructor_declaration", "method_declaration") => true,
250+
// All type declarations are similar
251+
("class_declaration", "interface_declaration")
252+
| ("interface_declaration", "class_declaration") => true,
253+
("class_declaration", "enum_declaration")
254+
| ("enum_declaration", "class_declaration") => true,
255+
("interface_declaration", "enum_declaration")
256+
| ("enum_declaration", "interface_declaration") => true,
257+
("annotation_type_declaration", "class_declaration")
258+
| ("class_declaration", "annotation_type_declaration") => true,
259+
("record_declaration", "class_declaration")
260+
| ("class_declaration", "record_declaration") => true,
261+
// Default: exact match
262+
_ => type1 == type2,
263+
}
264+
}
265+
266+
fn get_node_type_description(&self, node_type: &str) -> &'static str {
267+
match node_type {
268+
"class_declaration" => "Java class definition",
269+
"interface_declaration" => "Java interface definition",
270+
"enum_declaration" => "Java enum definition",
271+
"method_declaration" => "Java method definition",
272+
"constructor_declaration" => "Java constructor definition",
273+
"field_declaration" => "Java field declaration",
274+
"annotation_type_declaration" => "Java annotation type definition",
275+
"record_declaration" => "Java record definition (Java 14+)",
276+
"import_declaration" => "Java import statement",
277+
"package_declaration" => "Java package declaration",
278+
"lambda_expression" => "Java lambda expression",
279+
"method_reference" => "Java method reference",
280+
_ => "Java code element",
281+
}
282+
}
283+
284+
fn extract_identifiers(&self, node: Node, contents: &str, rust_files: &mut Vec<String>) {
285+
// For Java, identifiers are class names, method names, field names, etc.
286+
// This method populates the rust_files vector with discovered identifiers
287+
let symbols = self.extract_symbols(node, contents);
288+
rust_files.extend(symbols);
289+
}
290+
291+
fn resolve_import(
292+
&self,
293+
import_path: &str,
294+
_current_file: &str,
295+
_java_files: &[String],
296+
) -> Option<String> {
297+
// Java import resolution - convert import statements to file paths
298+
// For example: java.util.List -> java/util/List.java
299+
if import_path.contains('.') && !import_path.ends_with('*') {
300+
Some(format!("{}.java", import_path.replace('.', "/")))
301+
} else {
302+
None
303+
}
304+
}
305+
306+
fn get_file_extensions(&self) -> Vec<&'static str> {
307+
vec!["java"]
308+
}
309+
}

src/indexer/languages/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ mod bash;
2222
mod cpp;
2323
mod css;
2424
mod go;
25+
mod java;
2526
mod javascript;
2627
mod json;
2728
mod lua;
@@ -41,6 +42,7 @@ pub use bash::Bash;
4142
pub use cpp::Cpp;
4243
pub use css::Css;
4344
pub use go::Go;
45+
pub use java::Java;
4446
pub use javascript::JavaScript;
4547
pub use json::Json;
4648
pub use lua::Lua;
@@ -127,6 +129,7 @@ pub fn get_language(name: &str) -> Option<Box<dyn Language>> {
127129
"typescript" => Some(Box::new(TypeScript {})),
128130
"python" => Some(Box::new(Python {})),
129131
"go" => Some(Box::new(Go {})),
132+
"java" => Some(Box::new(Java {})),
130133
"cpp" => Some(Box::new(Cpp {})),
131134
"php" => Some(Box::new(Php {})),
132135
"bash" => Some(Box::new(Bash {})),

0 commit comments

Comments
 (0)