Skip to main content

normalize_languages/
java.rs

1//! Java language support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use tree_sitter::Node;
8
9// ============================================================================
10// Java external package resolution
11// ============================================================================
12
13/// Get Java version.
14pub fn get_java_version() -> Option<String> {
15    let output = Command::new("java").args(["--version"]).output().ok()?;
16
17    if output.status.success() {
18        let version_str = String::from_utf8_lossy(&output.stdout);
19        // "openjdk 17.0.1 2021-10-19" or "java 21.0.1 2023-10-17 LTS"
20        for line in version_str.lines() {
21            let parts: Vec<&str> = line.split_whitespace().collect();
22            if parts.len() >= 2 {
23                let version = parts[1];
24                let ver_parts: Vec<&str> = version.split('.').collect();
25                if ver_parts.len() >= 2 {
26                    return Some(format!("{}.{}", ver_parts[0], ver_parts[1]));
27                } else if ver_parts.len() == 1 {
28                    return Some(format!("{}.0", ver_parts[0]));
29                }
30            }
31        }
32    }
33
34    None
35}
36
37/// Find Maven local repository.
38pub fn find_maven_repository() -> Option<PathBuf> {
39    // Check M2_HOME or MAVEN_HOME env var
40    if let Ok(m2_home) = std::env::var("M2_HOME").or_else(|_| std::env::var("MAVEN_HOME")) {
41        let repo = PathBuf::from(m2_home).join("repository");
42        if repo.is_dir() {
43            return Some(repo);
44        }
45    }
46
47    // Default ~/.m2/repository
48    if let Ok(home) = std::env::var("HOME") {
49        let repo = PathBuf::from(home).join(".m2").join("repository");
50        if repo.is_dir() {
51            return Some(repo);
52        }
53    }
54
55    // Windows fallback
56    if let Ok(home) = std::env::var("USERPROFILE") {
57        let repo = PathBuf::from(home).join(".m2").join("repository");
58        if repo.is_dir() {
59            return Some(repo);
60        }
61    }
62
63    None
64}
65
66/// Find Gradle cache directory.
67pub fn find_gradle_cache() -> Option<PathBuf> {
68    // Check GRADLE_USER_HOME env var
69    if let Ok(gradle_home) = std::env::var("GRADLE_USER_HOME") {
70        let cache = PathBuf::from(gradle_home)
71            .join("caches")
72            .join("modules-2")
73            .join("files-2.1");
74        if cache.is_dir() {
75            return Some(cache);
76        }
77    }
78
79    // Default ~/.gradle/caches/modules-2/files-2.1
80    if let Ok(home) = std::env::var("HOME") {
81        let cache = PathBuf::from(home)
82            .join(".gradle")
83            .join("caches")
84            .join("modules-2")
85            .join("files-2.1");
86        if cache.is_dir() {
87            return Some(cache);
88        }
89    }
90
91    // Windows fallback
92    if let Ok(home) = std::env::var("USERPROFILE") {
93        let cache = PathBuf::from(home)
94            .join(".gradle")
95            .join("caches")
96            .join("modules-2")
97            .join("files-2.1");
98        if cache.is_dir() {
99            return Some(cache);
100        }
101    }
102
103    None
104}
105
106/// Resolve a Java import to a source file in Maven/Gradle cache.
107fn resolve_java_import(
108    import: &str,
109    maven_repo: Option<&Path>,
110    gradle_cache: Option<&Path>,
111) -> Option<ResolvedPackage> {
112    let package_path = import.replace('.', "/");
113
114    // Try Maven first
115    if let Some(maven) = maven_repo
116        && let Some(result) = find_java_package_in_maven(maven, &package_path, import)
117    {
118        return Some(result);
119    }
120
121    // Try Gradle
122    if let Some(gradle) = gradle_cache
123        && let Some(result) = find_java_package_in_gradle(gradle, &package_path, import)
124    {
125        return Some(result);
126    }
127
128    None
129}
130
131fn find_java_package_in_maven(
132    maven_repo: &Path,
133    package_path: &str,
134    import: &str,
135) -> Option<ResolvedPackage> {
136    let target_dir = maven_repo.join(package_path);
137    if target_dir.is_dir() {
138        return find_maven_artifact(&target_dir, import);
139    }
140
141    // Try parent paths
142    let parts: Vec<&str> = package_path.split('/').collect();
143    for i in (2..parts.len()).rev() {
144        let dir_path = parts[..i].join("/");
145        let artifact = parts[i - 1];
146        let search_dir = maven_repo.join(&dir_path);
147
148        if search_dir.is_dir() {
149            if let Some(result) = find_maven_artifact(&search_dir, import) {
150                return Some(result);
151            }
152            let artifact_dir = search_dir.join(artifact);
153            if artifact_dir.is_dir() {
154                if let Some(result) = find_maven_artifact(&artifact_dir, import) {
155                    return Some(result);
156                }
157            }
158        }
159    }
160
161    None
162}
163
164fn find_maven_artifact(artifact_dir: &Path, import: &str) -> Option<ResolvedPackage> {
165    let versions: Vec<_> = std::fs::read_dir(artifact_dir)
166        .ok()?
167        .flatten()
168        .filter(|e| e.path().is_dir())
169        .collect();
170
171    if versions.is_empty() {
172        return None;
173    }
174
175    let mut versions: Vec<_> = versions.into_iter().collect();
176    versions.sort_by(|a, b| {
177        let a_name = a.file_name().to_string_lossy().to_string();
178        let b_name = b.file_name().to_string_lossy().to_string();
179        version_cmp(&a_name, &b_name)
180    });
181
182    let version_dir = versions.last()?.path();
183    let artifact_name = artifact_dir.file_name()?.to_string_lossy().to_string();
184    let version = version_dir.file_name()?.to_string_lossy().to_string();
185
186    // Prefer sources JAR
187    let sources_jar = version_dir.join(format!("{}-{}-sources.jar", artifact_name, version));
188    if sources_jar.is_file() {
189        return Some(ResolvedPackage {
190            path: sources_jar,
191            name: import.to_string(),
192            is_namespace: false,
193        });
194    }
195
196    // Fall back to regular JAR
197    let jar = version_dir.join(format!("{}-{}.jar", artifact_name, version));
198    if jar.is_file() {
199        return Some(ResolvedPackage {
200            path: jar,
201            name: import.to_string(),
202            is_namespace: false,
203        });
204    }
205
206    None
207}
208
209fn find_java_package_in_gradle(
210    gradle_cache: &Path,
211    package_path: &str,
212    import: &str,
213) -> Option<ResolvedPackage> {
214    let parts: Vec<&str> = package_path.split('/').collect();
215
216    for i in (2..parts.len()).rev() {
217        let group = parts[..i - 1].join(".");
218        let artifact = parts[i - 1];
219        let group_dir = gradle_cache.join(&group);
220
221        if group_dir.is_dir() {
222            let artifact_dir = group_dir.join(artifact);
223            if artifact_dir.is_dir() {
224                if let Some(result) = find_gradle_artifact(&artifact_dir, import) {
225                    return Some(result);
226                }
227            }
228        }
229    }
230
231    None
232}
233
234fn find_gradle_artifact(artifact_dir: &Path, import: &str) -> Option<ResolvedPackage> {
235    let versions: Vec<_> = std::fs::read_dir(artifact_dir)
236        .ok()?
237        .flatten()
238        .filter(|e| e.path().is_dir())
239        .collect();
240
241    if versions.is_empty() {
242        return None;
243    }
244
245    let mut versions: Vec<_> = versions.into_iter().collect();
246    versions.sort_by(|a, b| {
247        let a_name = a.file_name().to_string_lossy().to_string();
248        let b_name = b.file_name().to_string_lossy().to_string();
249        version_cmp(&a_name, &b_name)
250    });
251
252    let version_dir = versions.last()?.path();
253
254    let hash_dirs: Vec<_> = std::fs::read_dir(&version_dir)
255        .ok()?
256        .flatten()
257        .filter(|e| e.path().is_dir())
258        .collect();
259
260    for hash_dir in hash_dirs {
261        let hash_path = hash_dir.path();
262
263        // Look for sources JAR first
264        if let Ok(entries) = std::fs::read_dir(&hash_path) {
265            for entry in entries.flatten() {
266                let name = entry.file_name().to_string_lossy().to_string();
267                if name.ends_with("-sources.jar") {
268                    return Some(ResolvedPackage {
269                        path: entry.path(),
270                        name: import.to_string(),
271                        is_namespace: false,
272                    });
273                }
274            }
275        }
276
277        // Fall back to regular JAR
278        if let Ok(entries) = std::fs::read_dir(&hash_path) {
279            for entry in entries.flatten() {
280                let name = entry.file_name().to_string_lossy().to_string();
281                if name.ends_with(".jar")
282                    && !name.ends_with("-sources.jar")
283                    && !name.ends_with("-javadoc.jar")
284                {
285                    return Some(ResolvedPackage {
286                        path: entry.path(),
287                        name: import.to_string(),
288                        is_namespace: false,
289                    });
290                }
291            }
292        }
293    }
294
295    None
296}
297
298fn version_cmp(a: &str, b: &str) -> std::cmp::Ordering {
299    let a_parts: Vec<u32> = a.split('.').filter_map(|p| p.parse().ok()).collect();
300    let b_parts: Vec<u32> = b.split('.').filter_map(|p| p.parse().ok()).collect();
301
302    for (ap, bp) in a_parts.iter().zip(b_parts.iter()) {
303        match ap.cmp(bp) {
304            std::cmp::Ordering::Equal => continue,
305            other => return other,
306        }
307    }
308    a_parts.len().cmp(&b_parts.len())
309}
310
311// ============================================================================
312// Java language support
313// ============================================================================
314
315/// Java language support.
316pub struct Java;
317
318impl Language for Java {
319    fn name(&self) -> &'static str {
320        "Java"
321    }
322    fn extensions(&self) -> &'static [&'static str] {
323        &["java"]
324    }
325    fn grammar_name(&self) -> &'static str {
326        "java"
327    }
328
329    fn has_symbols(&self) -> bool {
330        true
331    }
332
333    fn container_kinds(&self) -> &'static [&'static str] {
334        &[
335            "class_declaration",
336            "interface_declaration",
337            "enum_declaration",
338        ]
339    }
340
341    fn function_kinds(&self) -> &'static [&'static str] {
342        &["method_declaration", "constructor_declaration"]
343    }
344
345    fn type_kinds(&self) -> &'static [&'static str] {
346        &[
347            "class_declaration",
348            "interface_declaration",
349            "enum_declaration",
350        ]
351    }
352
353    fn import_kinds(&self) -> &'static [&'static str] {
354        &["import_declaration"]
355    }
356
357    fn public_symbol_kinds(&self) -> &'static [&'static str] {
358        &[
359            "class_declaration",
360            "interface_declaration",
361            "enum_declaration",
362            "method_declaration",
363        ]
364    }
365
366    fn visibility_mechanism(&self) -> VisibilityMechanism {
367        VisibilityMechanism::AccessModifier
368    }
369
370    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
371        if self.get_visibility(node, content) != Visibility::Public {
372            return Vec::new();
373        }
374
375        let name = match self.node_name(node, content) {
376            Some(n) => n.to_string(),
377            None => return Vec::new(),
378        };
379
380        let kind = match node.kind() {
381            "class_declaration" => SymbolKind::Class,
382            "interface_declaration" => SymbolKind::Interface,
383            "enum_declaration" => SymbolKind::Enum,
384            "method_declaration" | "constructor_declaration" => SymbolKind::Method,
385            _ => return Vec::new(),
386        };
387
388        vec![Export {
389            name,
390            kind,
391            line: node.start_position().row + 1,
392        }]
393    }
394
395    fn scope_creating_kinds(&self) -> &'static [&'static str] {
396        &[
397            "for_statement",
398            "enhanced_for_statement",
399            "while_statement",
400            "do_statement",
401            "try_statement",
402            "catch_clause",
403            "switch_expression",
404            "block",
405        ]
406    }
407
408    fn control_flow_kinds(&self) -> &'static [&'static str] {
409        &[
410            "if_statement",
411            "for_statement",
412            "enhanced_for_statement",
413            "while_statement",
414            "do_statement",
415            "switch_expression",
416            "try_statement",
417            "return_statement",
418            "break_statement",
419            "continue_statement",
420            "throw_statement",
421        ]
422    }
423
424    fn complexity_nodes(&self) -> &'static [&'static str] {
425        &[
426            "if_statement",
427            "for_statement",
428            "enhanced_for_statement",
429            "while_statement",
430            "do_statement",
431            "switch_label",
432            "catch_clause",
433            "ternary_expression",
434            "binary_expression",
435        ]
436    }
437
438    fn nesting_nodes(&self) -> &'static [&'static str] {
439        &[
440            "if_statement",
441            "for_statement",
442            "enhanced_for_statement",
443            "while_statement",
444            "do_statement",
445            "switch_expression",
446            "try_statement",
447            "method_declaration",
448            "class_declaration",
449        ]
450    }
451
452    fn signature_suffix(&self) -> &'static str {
453        " {}"
454    }
455
456    fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
457        let name = self.node_name(node, content)?;
458        let params = node
459            .child_by_field_name("parameters")
460            .map(|p| content[p.byte_range()].to_string())
461            .unwrap_or_else(|| "()".to_string());
462
463        // Check for @Override annotation
464        let is_override = if let Some(mods) = node.child_by_field_name("modifiers") {
465            let mut cursor = mods.walk();
466            let children: Vec<_> = mods.children(&mut cursor).collect();
467            children.iter().any(|child| {
468                child.kind() == "marker_annotation"
469                    && child
470                        .child(1)
471                        .map(|id| &content[id.byte_range()] == "Override")
472                        .unwrap_or(false)
473            })
474        } else {
475            false
476        };
477
478        Some(Symbol {
479            name: name.to_string(),
480            kind: SymbolKind::Method,
481            signature: format!("{}{}", name, params),
482            docstring: None,
483            attributes: Vec::new(),
484            start_line: node.start_position().row + 1,
485            end_line: node.end_position().row + 1,
486            visibility: self.get_visibility(node, content),
487            children: Vec::new(),
488            is_interface_impl: is_override,
489            implements: Vec::new(),
490        })
491    }
492
493    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
494        let name = self.node_name(node, content)?;
495        let kind = match node.kind() {
496            "interface_declaration" => SymbolKind::Interface,
497            "enum_declaration" => SymbolKind::Enum,
498            _ => SymbolKind::Class,
499        };
500
501        Some(Symbol {
502            name: name.to_string(),
503            kind,
504            signature: format!("{} {}", kind.as_str(), name),
505            docstring: None,
506            attributes: Vec::new(),
507            start_line: node.start_position().row + 1,
508            end_line: node.end_position().row + 1,
509            visibility: self.get_visibility(node, content),
510            children: Vec::new(),
511            is_interface_impl: false,
512            implements: Vec::new(),
513        })
514    }
515
516    fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
517        self.extract_container(node, content)
518    }
519
520    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
521        // Javadoc comments could be extracted but need special handling
522        None
523    }
524
525    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
526        Vec::new()
527    }
528
529    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
530        if node.kind() != "import_declaration" {
531            return Vec::new();
532        }
533
534        let line = node.start_position().row + 1;
535        let text = &content[node.byte_range()];
536
537        // Extract import path
538        let is_static = text.contains("static ");
539        let is_wildcard = text.contains(".*");
540
541        // Get the scoped_identifier
542        let mut cursor = node.walk();
543        for child in node.children(&mut cursor) {
544            if child.kind() == "scoped_identifier" || child.kind() == "identifier" {
545                let module = content[child.byte_range()].to_string();
546                return vec![Import {
547                    module,
548                    names: Vec::new(),
549                    alias: if is_static {
550                        Some("static".to_string())
551                    } else {
552                        None
553                    },
554                    is_wildcard,
555                    is_relative: false,
556                    line,
557                }];
558            }
559        }
560
561        Vec::new()
562    }
563
564    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
565        // Java: import pkg.Class; or import pkg.*;
566        if import.is_wildcard {
567            format!("import {}.*;", import.module)
568        } else {
569            format!("import {};", import.module)
570        }
571    }
572
573    fn is_public(&self, node: &Node, content: &str) -> bool {
574        self.get_visibility(node, content) == Visibility::Public
575    }
576
577    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
578        let has_test_attr = symbol.attributes.iter().any(|a| a.contains("@Test"));
579        if has_test_attr {
580            return true;
581        }
582        match symbol.kind {
583            crate::SymbolKind::Class => {
584                symbol.name.starts_with("Test") || symbol.name.ends_with("Test")
585            }
586            _ => false,
587        }
588    }
589
590    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
591        None
592    }
593
594    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
595        node.child_by_field_name("body")
596    }
597
598    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
599        false
600    }
601
602    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
603        let name_node = node.child_by_field_name("name")?;
604        Some(&content[name_node.byte_range()])
605    }
606
607    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
608        if path.extension()?.to_str()? != "java" {
609            return None;
610        }
611        // Java: com/foo/Bar.java -> com.foo.Bar
612        let path_str = path.to_str()?;
613        // Remove common source prefixes
614        let rel = path_str
615            .strip_prefix("src/main/java/")
616            .or_else(|| path_str.strip_prefix("src/"))
617            .unwrap_or(path_str);
618        let without_ext = rel.strip_suffix(".java")?;
619        Some(without_ext.replace('/', "."))
620    }
621
622    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
623        let path = module.replace('.', "/");
624        vec![
625            format!("src/main/java/{}.java", path),
626            format!("src/{}.java", path),
627        ]
628    }
629
630    fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
631        import_name.starts_with("java.") || import_name.starts_with("javax.")
632    }
633
634    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
635        // Java stdlib is in rt.jar/modules, not easily indexable
636        None
637    }
638
639    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
640        let mut cursor = node.walk();
641        for child in node.children(&mut cursor) {
642            if child.kind() == "modifiers" {
643                let mods = &content[child.byte_range()];
644                if mods.contains("private") {
645                    return Visibility::Private;
646                }
647                if mods.contains("protected") {
648                    return Visibility::Protected;
649                }
650                // public or no modifier = visible in skeleton
651                return Visibility::Public;
652            }
653        }
654        // No modifier = package-private, but still visible for skeleton purposes
655        Visibility::Public
656    }
657
658    // === Import Resolution ===
659
660    fn lang_key(&self) -> &'static str {
661        "java"
662    }
663
664    fn resolve_local_import(
665        &self,
666        import: &str,
667        current_file: &Path,
668        project_root: &Path,
669    ) -> Option<PathBuf> {
670        // Convert import to path: com.foo.Bar -> com/foo/Bar.java
671        let path_part = import.replace('.', "/");
672
673        // Common Java source directories
674        let source_dirs = [
675            "src/main/java",
676            "src/java",
677            "src",
678            "app/src/main/java", // Android
679        ];
680
681        for src_dir in &source_dirs {
682            let source_path = project_root
683                .join(src_dir)
684                .join(format!("{}.java", path_part));
685            if source_path.is_file() {
686                return Some(source_path);
687            }
688        }
689
690        // Also try relative to current file's package structure
691        // Find the source root by walking up from current file
692        let mut current = current_file.parent()?;
693        while current != project_root {
694            // Check if this might be a source root
695            let potential = current.join(format!("{}.java", path_part));
696            if potential.is_file() {
697                return Some(potential);
698            }
699            current = current.parent()?;
700        }
701
702        None
703    }
704
705    fn resolve_external_import(
706        &self,
707        import_name: &str,
708        _project_root: &Path,
709    ) -> Option<ResolvedPackage> {
710        let maven_repo = find_maven_repository();
711        let gradle_cache = find_gradle_cache();
712
713        resolve_java_import(import_name, maven_repo.as_deref(), gradle_cache.as_deref())
714    }
715
716    fn get_version(&self, _project_root: &Path) -> Option<String> {
717        get_java_version()
718    }
719
720    fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
721        find_maven_repository().or_else(find_gradle_cache)
722    }
723
724    fn indexable_extensions(&self) -> &'static [&'static str] {
725        &["java"]
726    }
727
728    fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
729        use crate::{PackageSource, PackageSourceKind};
730        let mut sources = Vec::new();
731        if let Some(maven) = find_maven_repository() {
732            sources.push(PackageSource {
733                name: "maven",
734                path: maven,
735                kind: PackageSourceKind::Maven,
736                version_specific: false,
737            });
738        }
739        if let Some(gradle) = find_gradle_cache() {
740            sources.push(PackageSource {
741                name: "gradle",
742                path: gradle,
743                kind: PackageSourceKind::Gradle,
744                version_specific: false,
745            });
746        }
747        sources
748    }
749
750    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
751        use crate::traits::{has_extension, skip_dotfiles};
752        if skip_dotfiles(name) {
753            return true;
754        }
755        // Skip META-INF, test dirs
756        if is_dir && (name == "META-INF" || name == "test" || name == "tests") {
757            return true;
758        }
759        !is_dir && !has_extension(name, self.indexable_extensions())
760    }
761
762    fn discover_packages(&self, source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
763        match source.kind {
764            crate::PackageSourceKind::Maven => discover_maven_packages(&source.path, &source.path),
765            crate::PackageSourceKind::Gradle => {
766                discover_gradle_packages(&source.path, &source.path)
767            }
768            _ => Vec::new(),
769        }
770    }
771
772    fn package_module_name(&self, entry_name: &str) -> String {
773        entry_name
774            .strip_suffix(".java")
775            .unwrap_or(entry_name)
776            .to_string()
777    }
778
779    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
780        if path.is_file() {
781            return Some(path.to_path_buf());
782        }
783        // For JAR files, return the JAR itself
784        if path.extension().map(|e| e == "jar").unwrap_or(false) {
785            return Some(path.to_path_buf());
786        }
787        None
788    }
789}
790
791/// Check if a directory contains JAR files (indicates a version directory).
792fn has_jar_files(path: &Path) -> bool {
793    std::fs::read_dir(path)
794        .into_iter()
795        .flatten()
796        .flatten()
797        .any(|e| e.file_name().to_string_lossy().ends_with(".jar"))
798}
799
800/// Find the main JAR in a Maven version directory.
801fn find_maven_jar(version_dir: &Path, artifact: &str) -> Option<PathBuf> {
802    // Prefer sources JAR
803    let entries: Vec<_> = std::fs::read_dir(version_dir).ok()?.flatten().collect();
804
805    // Look for artifact-version-sources.jar first
806    for entry in &entries {
807        let name = entry.file_name().to_string_lossy().to_string();
808        if name.starts_with(artifact) && name.ends_with("-sources.jar") {
809            return Some(entry.path());
810        }
811    }
812
813    // Fall back to regular jar
814    for entry in &entries {
815        let name = entry.file_name().to_string_lossy().to_string();
816        if name.starts_with(artifact)
817            && name.ends_with(".jar")
818            && !name.ends_with("-sources.jar")
819            && !name.ends_with("-javadoc.jar")
820        {
821            return Some(entry.path());
822        }
823    }
824
825    None
826}
827
828/// Discover packages in Maven repository structure.
829fn discover_maven_packages(maven_repo: &Path, current: &Path) -> Vec<(String, PathBuf)> {
830    let entries = match std::fs::read_dir(current) {
831        Ok(e) => e,
832        Err(_) => return Vec::new(),
833    };
834
835    let mut packages = Vec::new();
836
837    for entry in entries.flatten() {
838        let path = entry.path();
839
840        if path.is_dir() {
841            if has_jar_files(&path) {
842                // This is a version directory - parent is artifact, grandparent path is group
843                if let Some(artifact_dir) = current.parent() {
844                    let artifact = current.file_name().unwrap_or_default().to_string_lossy();
845                    if let Ok(group_path) = artifact_dir.strip_prefix(maven_repo) {
846                        let group = group_path.to_string_lossy().replace('/', ".");
847                        let pkg_name = format!("{}:{}", group, artifact);
848
849                        if let Some(jar_path) = find_maven_jar(&path, &artifact) {
850                            packages.push((pkg_name, jar_path));
851                        }
852                    }
853                }
854            } else {
855                packages.extend(discover_maven_packages(maven_repo, &path));
856            }
857        }
858    }
859
860    packages
861}
862
863/// Discover packages in Gradle cache structure.
864fn discover_gradle_packages(gradle_cache: &Path, current: &Path) -> Vec<(String, PathBuf)> {
865    let entries = match std::fs::read_dir(current) {
866        Ok(e) => e,
867        Err(_) => return Vec::new(),
868    };
869
870    let mut packages = Vec::new();
871
872    for entry in entries.flatten() {
873        let path = entry.path();
874
875        if path.is_dir() {
876            let name = entry.file_name().to_string_lossy().to_string();
877            // Check if this is a hash directory (40 hex chars)
878            if name.len() == 40 && name.chars().all(|c| c.is_ascii_hexdigit()) {
879                // This is a hash dir, find JAR
880                if let Ok(files) = std::fs::read_dir(&path) {
881                    for file in files.flatten() {
882                        let file_name = file.file_name().to_string_lossy().to_string();
883                        if file_name.ends_with(".jar")
884                            && !file_name.ends_with("-sources.jar")
885                            && !file_name.ends_with("-javadoc.jar")
886                        {
887                            // Extract package info from path
888                            if let Ok(rel) = current.strip_prefix(gradle_cache) {
889                                let parts: Vec<_> = rel.components().collect();
890                                if parts.len() >= 2 {
891                                    let group = parts[..parts.len() - 1]
892                                        .iter()
893                                        .map(|c| c.as_os_str().to_string_lossy())
894                                        .collect::<Vec<_>>()
895                                        .join(".");
896                                    let artifact =
897                                        parts.last().unwrap().as_os_str().to_string_lossy();
898                                    let pkg_name = format!("{}:{}", group, artifact);
899                                    packages.push((pkg_name, file.path()));
900                                }
901                            }
902                        }
903                    }
904                }
905            } else {
906                packages.extend(discover_gradle_packages(gradle_cache, &path));
907            }
908        }
909    }
910
911    packages
912}
913
914#[cfg(test)]
915mod tests {
916    use super::*;
917    use crate::validate_unused_kinds_audit;
918
919    /// Documents node kinds that exist in the Java grammar but aren't used in trait methods.
920    /// Run `cross_check_node_kinds` in registry.rs to see all potentially useful kinds.
921    #[test]
922    fn unused_node_kinds_audit() {
923        #[rustfmt::skip]
924        let documented_unused: &[&str] = &[
925            // STRUCTURAL
926            "block_comment",           // comments
927            "class_body",              // class body
928            "class_literal",           // Foo.class
929            "constructor_body",        // constructor body
930            "enum_body",               // enum body
931            "enum_body_declarations",  // enum body decls
932            "enum_constant",           // enum value
933            "field_declaration",       // field decl
934            "formal_parameter",        // method param
935            "formal_parameters",       // param list
936            "identifier",              // too common
937            "interface_body",          // interface body
938            "modifiers",               // access modifiers
939            "scoped_identifier",       // pkg.Class
940            "scoped_type_identifier",  // pkg.Type
941            "superclass",              // extends
942            "super_interfaces",        // implements
943            "type_identifier",         // type name
944
945            // CLAUSE
946            "catch_formal_parameter",  // catch param
947            "catch_type",              // catch type
948            "extends_interfaces",      // extends for interfaces
949            "finally_clause",          // finally block
950            "switch_block",            // switch body
951            "switch_block_statement_group", // case group
952            "throws",                  // throws clause
953
954            // EXPRESSION
955            "array_creation_expression", // new T[]
956            "assignment_expression",   // x = y
957            "cast_expression",         // (T)x
958            "instanceof_expression",   // x instanceof T
959            "lambda_expression",       // x -> y
960            "method_invocation",       // obj.method()
961            "method_reference",        // Class::method
962            "object_creation_expression", // new Foo()
963            "parenthesized_expression",// (expr)
964            "template_expression",     // string template
965            "unary_expression",        // -x, !x
966            "update_expression",       // x++
967            "yield_statement",         // yield x
968
969            // TYPE
970            "annotated_type",          // @Ann Type
971            "array_type",              // T[]
972            "boolean_type",            // boolean
973            "floating_point_type",     // float, double
974            "generic_type",            // T<U>
975            "integral_type",           // int, long
976            "type_arguments",          // <T, U>
977            "type_bound",              // T extends X
978            "type_list",               // T, U, V
979            "type_parameter",          // T
980            "type_parameters",         // <T, U>
981            "type_pattern",            // type pattern
982            "void_type",               // void
983
984            // DECLARATION
985            "annotation_type_body",    // @interface body
986            "annotation_type_declaration", // @interface
987            "annotation_type_element_declaration", // @interface element
988            "assert_statement",        // assert
989            "compact_constructor_declaration", // record constructor
990            "constant_declaration",    // const decl
991            "explicit_constructor_invocation", // this(), super()
992            "expression_statement",    // expr;
993            "labeled_statement",       // label: stmt
994            "local_variable_declaration", // local var
995            "record_declaration",      // record
996            "record_pattern_body",     // record pattern
997
998            // MODULE
999            "exports_module_directive",// exports
1000            "module_body",             // module body
1001            "module_declaration",      // module
1002            "opens_module_directive",  // opens
1003            "package_declaration",     // package
1004            "provides_module_directive", // provides
1005            "requires_modifier",       // requires modifier
1006            "requires_module_directive", // requires
1007            "uses_module_directive",   // uses
1008
1009            // OTHER
1010            "resource_specification", // try-with-resources
1011            "synchronized_statement",  // synchronized
1012            "try_with_resources_statement", // try-with
1013        ];
1014
1015        validate_unused_kinds_audit(&Java, documented_unused)
1016            .expect("Java unused node kinds audit failed");
1017    }
1018}