1use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
5use std::path::Path;
6use tree_sitter::Node;
7
8pub struct Java;
10
11impl Language for Java {
12 fn name(&self) -> &'static str {
13 "Java"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["java"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "java"
20 }
21
22 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
23 Some(self)
24 }
25
26 fn signature_suffix(&self) -> &'static str {
27 " {}"
28 }
29
30 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
31 extract_javadoc(node, content)
32 }
33
34 fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
35 extract_annotations(node, content)
36 }
37
38 fn refine_kind(
39 &self,
40 node: &Node,
41 _content: &str,
42 tag_kind: crate::SymbolKind,
43 ) -> crate::SymbolKind {
44 match node.kind() {
45 "enum_declaration" => crate::SymbolKind::Enum,
46 "interface_declaration" | "annotation_type_declaration" => crate::SymbolKind::Interface,
47 "record_declaration" => crate::SymbolKind::Struct,
48 _ => tag_kind,
49 }
50 }
51
52 fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
53 let mut implements = Vec::new();
54 let mut cursor = node.walk();
55 for child in node.children(&mut cursor) {
56 if child.kind() == "superclass" {
57 let mut sc = child.walk();
58 for t in child.children(&mut sc) {
59 if t.kind() == "type_identifier" {
60 implements.push(content[t.byte_range()].to_string());
61 }
62 }
63 } else if child.kind() == "super_interfaces" {
64 let mut si = child.walk();
65 for list in child.children(&mut si) {
66 if list.kind() == "type_list" {
67 let mut tc = list.walk();
68 for t in list.children(&mut tc) {
69 if t.kind() == "type_identifier" {
70 implements.push(content[t.byte_range()].to_string());
71 }
72 }
73 }
74 }
75 }
76 }
77 crate::ImplementsInfo {
78 is_interface: node.kind() == "interface_declaration",
79 implements,
80 }
81 }
82
83 fn build_signature(&self, node: &Node, content: &str) -> String {
84 let name = match self.node_name(node, content) {
85 Some(n) => n,
86 None => {
87 return content[node.byte_range()]
88 .lines()
89 .next()
90 .unwrap_or("")
91 .trim()
92 .to_string();
93 }
94 };
95 match node.kind() {
96 "method_declaration" | "constructor_declaration" => {
97 let params = node
98 .child_by_field_name("parameters")
99 .map(|p| content[p.byte_range()].to_string())
100 .unwrap_or_else(|| "()".to_string());
101 format!("{}{}", name, params)
102 }
103 "class_declaration" => format!("class {}", name),
104 "interface_declaration" => format!("interface {}", name),
105 "enum_declaration" => format!("enum {}", name),
106 _ => {
107 let text = &content[node.byte_range()];
108 text.lines().next().unwrap_or(text).trim().to_string()
109 }
110 }
111 }
112
113 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
114 if node.kind() != "import_declaration" {
115 return Vec::new();
116 }
117
118 let line = node.start_position().row + 1;
119 let text = &content[node.byte_range()];
120
121 let is_static = text.contains("static ");
123 let is_wildcard = text.contains(".*");
124
125 let mut cursor = node.walk();
127 for child in node.children(&mut cursor) {
128 if child.kind() == "scoped_identifier" || child.kind() == "identifier" {
129 let module = content[child.byte_range()].to_string();
130 return vec![Import {
131 module,
132 names: Vec::new(),
133 alias: if is_static {
134 Some("static".to_string())
135 } else {
136 None
137 },
138 is_wildcard,
139 is_relative: false,
140 line,
141 }];
142 }
143 }
144
145 Vec::new()
146 }
147
148 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
149 if import.is_wildcard {
151 format!("import {}.*;", import.module)
152 } else {
153 format!("import {};", import.module)
154 }
155 }
156
157 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
158 let has_test_attr = symbol.attributes.iter().any(|a| a.contains("@Test"));
159 if has_test_attr {
160 return true;
161 }
162 match symbol.kind {
163 crate::SymbolKind::Class => {
164 symbol.name.starts_with("Test") || symbol.name.ends_with("Test")
165 }
166 _ => false,
167 }
168 }
169
170 fn test_file_globs(&self) -> &'static [&'static str] {
171 &[
172 "**/src/test/**/*.java",
173 "**/Test*.java",
174 "**/*Test.java",
175 "**/*Tests.java",
176 ]
177 }
178
179 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
180 node.child_by_field_name("body")
181 }
182
183 fn analyze_container_body(
184 &self,
185 body_node: &Node,
186 content: &str,
187 inner_indent: &str,
188 ) -> Option<ContainerBody> {
189 crate::body::analyze_brace_body(body_node, content, inner_indent)
190 }
191
192 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
193 let mut cursor = node.walk();
194 for child in node.children(&mut cursor) {
195 if child.kind() == "modifiers" {
196 let mods = &content[child.byte_range()];
197 if mods.contains("private") {
198 return Visibility::Private;
199 }
200 if mods.contains("protected") {
201 return Visibility::Protected;
202 }
203 return Visibility::Public;
205 }
206 }
207 Visibility::Public
209 }
210
211 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
212 static RESOLVER: JavaModuleResolver = JavaModuleResolver;
213 Some(&RESOLVER)
214 }
215}
216
217impl LanguageSymbols for Java {}
218
219pub struct JavaModuleResolver;
228
229const JAVA_SRC_DIRS: &[&str] = &["src/main/java", "src/test/java", ""];
231
232impl ModuleResolver for JavaModuleResolver {
233 fn workspace_config(&self, root: &Path) -> ResolverConfig {
234 ResolverConfig {
235 workspace_root: root.to_path_buf(),
236 path_mappings: Vec::new(),
237 search_roots: JAVA_SRC_DIRS.iter().map(|d| root.join(d)).collect(),
238 }
239 }
240
241 fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
242 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
243 if ext != "java" {
244 return Vec::new();
245 }
246 for search_root in &cfg.search_roots {
247 if let Ok(rel) = file.strip_prefix(search_root) {
248 let rel_str = rel
249 .to_str()
250 .unwrap_or("")
251 .trim_end_matches(".java")
252 .replace(['/', '\\'], ".");
253 if !rel_str.is_empty() {
254 return vec![ModuleId {
255 canonical_path: rel_str,
256 }];
257 }
258 }
259 }
260 Vec::new()
261 }
262
263 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
264 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
265 if ext != "java" {
266 return Resolution::NotApplicable;
267 }
268
269 let raw = &spec.raw;
270 let path_part = raw.replace('.', "/");
272 let file_name = format!("{}.java", path_part);
273 let exported_name = raw.rsplit('.').next().unwrap_or(raw).to_string();
274
275 for search_root in &cfg.search_roots {
276 let candidate = search_root.join(&file_name);
277 if candidate.exists() {
278 return Resolution::Resolved(candidate, exported_name);
279 }
280 }
281
282 Resolution::NotFound
283 }
284}
285
286fn extract_javadoc(node: &Node, content: &str) -> Option<String> {
290 let mut prev = node.prev_sibling();
291 while let Some(sibling) = prev {
292 match sibling.kind() {
293 "block_comment" => {
294 let text = &content[sibling.byte_range()];
295 if text.starts_with("/**") {
296 return Some(clean_block_doc_comment(text));
297 }
298 return None;
299 }
300 "line_comment" => {
301 }
303 "modifiers" | "marker_annotation" | "annotation" => {
304 }
306 _ => return None,
307 }
308 prev = sibling.prev_sibling();
309 }
310 None
311}
312
313fn clean_block_doc_comment(text: &str) -> String {
315 let lines: Vec<&str> = text
316 .strip_prefix("/**")
317 .unwrap_or(text)
318 .strip_suffix("*/")
319 .unwrap_or(text)
320 .lines()
321 .map(|l| l.trim().strip_prefix('*').unwrap_or(l).trim())
322 .filter(|l| !l.is_empty())
323 .collect();
324 lines.join(" ")
325}
326
327fn extract_annotations(node: &Node, content: &str) -> Vec<String> {
329 let mut attrs = Vec::new();
330 if let Some(modifiers) = node.child_by_field_name("modifiers").or_else(|| {
331 let mut cursor = node.walk();
332 node.children(&mut cursor).find(|c| c.kind() == "modifiers")
333 }) {
334 let mut cursor = modifiers.walk();
335 for child in modifiers.children(&mut cursor) {
336 if child.kind() == "marker_annotation" || child.kind() == "annotation" {
337 attrs.push(content[child.byte_range()].to_string());
338 }
339 }
340 }
341 attrs
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use crate::validate_unused_kinds_audit;
348
349 #[test]
352 fn unused_node_kinds_audit() {
353 #[rustfmt::skip]
354 let documented_unused: &[&str] = &[
355 "block_comment", "class_body", "class_literal", "constructor_body", "enum_body", "enum_body_declarations", "enum_constant", "field_declaration", "formal_parameter", "formal_parameters", "identifier", "interface_body", "modifiers", "scoped_identifier", "scoped_type_identifier", "super_interfaces", "catch_formal_parameter", "catch_type", "extends_interfaces", "finally_clause", "switch_block", "switch_block_statement_group", "throws", "array_creation_expression", "assignment_expression", "cast_expression", "instanceof_expression", "lambda_expression", "method_reference", "parenthesized_expression","template_expression", "unary_expression", "update_expression", "yield_statement", "annotated_type", "array_type", "boolean_type", "floating_point_type", "generic_type", "integral_type", "type_arguments", "type_bound", "type_parameter", "type_parameters", "type_pattern", "void_type", "annotation_type_body", "annotation_type_declaration", "annotation_type_element_declaration", "assert_statement", "compact_constructor_declaration", "constant_declaration", "explicit_constructor_invocation", "expression_statement", "labeled_statement", "local_variable_declaration", "record_declaration", "record_pattern_body", "exports_module_directive","module_body", "module_declaration", "opens_module_directive", "package_declaration", "provides_module_directive", "requires_modifier", "requires_module_directive", "uses_module_directive", "resource_specification", "synchronized_statement", "try_with_resources_statement", "do_statement",
440 "return_statement",
441 "constructor_declaration",
442 "binary_expression",
443 "try_statement",
444 "continue_statement",
445 "switch_expression",
446 "ternary_expression",
447 "while_statement",
448 "break_statement",
449 "enhanced_for_statement",
450 "import_declaration",
451 "for_statement",
452 "block",
453 "throw_statement",
454 "catch_clause",
455 "if_statement",
456 ];
457
458 validate_unused_kinds_audit(&Java, documented_unused)
459 .expect("Java unused node kinds audit failed");
460 }
461}