use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
use std::path::Path;
use tree_sitter::Node;
pub struct Java;
impl Language for Java {
fn name(&self) -> &'static str {
"Java"
}
fn extensions(&self) -> &'static [&'static str] {
&["java"]
}
fn grammar_name(&self) -> &'static str {
"java"
}
fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
Some(self)
}
fn signature_suffix(&self) -> &'static str {
" {}"
}
fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
extract_javadoc(node, content)
}
fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
extract_annotations(node, content)
}
fn refine_kind(
&self,
node: &Node,
_content: &str,
tag_kind: crate::SymbolKind,
) -> crate::SymbolKind {
match node.kind() {
"enum_declaration" => crate::SymbolKind::Enum,
"interface_declaration" | "annotation_type_declaration" => crate::SymbolKind::Interface,
"record_declaration" => crate::SymbolKind::Struct,
_ => tag_kind,
}
}
fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
let mut implements = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "superclass" {
let mut sc = child.walk();
for t in child.children(&mut sc) {
if t.kind() == "type_identifier" {
implements.push(content[t.byte_range()].to_string());
}
}
} else if child.kind() == "super_interfaces" {
let mut si = child.walk();
for list in child.children(&mut si) {
if list.kind() == "type_list" {
let mut tc = list.walk();
for t in list.children(&mut tc) {
if t.kind() == "type_identifier" {
implements.push(content[t.byte_range()].to_string());
}
}
}
}
}
}
crate::ImplementsInfo {
is_interface: node.kind() == "interface_declaration",
implements,
}
}
fn build_signature(&self, node: &Node, content: &str) -> String {
let name = match self.node_name(node, content) {
Some(n) => n,
None => {
return content[node.byte_range()]
.lines()
.next()
.unwrap_or("")
.trim()
.to_string();
}
};
match node.kind() {
"method_declaration" | "constructor_declaration" => {
let params = node
.child_by_field_name("parameters")
.map(|p| content[p.byte_range()].to_string())
.unwrap_or_else(|| "()".to_string());
format!("{}{}", name, params)
}
"class_declaration" => format!("class {}", name),
"interface_declaration" => format!("interface {}", name),
"enum_declaration" => format!("enum {}", name),
_ => {
let text = &content[node.byte_range()];
text.lines().next().unwrap_or(text).trim().to_string()
}
}
}
fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
if node.kind() != "import_declaration" {
return Vec::new();
}
let line = node.start_position().row + 1;
let text = &content[node.byte_range()];
let is_static = text.contains("static ");
let is_wildcard = text.contains(".*");
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "scoped_identifier" || child.kind() == "identifier" {
let module = content[child.byte_range()].to_string();
return vec![Import {
module,
names: Vec::new(),
alias: if is_static {
Some("static".to_string())
} else {
None
},
is_wildcard,
is_relative: false,
line,
}];
}
}
Vec::new()
}
fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
if import.is_wildcard {
format!("import {}.*;", import.module)
} else {
format!("import {};", import.module)
}
}
fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
let has_test_attr = symbol.attributes.iter().any(|a| a.contains("@Test"));
if has_test_attr {
return true;
}
match symbol.kind {
crate::SymbolKind::Class => {
symbol.name.starts_with("Test") || symbol.name.ends_with("Test")
}
_ => false,
}
}
fn test_file_globs(&self) -> &'static [&'static str] {
&[
"**/src/test/**/*.java",
"**/Test*.java",
"**/*Test.java",
"**/*Tests.java",
]
}
fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
node.child_by_field_name("body")
}
fn analyze_container_body(
&self,
body_node: &Node,
content: &str,
inner_indent: &str,
) -> Option<ContainerBody> {
crate::body::analyze_brace_body(body_node, content, inner_indent)
}
fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "modifiers" {
let mods = &content[child.byte_range()];
if mods.contains("private") {
return Visibility::Private;
}
if mods.contains("protected") {
return Visibility::Protected;
}
return Visibility::Public;
}
}
Visibility::Public
}
fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
static RESOLVER: JavaModuleResolver = JavaModuleResolver;
Some(&RESOLVER)
}
}
impl LanguageSymbols for Java {}
pub struct JavaModuleResolver;
const JAVA_SRC_DIRS: &[&str] = &["src/main/java", "src/test/java", ""];
impl ModuleResolver for JavaModuleResolver {
fn workspace_config(&self, root: &Path) -> ResolverConfig {
ResolverConfig {
workspace_root: root.to_path_buf(),
path_mappings: Vec::new(),
search_roots: JAVA_SRC_DIRS.iter().map(|d| root.join(d)).collect(),
}
}
fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
if ext != "java" {
return Vec::new();
}
for search_root in &cfg.search_roots {
if let Ok(rel) = file.strip_prefix(search_root) {
let rel_str = rel
.to_str()
.unwrap_or("")
.trim_end_matches(".java")
.replace(['/', '\\'], ".");
if !rel_str.is_empty() {
return vec![ModuleId {
canonical_path: rel_str,
}];
}
}
}
Vec::new()
}
fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
if ext != "java" {
return Resolution::NotApplicable;
}
let raw = &spec.raw;
let path_part = raw.replace('.', "/");
let file_name = format!("{}.java", path_part);
let exported_name = raw.rsplit('.').next().unwrap_or(raw).to_string();
for search_root in &cfg.search_roots {
let candidate = search_root.join(&file_name);
if candidate.exists() {
return Resolution::Resolved(candidate, exported_name);
}
}
Resolution::NotFound
}
}
fn extract_javadoc(node: &Node, content: &str) -> Option<String> {
let mut prev = node.prev_sibling();
while let Some(sibling) = prev {
match sibling.kind() {
"block_comment" => {
let text = &content[sibling.byte_range()];
if text.starts_with("/**") {
return Some(clean_block_doc_comment(text));
}
return None;
}
"line_comment" => {
}
"modifiers" | "marker_annotation" | "annotation" => {
}
_ => return None,
}
prev = sibling.prev_sibling();
}
None
}
fn clean_block_doc_comment(text: &str) -> String {
let lines: Vec<&str> = text
.strip_prefix("/**")
.unwrap_or(text)
.strip_suffix("*/")
.unwrap_or(text)
.lines()
.map(|l| l.trim().strip_prefix('*').unwrap_or(l).trim())
.filter(|l| !l.is_empty())
.collect();
lines.join(" ")
}
fn extract_annotations(node: &Node, content: &str) -> Vec<String> {
let mut attrs = Vec::new();
if let Some(modifiers) = node.child_by_field_name("modifiers").or_else(|| {
let mut cursor = node.walk();
node.children(&mut cursor).find(|c| c.kind() == "modifiers")
}) {
let mut cursor = modifiers.walk();
for child in modifiers.children(&mut cursor) {
if child.kind() == "marker_annotation" || child.kind() == "annotation" {
attrs.push(content[child.byte_range()].to_string());
}
}
}
attrs
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validate_unused_kinds_audit;
#[test]
fn unused_node_kinds_audit() {
#[rustfmt::skip]
let documented_unused: &[&str] = &[
"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",
"return_statement",
"constructor_declaration",
"binary_expression",
"try_statement",
"continue_statement",
"switch_expression",
"ternary_expression",
"while_statement",
"break_statement",
"enhanced_for_statement",
"import_declaration",
"for_statement",
"block",
"throw_statement",
"catch_clause",
"if_statement",
];
validate_unused_kinds_audit(&Java, documented_unused)
.expect("Java unused node kinds audit failed");
}
}