1use 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
9pub 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 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
37pub fn find_maven_repository() -> Option<PathBuf> {
39 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 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 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
66pub fn find_gradle_cache() -> Option<PathBuf> {
68 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 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 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
106fn 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 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 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 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 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 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 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 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
311pub 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 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 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 let is_static = text.contains("static ");
539 let is_wildcard = text.contains(".*");
540
541 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 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 let path_str = path.to_str()?;
613 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 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 return Visibility::Public;
652 }
653 }
654 Visibility::Public
656 }
657
658 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 let path_part = import.replace('.', "/");
672
673 let source_dirs = [
675 "src/main/java",
676 "src/java",
677 "src",
678 "app/src/main/java", ];
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 let mut current = current_file.parent()?;
693 while current != project_root {
694 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 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 if path.extension().map(|e| e == "jar").unwrap_or(false) {
785 return Some(path.to_path_buf());
786 }
787 None
788 }
789}
790
791fn 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
800fn find_maven_jar(version_dir: &Path, artifact: &str) -> Option<PathBuf> {
802 let entries: Vec<_> = std::fs::read_dir(version_dir).ok()?.flatten().collect();
804
805 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 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
828fn 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 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
863fn 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 if name.len() == 40 && name.chars().all(|c| c.is_ascii_hexdigit()) {
879 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 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 #[test]
922 fn unused_node_kinds_audit() {
923 #[rustfmt::skip]
924 let documented_unused: &[&str] = &[
925 "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", "superclass", "super_interfaces", "type_identifier", "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_invocation", "method_reference", "object_creation_expression", "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_list", "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", ];
1014
1015 validate_unused_kinds_audit(&Java, documented_unused)
1016 .expect("Java unused node kinds audit failed");
1017 }
1018}