diff --git a/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs b/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs index 55bdac5..52d7051 100644 --- a/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs +++ b/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs @@ -1,4 +1,4 @@ -use crate::graph::{DependencyGraph, GraphError}; +use crate::graph::{infer_source_attachment, DependencyGraph, GraphError}; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; @@ -195,13 +195,12 @@ impl ComputedClasspath { let deps = graph.transitive_deps(target_label)?; let mut entries = Vec::new(); - let mut seen_jars = std::collections::HashSet::new(); + let mut seen_jars: std::collections::HashMap = + std::collections::HashMap::new(); for dep_label in &deps { let dep_is_testonly = is_test_context && graph.is_testonly(dep_label); - // Bazel 6+ uses "@@" canonical labels for external repos. Only skip - // toolchain/platform targets — Maven deps etc. must pass through. if is_bazel_internal_label(dep_label) { continue; } @@ -222,25 +221,34 @@ impl ComputedClasspath { if let Some(jars) = graph.get_target_jars(dep_label) { for jar in jars { - if seen_jars.insert(jar.clone()) { - let source_path = graph.get_target_source_jar(dep_label, jar); - let effective_source = if let Some(ref sp) = source_path { - if std::path::Path::new(sp).exists() { - source_path - } else if is_workspace_internal { - infer_source_attachment(dep_label, workspace_root) - } else { - None + let resolve_source = |jar: &crate::graph::ResolvedJar| { + jar.source_path + .as_ref() + .filter(|p| std::path::Path::new(p).exists()) + .cloned() + .or_else(|| { + if is_workspace_internal { + infer_source_attachment(dep_label, workspace_root) + } else { + None + } + }) + }; + + if let Some(&existing_idx) = seen_jars.get(&jar.classpath_path) { + if entries[existing_idx].source_attachment_path.is_none() { + let source = resolve_source(jar); + if source.is_some() { + entries[existing_idx].source_attachment_path = source; } - } else if is_workspace_internal { - infer_source_attachment(dep_label, workspace_root) - } else { - None - }; + } + } else { + let source = resolve_source(jar); + seen_jars.insert(jar.classpath_path.clone(), entries.len()); entries.push(ClasspathEntry { entry_type: ClasspathEntryType::Library, - path: jar.clone(), - source_attachment_path: effective_source, + path: jar.classpath_path.clone(), + source_attachment_path: source, is_test: dep_is_testonly, is_exported: false, access_rules: Vec::new(), @@ -253,7 +261,7 @@ impl ComputedClasspath { let output_jars = graph .get_target_jars(target_label) - .cloned() + .map(|jars| jars.iter().map(|j| j.classpath_path.clone()).collect()) .unwrap_or_default(); Ok(ComputedClasspath { @@ -277,11 +285,15 @@ impl ComputedClasspath { if let Some(jars) = graph.get_target_jars(target_label) { for jar in jars { - let source_path = graph.get_target_source_jar(target_label, jar); + let source = jar + .source_path + .as_ref() + .filter(|p| std::path::Path::new(p).exists()) + .cloned(); entries.push(ClasspathEntry { entry_type: ClasspathEntryType::Library, - path: jar.clone(), - source_attachment_path: source_path, + path: jar.classpath_path.clone(), + source_attachment_path: source, is_test: false, is_exported: false, access_rules: Vec::new(), @@ -292,7 +304,7 @@ impl ComputedClasspath { let output_jars = graph .get_target_jars(target_label) - .cloned() + .map(|jars| jars.iter().map(|j| j.classpath_path.clone()).collect()) .unwrap_or_default(); Ok(ComputedClasspath { @@ -366,64 +378,6 @@ impl ComputedClasspath { } } -fn pkg_contains_java_content(dir: &std::path::Path) -> bool { - dir.read_dir() - .ok() - .map(|mut entries| { - entries.any(|e| { - e.map(|e| { - let name = e.file_name(); - let name_str = name.to_string_lossy(); - name_str.ends_with(".java") - }) - .unwrap_or(false) - }) - }) - .unwrap_or(false) -} - -fn infer_source_attachment(dep_label: &str, workspace_root: Option<&str>) -> Option { - let ws_root = workspace_root?; - let label = dep_label.strip_prefix("//")?; - let package_path = label.split(':').next().unwrap_or(label); - if package_path.is_empty() { - return None; - } - - let source_root_markers = ["src/main/java", "src/test/java", "src/java", "java"]; - let pkg = std::path::Path::new(ws_root).join(package_path); - - // Probe filesystem: // - for marker in &source_root_markers { - let candidate = pkg.join(marker); - if candidate.is_dir() { - return Some(candidate.to_string_lossy().into_owned()); - } - } - - // Fallback: package directory itself (flat layouts) - if pkg.is_dir() && pkg_contains_java_content(&pkg) { - return Some(pkg.to_string_lossy().into_owned()); - } - - // Substring fallback for labels with embedded source paths - let substring_markers = [ - "src/main/java/", - "src/test/java/", - "src/java/", - "javatests/", - "java/", - ]; - for marker in &substring_markers { - if let Some(idx) = package_path.find(marker) { - let root = &package_path[..idx + marker.len() - 1]; - return Some(format!("{}/{}", ws_root, root)); - } - } - - None -} - /// Returns true for Bazel-internal toolchain/platform targets that should never /// appear on a Java classpath. In Bazel 6+, canonical repo labels use "@@" prefix. /// External dependencies like Maven artifacts (e.g. `@@maven+...//:guava`) must NOT @@ -437,6 +391,7 @@ pub fn is_bazel_internal_label(label: &str) -> bool { #[cfg(test)] mod tests { use super::*; + use crate::graph::{infer_source_attachment, ResolvedJar}; use bazel_aspect::{ArtifactLocation, JarInfo, JavaIdeInfo, TargetIdeInfo}; use std::path::Path; @@ -987,7 +942,10 @@ mod tests { graph.add_target("@@rules_jvm_external~maven~maven//:guava"); graph.set_target_jars( "@@rules_jvm_external~maven~maven//:guava", - vec!["/guava.jar".to_string()], + vec![ResolvedJar { + classpath_path: "/guava.jar".to_string(), + source_path: None, + }], ); graph.label_aliases.insert( "@maven//:guava".to_string(), @@ -996,7 +954,7 @@ mod tests { let jars = graph.get_target_jars("@maven//:guava"); assert!(jars.is_some()); - assert_eq!(jars.unwrap()[0], "/guava.jar"); + assert_eq!(jars.unwrap()[0].classpath_path, "/guava.jar"); } #[test] @@ -1010,7 +968,10 @@ mod tests { graph.add_target("@@rules_jvm_external~maven~maven//:com_google_guava_guava"); graph.set_target_jars( "@@rules_jvm_external~maven~maven//:com_google_guava_guava", - vec!["/guava-33.4.0-jre.jar".to_string()], + vec![ResolvedJar { + classpath_path: "/guava-33.4.0-jre.jar".to_string(), + source_path: None, + }], ); graph.label_aliases.insert( "@maven//:com_google_guava_guava".to_string(), @@ -1151,8 +1112,8 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let guava_jar = tmp.path().join("guava.jar"); let src_jar = tmp.path().join("guava-sources.jar"); - std::fs::File::create(&guava_jar).unwrap(); - std::fs::File::create(&src_jar).unwrap(); + std::fs::write(&guava_jar, [0u8; 2048]).unwrap(); + std::fs::write(&src_jar, [0u8; 2048]).unwrap(); let mut graph = DependencyGraph::new(); let results = vec![ @@ -1302,8 +1263,8 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let guava_jar = tmp.path().join("guava.jar"); let src_jar = tmp.path().join("guava-sources.jar"); - std::fs::File::create(&guava_jar).unwrap(); - std::fs::File::create(&src_jar).unwrap(); + std::fs::write(&guava_jar, [0u8; 2048]).unwrap(); + std::fs::write(&src_jar, [0u8; 2048]).unwrap(); let mut graph = DependencyGraph::new(); let results = vec![ @@ -1461,8 +1422,8 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let binary_jar = tmp.path().join("lib.jar"); let source_jar = tmp.path().join("lib-sources.jar"); - std::fs::File::create(&binary_jar).unwrap(); - std::fs::File::create(&source_jar).unwrap(); + std::fs::write(&binary_jar, [0u8; 2048]).unwrap(); + std::fs::write(&source_jar, [0u8; 2048]).unwrap(); let mut graph = DependencyGraph::new(); let results = vec![ @@ -1562,4 +1523,86 @@ mod tests { "Phantom source JAR for workspace-internal dep should fall back to infer_source_attachment" ); } + + #[test] + fn test_duplicate_jar_merges_source_from_later_target() { + let tmp = tempfile::tempdir().unwrap(); + let guava_jar = tmp.path().join("guava.jar"); + let src_jar = tmp.path().join("guava-sources.jar"); + std::fs::write(&guava_jar, [0u8; 2048]).unwrap(); + std::fs::write(&src_jar, [0u8; 2048]).unwrap(); + + let guava_path = guava_jar.to_str().unwrap(); + let src_path = src_jar.to_str().unwrap(); + + let mut graph = DependencyGraph::new(); + let results = vec![ + make_target("//app:app", vec!["//3rdparty:guava"], vec!["/app.jar"]), + make_target_with_jar_path("//3rdparty:guava", vec!["@maven//:guava"], guava_path), + make_target_with_source_jar("@maven//:guava", vec![], guava_path, src_path), + ]; + graph.populate_from_aspects(&results, Path::new(tmp.path())); + + let ws = tmp.path().to_string_lossy().into_owned(); + let cp = + ComputedClasspath::compute_for(&graph, "//app:app", TargetKind::JavaLibrary, Some(&ws)) + .unwrap(); + + let jar_entries: Vec<_> = cp.entries.iter().filter(|e| e.path == guava_path).collect(); + assert_eq!( + jar_entries.len(), + 1, + "Duplicate JAR should appear only once" + ); + assert_eq!( + jar_entries[0].source_attachment_path, + Some(src_path.to_string()), + "Source from later @maven// target should be merged into entry from //3rdparty: wrapper" + ); + } + + #[test] + fn test_duplicate_jar_preserves_first_valid_source() { + let tmp = tempfile::tempdir().unwrap(); + let guava_jar = tmp.path().join("guava.jar"); + let src1_jar = tmp.path().join("src1-sources.jar"); + let src2_jar = tmp.path().join("src2-sources.jar"); + std::fs::write(&guava_jar, [0u8; 2048]).unwrap(); + std::fs::write(&src1_jar, [0u8; 2048]).unwrap(); + std::fs::write(&src2_jar, [0u8; 2048]).unwrap(); + + let guava_path = guava_jar.to_str().unwrap(); + let src1_path = src1_jar.to_str().unwrap(); + let src2_path = src2_jar.to_str().unwrap(); + + let mut graph = DependencyGraph::new(); + let results = vec![ + make_target("//app:app", vec!["@repo_a//:guava"], vec!["/app.jar"]), + make_target_with_source_jar( + "@repo_a//:guava", + vec!["@repo_b//:guava"], + guava_path, + src1_path, + ), + make_target_with_source_jar("@repo_b//:guava", vec![], guava_path, src2_path), + ]; + graph.populate_from_aspects(&results, Path::new(tmp.path())); + + let ws = tmp.path().to_string_lossy().into_owned(); + let cp = + ComputedClasspath::compute_for(&graph, "//app:app", TargetKind::JavaLibrary, Some(&ws)) + .unwrap(); + + let jar_entries: Vec<_> = cp.entries.iter().filter(|e| e.path == guava_path).collect(); + assert_eq!( + jar_entries.len(), + 1, + "Duplicate JAR should appear only once" + ); + assert_eq!( + jar_entries[0].source_attachment_path, + Some(src1_path.to_string()), + "First valid source should be preserved, not overwritten by later target" + ); + } } diff --git a/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs b/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs index d0c962b..0cca775 100644 --- a/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs +++ b/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs @@ -3,14 +3,19 @@ use petgraph::visit::EdgeRef; use petgraph::Direction; use std::collections::{HashMap, HashSet, VecDeque}; +/// A classpath JAR paired with its resolved source attachment. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedJar { + pub classpath_path: String, + pub source_path: Option, +} + /// Dependency graph of Bazel targets pub struct DependencyGraph { graph: DiGraph, label_to_index: HashMap, - /// JARs associated with each target - target_jars: HashMap>, - /// Source JAR mappings per target: {target_label → {binary_jar_path → source_jar_path}} - target_source_jars: HashMap>, + /// JARs associated with each target, each carrying its resolved source attachment + target_jars: HashMap>, /// Targets that have `testonly = True` in their Bazel rule definition. /// These targets can only be depended on by test targets. testonly_targets: HashSet, @@ -35,7 +40,6 @@ impl DependencyGraph { graph: DiGraph::new(), label_to_index: HashMap::new(), target_jars: HashMap::new(), - target_source_jars: HashMap::new(), testonly_targets: HashSet::new(), label_aliases: HashMap::new(), } @@ -68,8 +72,8 @@ impl DependencyGraph { } } - /// Associate JARs with a target - pub fn set_target_jars(&mut self, label: &str, jars: Vec) { + /// Associate resolved JARs (with source attachments) with a target + pub fn set_target_jars(&mut self, label: &str, jars: Vec) { self.add_target(label); self.target_jars.insert(label.to_string(), jars); } @@ -131,7 +135,7 @@ impl DependencyGraph { } /// Get JARs for a target, resolving through the alias map if needed - pub fn get_target_jars(&self, label: &str) -> Option<&Vec> { + pub fn get_target_jars(&self, label: &str) -> Option<&Vec> { if let Some(jars) = self.target_jars.get(label) { return Some(jars); } @@ -141,22 +145,6 @@ impl DependencyGraph { None } - /// Get the source JAR path for a specific binary JAR of a target. - /// Resolves through label_aliases like get_target_jars(). - pub fn get_target_source_jar(&self, label: &str, binary_jar: &str) -> Option { - if let Some(sources) = self.target_source_jars.get(label) { - if let Some(src) = sources.get(binary_jar) { - return Some(src.clone()); - } - } - if let Some(canonical) = self.label_aliases.get(label) { - if let Some(sources) = self.target_source_jars.get(canonical) { - return sources.get(binary_jar).cloned(); - } - } - None - } - /// Check if a target has `testonly = True`. pub fn is_testonly(&self, label: &str) -> bool { self.testonly_targets.contains(label) @@ -172,7 +160,6 @@ impl DependencyGraph { self.graph = DiGraph::new(); self.label_to_index.clear(); self.target_jars.clear(); - self.target_source_jars.clear(); self.testonly_targets.clear(); self.label_aliases.clear(); } @@ -203,102 +190,39 @@ impl DependencyGraph { } if let Some(ref java_info) = info.java_info { - let mut jars: Vec = java_info + let full_jars: Vec = java_info .jars .iter() - .filter_map(|j| { - let path = j.jar.best_path()?; - Some(if j.jar.is_source { - resolve_external_path(&path, workspace_root).unwrap_or(path) - } else { - path - }) - }) + .filter_map(|j| normalize_artifact_path(&j.jar, workspace_root)) .collect(); let compile_jars: Vec = java_info .compile_jars .iter() - .filter_map(|j| { - let path = j.best_path()?; - Some(if j.is_source { - resolve_external_path(&path, workspace_root).unwrap_or(path) - } else { - path - }) - }) + .filter_map(|j| normalize_artifact_path(j, workspace_root)) .collect(); - // Prefer compile_jars over jars when: - // - jars is empty (java_import targets), OR - // - jars only contains derived non-external artifacts and - // compile_jars is non-empty. This covers 3rdparty wrappers - // (compile_jars = Maven JAR) AND Thrift/Avro/internal targets - // (compile_jars = header JAR that exists on disk). - let jars_all_derived_internal = !jars.is_empty() - && java_info - .jars - .iter() - .all(|j| !j.jar.is_source && !j.jar.is_external); - if jars.is_empty() || (jars_all_derived_internal && !compile_jars.is_empty()) { - jars = compile_jars; - } - - if !jars.is_empty() { - self.set_target_jars(label, jars); - } - - let mut source_map: HashMap = HashMap::new(); - for jar_info in &java_info.jars { - if let (Some(bin_path), Some(src_path)) = ( - jar_info.jar.best_path(), - jar_info.source_jar.as_ref().and_then(|s| s.best_path()), - ) { - let src_is_source = - jar_info.source_jar.as_ref().map_or(true, |s| s.is_source); - let resolved_src = if src_is_source { - resolve_external_path(&src_path, workspace_root).unwrap_or(src_path) - } else { - src_path - }; - source_map.insert(bin_path, resolved_src); - } - } - - // When jars is empty (compile_jars fallback for java_import targets), - // match source_jars to compile_jars by parent directory (Maven coordinates). - if source_map.is_empty() && !java_info.source_jars.is_empty() { - let source_by_dir: HashMap = java_info - .source_jars - .iter() - .filter_map(|s| { - let p = s.best_path()?; - let resolved = if s.is_source { - resolve_external_path(&p, workspace_root).unwrap_or(p) + let jars = if full_jars.is_empty() { + compile_jars + } else { + full_jars + .into_iter() + .enumerate() + .map(|(i, jar)| { + if std::path::Path::new(&jar).exists() { + jar + } else if i < compile_jars.len() { + compile_jars[i].clone() } else { - p - }; - Some((parent_key(&resolved), resolved)) + jar + } }) - .collect(); - - for bin_jar in self.target_jars.get(label).into_iter().flatten() { - if let Some(src_path) = source_by_dir.get(&parent_key(bin_jar)) { - source_map.insert(bin_jar.clone(), src_path.clone()); - } - } - } + .collect() + }; - if !source_map.is_empty() { - log::debug!( - "[bazel-jdt] source_map for '{}': {} entries", - label, - source_map.len() - ); - for (bin, src) in &source_map { - log::trace!("[bazel-jdt] {} -> {}", bin, src); - } - self.target_source_jars.insert(label.clone(), source_map); + if !jars.is_empty() { + let resolved = build_resolved_jars(java_info, &jars, workspace_root, label); + self.set_target_jars(label, resolved); } let pkg = package_of(label); @@ -420,7 +344,6 @@ impl DependencyGraph { self.graph.remove_node(idx); } self.target_jars.remove(label); - self.target_source_jars.remove(label); self.testonly_targets.remove(label); removed.push(label.clone()); } @@ -660,6 +583,317 @@ fn parent_key(path: &str) -> String { .unwrap_or_default() } +fn normalize_artifact_path( + artifact: &bazel_aspect::ArtifactLocation, + workspace_root: &std::path::Path, +) -> Option { + let path = artifact.best_path()?; + Some(if artifact.is_source { + resolve_external_path(&path, workspace_root).unwrap_or(path) + } else { + path + }) +} + +/// Build `Vec` using a 6-strategy source resolution chain. +fn build_resolved_jars( + java_info: &bazel_aspect::JavaIdeInfo, + classpath_jars: &[String], + workspace_root: &std::path::Path, + label: &str, +) -> Vec { + let mut class_to_source: HashMap = HashMap::new(); + let mut interface_to_source: HashMap = HashMap::new(); + let mut all_source_jars: HashSet = HashSet::new(); + + for jar_info in &java_info.jars { + if let Some(src_loc) = jar_info.source_jar.as_ref() { + if let Some(src_path) = src_loc.best_path() { + let resolved_src = if src_loc.is_source { + resolve_external_path(&src_path, workspace_root).unwrap_or(src_path) + } else { + absolutize_source_path(&src_path, workspace_root) + }; + all_source_jars.insert(resolved_src.clone()); + + if let Some(key) = normalize_artifact_path(&jar_info.jar, workspace_root) { + class_to_source.insert(key, resolved_src.clone()); + } + if let Some(iface_loc) = jar_info.interface_jar.as_ref() { + if let Some(key) = normalize_artifact_path(iface_loc, workspace_root) { + interface_to_source.insert(key, resolved_src.clone()); + } + } + } + } + } + + let source_by_dir: HashMap = java_info + .source_jars + .iter() + .filter_map(|s| { + let p = s.best_path()?; + let resolved = if s.is_source { + resolve_external_path(&p, workspace_root).unwrap_or(p) + } else { + absolutize_source_path(&p, workspace_root) + }; + all_source_jars.insert(resolved.clone()); + Some((parent_key(&resolved), resolved)) + }) + .collect(); + + let single_source = if all_source_jars.len() == 1 && classpath_jars.len() == 1 { + all_source_jars.into_iter().next() + } else { + None + }; + + let ws_str = workspace_root.to_str().unwrap_or(""); + + let mut stubs_discarded: u32 = 0; + let mut maven_cache_hits: u32 = 0; + let mut with_source: u32 = 0; + + let result = classpath_jars + .iter() + .map(|jar_path| { + let mut strategy = ""; + let source_path = class_to_source + .get(jar_path) + .cloned() + .and_then(|p| validate_source_jar(&p)) + .map(|p| { + strategy = "class_to_source"; + p + }) + .or_else(|| { + interface_to_source + .get(jar_path) + .cloned() + .and_then(|p| validate_source_jar(&p)) + .map(|p| { + strategy = "interface_to_source"; + p + }) + }) + .or_else(|| { + source_by_dir + .get(&parent_key(jar_path)) + .cloned() + .and_then(|p| validate_source_jar(&p)) + .map(|p| { + strategy = "source_by_dir"; + p + }) + }) + .or_else(|| { + single_source + .clone() + .and_then(|p| validate_source_jar(&p)) + .map(|p| { + strategy = "single_source"; + p + }) + }) + .or_else(|| { + if !label.starts_with('@') { + infer_source_attachment(label, Some(ws_str)).map(|p| { + strategy = "infer_source"; + p + }) + } else { + None + } + }) + .or_else(|| { + probe_source_jar_by_convention(jar_path) + .and_then(|p| validate_source_jar(&p)) + .map(|p| { + strategy = "convention_probe"; + p + }) + }) + .or_else(|| { + let (g, a, v) = extract_maven_coordinates(jar_path)?; + let result = probe_maven_local_cache(&g, &a, &v); + if result.is_some() { + strategy = "maven_cache"; + maven_cache_hits += 1; + } + result + }); + + if let Some(ref src) = source_path { + log::info!( + "[bazel-jdt] {} -> source via {}: {}", + jar_path, + strategy, + src + ); + } + + if source_path.is_none() + && class_to_source.contains_key(jar_path) + && validate_source_jar(class_to_source.get(jar_path).unwrap()).is_none() + { + stubs_discarded += 1; + } + + if source_path.is_some() { + with_source += 1; + } + + ResolvedJar { + classpath_path: jar_path.clone(), + source_path, + } + }) + .collect(); + + log::info!( + "[bazel-jdt] Source resolution for '{}': {}/{} with source, {} stubs discarded, {} maven cache hits", + label, + with_source, + classpath_jars.len(), + stubs_discarded, + maven_cache_hits + ); + + result +} + +fn validate_source_jar(path: &str) -> Option { + match std::fs::metadata(path) { + Ok(meta) if meta.len() < 1024 => None, + Ok(_) => Some(path.to_string()), + Err(_) => None, + } +} + +fn extract_maven_coordinates(jar_path: &str) -> Option<(String, String, String)> { + let maven_idx = jar_path + .rfind("/maven2/") + .map(|i| i + 8) + .or_else(|| jar_path.rfind("/maven/").map(|i| i + 7))?; + let after_maven = &jar_path[maven_idx..]; + + let parts: Vec<&str> = after_maven.split('/').collect(); + if parts.len() < 4 { + return None; + } + + let filename = *parts.last()?; + if !filename.ends_with(".jar") { + return None; + } + + let version = parts[parts.len() - 2]; + let artifact_id = parts[parts.len() - 3]; + let group_parts = &parts[..parts.len() - 3]; + if group_parts.is_empty() { + return None; + } + let group_id = group_parts.join("."); + + Some((group_id, artifact_id.to_string(), version.to_string())) +} + +fn probe_maven_local_cache(group_id: &str, artifact_id: &str, version: &str) -> Option { + let home = std::env::var("HOME").ok()?; + let group_path = group_id.replace('.', "/"); + let candidate = std::path::PathBuf::from(&home) + .join(".m2/repository") + .join(&group_path) + .join(artifact_id) + .join(version) + .join(format!("{}-{}-sources.jar", artifact_id, version)); + if candidate.exists() { + Some(candidate.to_string_lossy().into_owned()) + } else { + None + } +} + +fn absolutize_source_path(path: &str, workspace_root: &std::path::Path) -> String { + if path.starts_with('/') { + return path.to_string(); + } + if path.starts_with("bazel-out/") { + return workspace_root.join(path).to_string_lossy().into_owned(); + } + path.to_string() +} + +fn probe_source_jar_by_convention(jar_path: &str) -> Option { + let path = std::path::Path::new(jar_path); + let stem = path.file_stem()?.to_str()?; + let parent = path.parent()?; + let candidate = parent.join(format!("{}-sources.jar", stem)); + if candidate.exists() { + return Some(candidate.to_string_lossy().into_owned()); + } + None +} + +fn pkg_contains_java_content(dir: &std::path::Path) -> bool { + dir.read_dir() + .ok() + .map(|mut entries| { + entries.any(|e| { + e.map(|e| { + let name = e.file_name(); + let name_str = name.to_string_lossy(); + name_str.ends_with(".java") + }) + .unwrap_or(false) + }) + }) + .unwrap_or(false) +} + +pub(crate) fn infer_source_attachment( + dep_label: &str, + workspace_root: Option<&str>, +) -> Option { + let ws_root = workspace_root?; + let label = dep_label.strip_prefix("//")?; + let package_path = label.split(':').next().unwrap_or(label); + if package_path.is_empty() { + return None; + } + + let source_root_markers = ["src/main/java", "src/test/java", "src/java", "java"]; + let pkg = std::path::Path::new(ws_root).join(package_path); + + for marker in &source_root_markers { + let candidate = pkg.join(marker); + if candidate.is_dir() { + return Some(candidate.to_string_lossy().into_owned()); + } + } + + if pkg.is_dir() && pkg_contains_java_content(&pkg) { + return Some(pkg.to_string_lossy().into_owned()); + } + + let substring_markers = [ + "src/main/java/", + "src/test/java/", + "src/java/", + "javatests/", + "java/", + ]; + for marker in &substring_markers { + if let Some(idx) = package_path.find(marker) { + let root = &package_path[..idx + marker.len() - 1]; + return Some(format!("{}/{}", ws_root, root)); + } + } + + None +} + impl Default for DependencyGraph { fn default() -> Self { Self::new() @@ -823,7 +1057,7 @@ mod tests { assert_eq!(graph.target_count(), 1); let jars = graph.get_target_jars("//foo:lib").unwrap(); - assert_eq!(jars[0], "/second.jar"); + assert_eq!(jars[0].classpath_path, "/second.jar"); } #[test] @@ -1020,13 +1254,17 @@ mod tests { ); let jar_list = jars.unwrap(); assert_eq!(jar_list.len(), 1); - assert_eq!(jar_list[0], "/guava.jar"); + assert_eq!(jar_list[0].classpath_path, "/guava.jar"); } #[test] - fn test_populate_from_aspects_source_jars_kept_over_compile_jars() { + fn test_populate_from_aspects_full_jars_preferred_over_compile_jars() { let mut graph = DependencyGraph::new(); + let tmp_dir = std::env::temp_dir(); + let full_jar_path = tmp_dir.join("test_full_jar_preferred.jar"); + std::fs::write(&full_jar_path, b"").unwrap(); + let target = TargetIdeInfo { label: "//lib:mylib".to_string(), kind: "java_library".to_string(), @@ -1035,7 +1273,7 @@ mod tests { jars: vec![JarInfo { jar: ArtifactLocation { is_source: true, - absolute_path: Some("/output.jar".to_string()), + absolute_path: Some(full_jar_path.to_string_lossy().into_owned()), ..Default::default() }, ..Default::default() @@ -1056,15 +1294,21 @@ mod tests { let jars = graph.get_target_jars("//lib:mylib").unwrap(); assert_eq!(jars.len(), 1); assert_eq!( - jars[0], "/output.jar", - "Source jars should be kept over compile_jars" + jars[0].classpath_path, + full_jar_path.to_string_lossy(), + "Full JARs should be preferred over compile_jars when they exist on disk" ); + let _ = std::fs::remove_file(&full_jar_path); } #[test] - fn test_compile_jars_preferred_for_derived_internal_targets() { + fn test_populate_from_aspects_derived_internal_jars_preferred_over_compile_jars() { let mut graph = DependencyGraph::new(); + let tmp_dir = std::env::temp_dir(); + let full_jar_path = tmp_dir.join("test_derived_internal.jar"); + std::fs::write(&full_jar_path, b"").unwrap(); + let target = TargetIdeInfo { label: "//thrift:service".to_string(), kind: "java_library".to_string(), @@ -1074,14 +1318,12 @@ mod tests { jar: ArtifactLocation { is_source: false, is_external: false, - absolute_path: Some("/libservice.jar".to_string()), + absolute_path: Some(full_jar_path.to_string_lossy().into_owned()), ..Default::default() }, ..Default::default() }], compile_jars: vec![ArtifactLocation { - is_source: false, - is_external: false, absolute_path: Some("/libservice-hjar.jar".to_string()), ..Default::default() }], @@ -1097,9 +1339,11 @@ mod tests { let jars = graph.get_target_jars("//thrift:service").unwrap(); assert_eq!(jars.len(), 1); assert_eq!( - jars[0], "/libservice-hjar.jar", - "compile_jars (hjar) should be preferred for derived internal targets" + jars[0].classpath_path, + full_jar_path.to_string_lossy(), + "Derived internal full JARs should be preferred over header JARs when they exist" ); + let _ = std::fs::remove_file(&full_jar_path); } #[test] @@ -1133,13 +1377,88 @@ mod tests { let jars = graph.get_target_jars("//lib:plain").unwrap(); assert_eq!(jars.len(), 1); assert_eq!( - jars[0], "/libplain.jar", + jars[0].classpath_path, "/libplain.jar", "jars should be kept when compile_jars is empty" ); } + #[test] + fn test_populate_from_aspects_full_jar_missing_falls_back_to_hjar() { + let mut graph = DependencyGraph::new(); + + let target = TargetIdeInfo { + label: "//lib:fallback".to_string(), + kind: "java_library".to_string(), + build_file: None, + java_info: Some(JavaIdeInfo { + jars: vec![JarInfo { + jar: ArtifactLocation { + absolute_path: Some("/nonexistent/libfallback.jar".to_string()), + ..Default::default() + }, + ..Default::default() + }], + compile_jars: vec![ArtifactLocation { + absolute_path: Some("/existing/libfallback-hjar.jar".to_string()), + ..Default::default() + }], + ..Default::default() + }), + deps: vec![], + runtime_deps: Vec::new(), + exports: Vec::new(), + }; + + graph.populate_from_aspects(&[target], Path::new("/workspace")); + + let jars = graph.get_target_jars("//lib:fallback").unwrap(); + assert_eq!(jars.len(), 1); + assert_eq!( + jars[0].classpath_path, "/existing/libfallback-hjar.jar", + "Should fall back to compile_jar (hjar) when full JAR does not exist on disk" + ); + } + + #[test] + fn test_populate_from_aspects_no_jars_no_classpath_entries() { + let mut graph = DependencyGraph::new(); + + let target = TargetIdeInfo { + label: "//lib:empty".to_string(), + kind: "java_library".to_string(), + build_file: None, + java_info: Some(JavaIdeInfo { + jars: vec![], + compile_jars: vec![], + ..Default::default() + }), + deps: vec![], + runtime_deps: Vec::new(), + exports: Vec::new(), + }; + + graph.populate_from_aspects(&[target], Path::new("/workspace")); + + let jars = graph.get_target_jars("//lib:empty"); + assert!( + jars.is_none(), + "Target with no JARs should have no classpath entries" + ); + } + #[test] fn test_populate_from_aspects_extracts_source_jars() { + let tmp = tempfile::tempdir().unwrap(); + let workspace = tmp.path().join("workspace"); + std::fs::create_dir_all(&workspace).unwrap(); + + let output_jar_path = workspace.join("output.jar"); + let output_src_path = workspace.join("output-sources.jar"); + let extra_jar_path = workspace.join("extra.jar"); + std::fs::write(&output_jar_path, [0u8; 2048]).unwrap(); + std::fs::write(&output_src_path, [0u8; 2048]).unwrap(); + std::fs::write(&extra_jar_path, [0u8; 2048]).unwrap(); + let mut graph = DependencyGraph::new(); let target = TargetIdeInfo { label: "//lib:utils".to_string(), @@ -1149,18 +1468,18 @@ mod tests { jars: vec![ JarInfo { jar: ArtifactLocation { - absolute_path: Some("/output.jar".to_string()), + absolute_path: Some(output_jar_path.to_string_lossy().into_owned()), ..Default::default() }, source_jar: Some(ArtifactLocation { - absolute_path: Some("/output-sources.jar".to_string()), + absolute_path: Some(output_src_path.to_string_lossy().into_owned()), ..Default::default() }), ..Default::default() }, JarInfo { jar: ArtifactLocation { - absolute_path: Some("/extra.jar".to_string()), + absolute_path: Some(extra_jar_path.to_string_lossy().into_owned()), ..Default::default() }, source_jar: None, @@ -1174,22 +1493,44 @@ mod tests { exports: Vec::new(), }; - graph.populate_from_aspects(&[target], Path::new("/workspace")); + graph.populate_from_aspects(&[target], &workspace); + let jars = graph.get_target_jars("//lib:utils").unwrap(); + let output_jar = jars + .iter() + .find(|j| j.classpath_path == output_jar_path.to_string_lossy().as_ref()) + .expect("Expected output.jar"); assert_eq!( - graph.get_target_source_jar("//lib:utils", "/output.jar"), - Some("/output-sources.jar".to_string()), - "Expected source JAR mapping for /output.jar" + output_jar.source_path.as_deref(), + Some(output_src_path.to_str().unwrap()), + "Expected source JAR mapping for output.jar" ); + let extra_jar = jars + .iter() + .find(|j| j.classpath_path == extra_jar_path.to_string_lossy().as_ref()) + .expect("Expected extra.jar"); assert_eq!( - graph.get_target_source_jar("//lib:utils", "/extra.jar"), - None, - "Expected no source JAR for /extra.jar (no source_jar in JarInfo)" + extra_jar.source_path, None, + "Expected no source JAR for extra.jar (no source_jar in JarInfo)" ); } #[test] fn test_compile_jars_fallback_matches_source_jars_by_directory() { + let tmp = tempfile::tempdir().unwrap(); + let workspace = tmp.path().join("workspace"); + std::fs::create_dir_all(&workspace).unwrap(); + + let jar_dir = workspace.join("external/maven/guava/33.4.0-jre"); + std::fs::create_dir_all(&jar_dir).unwrap(); + let bin_file = jar_dir.join("processed_guava-33.4.0-jre.jar"); + let src_file = jar_dir.join("guava-33.4.0-jre-sources.jar"); + std::fs::write(&bin_file, [0u8; 2048]).unwrap(); + std::fs::write(&src_file, [0u8; 2048]).unwrap(); + + let bin_path = bin_file.to_string_lossy().into_owned(); + let src_path = src_file.to_string_lossy().into_owned(); + let mut graph = DependencyGraph::new(); let target = TargetIdeInfo { label: "@maven//:guava".to_string(), @@ -1198,16 +1539,11 @@ mod tests { java_info: Some(JavaIdeInfo { jars: vec![], compile_jars: vec![ArtifactLocation { - absolute_path: Some( - "external/maven/guava/33.4.0-jre/processed_guava-33.4.0-jre.jar" - .to_string(), - ), + absolute_path: Some(bin_path.clone()), ..Default::default() }], source_jars: vec![ArtifactLocation { - absolute_path: Some( - "external/maven/guava/33.4.0-jre/guava-33.4.0-jre-sources.jar".to_string(), - ), + absolute_path: Some(src_path.clone()), ..Default::default() }], ..Default::default() @@ -1217,39 +1553,47 @@ mod tests { exports: Vec::new(), }; - graph.populate_from_aspects(&[target], Path::new("/workspace")); + graph.populate_from_aspects(&[target], &workspace); - let bin_path = "external/maven/guava/33.4.0-jre/processed_guava-33.4.0-jre.jar"; - let src_path = "external/maven/guava/33.4.0-jre/guava-33.4.0-jre-sources.jar"; + let jars = graph.get_target_jars("@maven//:guava").unwrap(); + let jar = jars + .iter() + .find(|j| j.classpath_path == bin_path) + .expect("Expected compile_jar entry"); assert_eq!( - graph.get_target_source_jar("@maven//:guava", bin_path), - Some(src_path.to_string()), + jar.source_path, + Some(src_path), "Expected source JAR matched by parent directory for java_import compile_jars fallback" ); } #[test] - fn test_get_target_source_jar_resolves_alias() { + fn test_resolved_jar_source_via_alias() { let mut graph = DependencyGraph::new(); let canonical = "@@rules_jvm_external~maven~maven//:guava"; let apparent = "@maven//:guava"; - let mut source_map = HashMap::new(); - source_map.insert("/guava.jar".to_string(), "/guava-sources.jar".to_string()); - graph - .target_source_jars - .insert(canonical.to_string(), source_map); + graph.add_target(canonical); + graph.set_target_jars( + canonical, + vec![ResolvedJar { + classpath_path: "/guava.jar".to_string(), + source_path: Some("/guava-sources.jar".to_string()), + }], + ); graph .label_aliases .insert(apparent.to_string(), canonical.to_string()); + let jars = graph.get_target_jars(apparent).unwrap(); assert_eq!( - graph.get_target_source_jar(apparent, "/guava.jar"), + jars[0].source_path, Some("/guava-sources.jar".to_string()), "Expected source JAR via alias resolution" ); + let jars2 = graph.get_target_jars(canonical).unwrap(); assert_eq!( - graph.get_target_source_jar(canonical, "/guava.jar"), + jars2[0].source_path, Some("/guava-sources.jar".to_string()), "Expected source JAR via direct canonical label" ); @@ -1381,7 +1725,10 @@ mod tests { graph.get_target_jars("//foo:lib").is_some(), "JAR data should be preserved after deps-only change" ); - assert_eq!(graph.get_target_jars("//foo:lib").unwrap()[0], "/foo.jar"); + assert_eq!( + graph.get_target_jars("//foo:lib").unwrap()[0].classpath_path, + "/foo.jar" + ); assert!(graph.has_target("//baz:new")); } @@ -1425,7 +1772,13 @@ mod tests { graph.populate_from_parsed(&parsed, &workspace_root); assert_eq!(graph.target_count(), 2); - graph.set_target_jars("//foo:old", vec!["/old.jar".to_string()]); + graph.set_target_jars( + "//foo:old", + vec![ResolvedJar { + classpath_path: "/old.jar".to_string(), + source_path: None, + }], + ); let new_parsed = ParsedBuildFile { path: PathBuf::from("/workspace/foo/BUILD"), @@ -1725,11 +2078,12 @@ mod tests { let jars = graph.get_target_jars("//app:app").unwrap(); assert_eq!(jars.len(), 1); - let resolved = &jars[0]; assert!( - resolved.contains("/execroot/external/org_example/jar/downloaded.jar"), + jars[0] + .classpath_path + .contains("/execroot/external/org_example/jar/downloaded.jar"), "Expected JAR resolved to execroot, got: {}", - resolved + jars[0].classpath_path ); } @@ -1765,7 +2119,7 @@ mod tests { let jars = graph.get_target_jars("//lib:foo").unwrap(); assert_eq!(jars.len(), 1); assert_eq!( - jars[0], jar_path, + jars[0].classpath_path, jar_path, "Non-external JAR path should remain unchanged" ); } @@ -1806,7 +2160,7 @@ mod tests { let jars = graph.get_target_jars("//app:app").unwrap(); assert_eq!(jars.len(), 1); assert_eq!( - jars[0], stale_path, + jars[0].classpath_path, stale_path, "Original path should be preserved when resolve_external_path returns None" ); } @@ -1854,13 +2208,15 @@ mod tests { let jars = graph.get_target_jars("//app:app").unwrap(); assert_eq!(jars.len(), 1); assert_eq!( - jars[0], derived_path, + jars[0].classpath_path, derived_path, "Derived artifact path should be preserved without resolve_external_path mangling" ); assert!( - jars[0].contains("bazel-out/k8-fastbuild/bin/external/maven"), - "bazel-out//bin/ prefix must be retained, got: {}", jars[0] + .classpath_path + .contains("bazel-out/k8-fastbuild/bin/external/maven"), + "bazel-out//bin/ prefix must be retained, got: {}", + jars[0].classpath_path ); } @@ -1884,6 +2240,10 @@ mod tests { "{}/bazel-out/k8-fastbuild/bin/external/maven/v1/artifact-1.0-sources.jar", workspace.display() ); + let jar_dir = std::path::Path::new(&bin_path).parent().unwrap(); + std::fs::create_dir_all(jar_dir).unwrap(); + std::fs::write(&bin_path, [0u8; 2048]).unwrap(); + std::fs::write(&src_path, [0u8; 2048]).unwrap(); let mut graph = DependencyGraph::new(); let results = vec![TargetIdeInfo { label: "//app:app".to_string(), @@ -1911,9 +2271,14 @@ mod tests { }]; graph.populate_from_aspects(&results, &workspace); - let resolved_src = graph.get_target_source_jar("//app:app", &bin_path).unwrap(); + let jars = graph.get_target_jars("//app:app").unwrap(); + let jar = jars + .iter() + .find(|j| j.classpath_path == bin_path) + .expect("Expected bin jar"); + let resolved_src = jar.source_path.as_ref().unwrap(); assert_eq!( - resolved_src, src_path, + resolved_src, &src_path, "Derived source attachment should not be resolved via resolve_external_path" ); assert!( @@ -1922,4 +2287,421 @@ mod tests { resolved_src ); } + + // --- absolutize_source_path tests --- + + #[test] + fn test_absolutize_relative_bazel_out_path() { + let ws = std::path::Path::new("/home/user/workspace"); + let result = absolutize_source_path("bazel-out/k8-fastbuild/bin/lib-src.jar", ws); + assert_eq!( + result, + "/home/user/workspace/bazel-out/k8-fastbuild/bin/lib-src.jar" + ); + } + + #[test] + fn test_absolutize_already_absolute_path() { + let ws = std::path::Path::new("/home/user/workspace"); + let result = absolutize_source_path("/absolute/path/to/src.jar", ws); + assert_eq!(result, "/absolute/path/to/src.jar"); + } + + #[test] + fn test_absolutize_non_bazel_out_relative_path() { + let ws = std::path::Path::new("/home/user/workspace"); + let result = absolutize_source_path("external/maven/src.jar", ws); + assert_eq!(result, "external/maven/src.jar"); + } + + #[test] + fn test_populate_absolutizes_derived_source_jar() { + let tmp = tempfile::tempdir().unwrap(); + let workspace = tmp.path().join("workspace"); + std::fs::create_dir_all(&workspace).unwrap(); + + let jar_dir = workspace.join("bazel-out/k8-fastbuild/bin/lib"); + std::fs::create_dir_all(&jar_dir).unwrap(); + std::fs::write(jar_dir.join("liblib.jar"), [0u8; 2048]).unwrap(); + std::fs::write(jar_dir.join("liblib-src.jar"), [0u8; 2048]).unwrap(); + + let mut graph = DependencyGraph::new(); + let results = vec![TargetIdeInfo { + label: "//lib:lib".to_string(), + kind: "java_library".to_string(), + build_file: None, + java_info: Some(JavaIdeInfo { + jars: vec![JarInfo { + jar: ArtifactLocation { + relative_path: Some("lib/liblib.jar".to_string()), + root_path: Some("bazel-out/k8-fastbuild/bin".to_string()), + is_source: false, + ..Default::default() + }, + source_jar: Some(ArtifactLocation { + relative_path: Some("lib/liblib-src.jar".to_string()), + root_path: Some("bazel-out/k8-fastbuild/bin".to_string()), + is_source: false, + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }), + deps: vec![], + runtime_deps: Vec::new(), + exports: Vec::new(), + }]; + graph.populate_from_aspects(&results, &workspace); + + let jars = graph.get_target_jars("//lib:lib").unwrap(); + let jar = &jars[0]; + let src = jar.source_path.as_ref().unwrap(); + assert!( + src.starts_with(workspace.to_str().unwrap()), + "Derived source path should be absolutized with workspace_root, got: {}", + src + ); + assert!( + src.ends_with("lib/liblib-src.jar"), + "Source path should end with the original relative path, got: {}", + src + ); + } + + // --- stub JAR detection tests --- + + #[test] + fn test_stub_source_jar_discarded() { + let tmp = tempfile::tempdir().unwrap(); + let workspace = tmp.path().join("workspace"); + std::fs::create_dir_all(&workspace).unwrap(); + + let stub_jar = workspace.join("stub-sources.jar"); + std::fs::write(&stub_jar, [0u8; 283]).unwrap(); + + let classpath_jar = workspace.join("lib.jar"); + std::fs::write(&classpath_jar, [0u8; 5000]).unwrap(); + + let java_info = JavaIdeInfo { + jars: vec![JarInfo { + jar: ArtifactLocation { + absolute_path: Some(classpath_jar.to_string_lossy().into_owned()), + is_source: false, + ..Default::default() + }, + source_jar: Some(ArtifactLocation { + absolute_path: Some(stub_jar.to_string_lossy().into_owned()), + is_source: false, + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }; + + let classpath_jars = vec![classpath_jar.to_string_lossy().into_owned()]; + let resolved = build_resolved_jars(&java_info, &classpath_jars, &workspace, "//lib:lib"); + + assert_eq!(resolved.len(), 1); + assert!( + resolved[0].source_path.is_none(), + "Stub source JAR (< 1KB) should be discarded, got: {:?}", + resolved[0].source_path + ); + } + + #[test] + fn test_real_source_jar_preserved() { + let tmp = tempfile::tempdir().unwrap(); + let workspace = tmp.path().join("workspace"); + std::fs::create_dir_all(&workspace).unwrap(); + + let source_jar = workspace.join("real-sources.jar"); + std::fs::write(&source_jar, [0u8; 5000]).unwrap(); + + let classpath_jar = workspace.join("lib.jar"); + std::fs::write(&classpath_jar, [0u8; 5000]).unwrap(); + + let java_info = JavaIdeInfo { + jars: vec![JarInfo { + jar: ArtifactLocation { + absolute_path: Some(classpath_jar.to_string_lossy().into_owned()), + is_source: false, + ..Default::default() + }, + source_jar: Some(ArtifactLocation { + absolute_path: Some(source_jar.to_string_lossy().into_owned()), + is_source: false, + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }; + + let classpath_jars = vec![classpath_jar.to_string_lossy().into_owned()]; + let resolved = build_resolved_jars(&java_info, &classpath_jars, &workspace, "//lib:lib"); + + assert_eq!(resolved.len(), 1); + assert_eq!( + resolved[0].source_path.as_deref(), + Some(source_jar.to_str().unwrap()), + "Real source JAR (>= 1KB) should be preserved" + ); + } + + #[test] + fn test_nonexistent_source_jar_filtered() { + let tmp = tempfile::tempdir().unwrap(); + let workspace = tmp.path().join("workspace"); + std::fs::create_dir_all(&workspace).unwrap(); + + let classpath_jar = workspace.join("lib.jar"); + std::fs::write(&classpath_jar, [0u8; 2048]).unwrap(); + let phantom_source = "/nonexistent/path/to/sources.jar".to_string(); + + let java_info = JavaIdeInfo { + jars: vec![JarInfo { + jar: ArtifactLocation { + absolute_path: Some(classpath_jar.to_string_lossy().into_owned()), + is_source: false, + ..Default::default() + }, + source_jar: Some(ArtifactLocation { + absolute_path: Some(phantom_source), + is_source: false, + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }; + + let classpath_jars = vec![classpath_jar.to_string_lossy().into_owned()]; + let resolved = build_resolved_jars(&java_info, &classpath_jars, &workspace, "//lib:lib"); + + assert_eq!(resolved.len(), 1); + assert!( + resolved[0].source_path.is_none(), + "Non-existent source path should be filtered so chain can try later strategies" + ); + } + + // --- validate_source_jar tests --- + + #[test] + fn test_validate_source_jar_stub() { + let tmp = tempfile::tempdir().unwrap(); + let stub = tmp.path().join("stub.jar"); + std::fs::write(&stub, [0u8; 283]).unwrap(); + assert_eq!(validate_source_jar(stub.to_str().unwrap()), None); + } + + #[test] + fn test_validate_source_jar_real() { + let tmp = tempfile::tempdir().unwrap(); + let real = tmp.path().join("real.jar"); + std::fs::write(&real, [0u8; 5000]).unwrap(); + assert_eq!( + validate_source_jar(real.to_str().unwrap()), + Some(real.to_str().unwrap().to_string()) + ); + } + + #[test] + fn test_validate_source_jar_nonexistent() { + let result = validate_source_jar("/nonexistent/path/sources.jar"); + assert_eq!(result, None); + } + + #[test] + fn test_stub_bypassed_maven_cache_used() { + let tmp = tempfile::tempdir().unwrap(); + let workspace = tmp.path().join("workspace"); + std::fs::create_dir_all(&workspace).unwrap(); + + let stub_jar = workspace.join("stub-sources.jar"); + std::fs::write(&stub_jar, [0u8; 283]).unwrap(); + + let m2_path = tmp + .path() + .join(".m2/repository/com/google/guava/guava/28.2-jre"); + std::fs::create_dir_all(&m2_path).unwrap(); + let maven_source = m2_path.join("guava-28.2-jre-sources.jar"); + std::fs::write(&maven_source, [0u8; 5000]).unwrap(); + + let classpath_jar_path = workspace + .join("external/maven/v1/https/repo1.maven.org/maven2/com/google/guava/guava/28.2-jre/guava-28.2-jre.jar"); + std::fs::create_dir_all(classpath_jar_path.parent().unwrap()).unwrap(); + std::fs::write(&classpath_jar_path, [0u8; 5000]).unwrap(); + + let java_info = JavaIdeInfo { + jars: vec![JarInfo { + jar: ArtifactLocation { + absolute_path: Some(classpath_jar_path.to_string_lossy().into_owned()), + is_source: false, + ..Default::default() + }, + source_jar: Some(ArtifactLocation { + absolute_path: Some(stub_jar.to_string_lossy().into_owned()), + is_source: false, + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }; + + std::env::set_var("HOME", tmp.path()); + let classpath_jars = vec![classpath_jar_path.to_string_lossy().into_owned()]; + let resolved = + build_resolved_jars(&java_info, &classpath_jars, &workspace, "@maven//:guava"); + + assert_eq!(resolved.len(), 1); + assert_eq!( + resolved[0].source_path.as_deref(), + Some(maven_source.to_str().unwrap()), + "Should fall through stub to Maven cache source" + ); + } + + #[test] + fn test_stub_bypassed_no_fallback_gives_none() { + let tmp = tempfile::tempdir().unwrap(); + let workspace = tmp.path().join("workspace"); + std::fs::create_dir_all(&workspace).unwrap(); + + let stub_jar = workspace.join("stub-sources.jar"); + std::fs::write(&stub_jar, [0u8; 283]).unwrap(); + + let classpath_jar = workspace.join("lib.jar"); + std::fs::write(&classpath_jar, [0u8; 5000]).unwrap(); + + let java_info = JavaIdeInfo { + jars: vec![JarInfo { + jar: ArtifactLocation { + absolute_path: Some(classpath_jar.to_string_lossy().into_owned()), + is_source: false, + ..Default::default() + }, + source_jar: Some(ArtifactLocation { + absolute_path: Some(stub_jar.to_string_lossy().into_owned()), + is_source: false, + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }; + + std::env::set_var("HOME", tmp.path()); + let classpath_jars = vec![classpath_jar.to_string_lossy().into_owned()]; + let resolved = build_resolved_jars(&java_info, &classpath_jars, &workspace, "//lib:lib"); + + assert_eq!(resolved.len(), 1); + assert!( + resolved[0].source_path.is_none(), + "Stub with no fallback should give None, got: {:?}", + resolved[0].source_path + ); + } + + #[test] + fn test_convention_stub_bypassed_to_maven_cache() { + let tmp = tempfile::tempdir().unwrap(); + let workspace = tmp.path().join("workspace"); + std::fs::create_dir_all(&workspace).unwrap(); + + let maven_jar_dir = workspace + .join("external/maven/v1/https/repo1.maven.org/maven2/com/google/guava/guava/28.2-jre"); + std::fs::create_dir_all(&maven_jar_dir).unwrap(); + let classpath_jar = maven_jar_dir.join("guava-28.2-jre.jar"); + std::fs::write(&classpath_jar, [0u8; 5000]).unwrap(); + let convention_stub = maven_jar_dir.join("guava-28.2-jre-sources.jar"); + std::fs::write(&convention_stub, [0u8; 283]).unwrap(); + + let m2_path = tmp + .path() + .join(".m2/repository/com/google/guava/guava/28.2-jre"); + std::fs::create_dir_all(&m2_path).unwrap(); + let maven_source = m2_path.join("guava-28.2-jre-sources.jar"); + std::fs::write(&maven_source, [0u8; 5000]).unwrap(); + + let java_info = JavaIdeInfo { + jars: vec![], + ..Default::default() + }; + + std::env::set_var("HOME", tmp.path()); + let classpath_jars = vec![classpath_jar.to_string_lossy().into_owned()]; + let resolved = + build_resolved_jars(&java_info, &classpath_jars, &workspace, "@maven//:guava"); + + assert_eq!(resolved.len(), 1); + assert_eq!( + resolved[0].source_path.as_deref(), + Some(maven_source.to_str().unwrap()), + "Convention stub should be filtered, falling through to Maven cache" + ); + } + + // --- Maven coordinate extraction tests --- + + #[test] + fn test_extract_maven_coordinates_standard() { + let path = "external/maven/v1/https/repo1.maven.org/maven2/com/google/guava/guava/28.2-jre/guava-28.2-jre.jar"; + let result = extract_maven_coordinates(path); + assert_eq!( + result, + Some(( + "com.google.guava".to_string(), + "guava".to_string(), + "28.2-jre".to_string() + )) + ); + } + + #[test] + fn test_extract_maven_coordinates_custom_host() { + let path = "external/maven/v1/https/artifacts.company.net/repository/maven/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar"; + let result = extract_maven_coordinates(path); + assert_eq!( + result, + Some(( + "org.slf4j".to_string(), + "slf4j-api".to_string(), + "1.7.36".to_string() + )) + ); + } + + #[test] + fn test_extract_maven_coordinates_non_maven_path() { + let path = "bazel-out/k8-fastbuild/bin/lib/liblib.jar"; + assert_eq!(extract_maven_coordinates(path), None); + } + + #[test] + fn test_probe_maven_local_cache_found() { + let tmp = tempfile::tempdir().unwrap(); + let m2_path = tmp + .path() + .join(".m2/repository/com/google/guava/guava/28.2-jre"); + std::fs::create_dir_all(&m2_path).unwrap(); + let sources_jar = m2_path.join("guava-28.2-jre-sources.jar"); + std::fs::write(&sources_jar, [0u8; 5000]).unwrap(); + + std::env::set_var("HOME", tmp.path()); + let result = probe_maven_local_cache("com.google.guava", "guava", "28.2-jre"); + assert_eq!(result, Some(sources_jar.to_string_lossy().into_owned())); + } + + #[test] + fn test_probe_maven_local_cache_not_found() { + let tmp = tempfile::tempdir().unwrap(); + std::env::set_var("HOME", tmp.path()); + let result = probe_maven_local_cache("com.nonexistent", "artifact", "1.0"); + assert_eq!(result, None); + } } diff --git a/bazel-jdt-bridge/crates/bazel-graph/src/lib.rs b/bazel-jdt-bridge/crates/bazel-graph/src/lib.rs index fffd423..65c2ad1 100644 --- a/bazel-jdt-bridge/crates/bazel-graph/src/lib.rs +++ b/bazel-jdt-bridge/crates/bazel-graph/src/lib.rs @@ -5,4 +5,4 @@ pub use classpath::{ infer_target_kind, is_bazel_internal_label, AccessRule, ClasspathEntry, ClasspathEntryType, ComputedClasspath, JarConflict, TargetKind, }; -pub use graph::{normalize_label, DependencyGraph, GraphError}; +pub use graph::{normalize_label, DependencyGraph, GraphError, ResolvedJar}; diff --git a/bazel-jdt-bridge/crates/bazel-jdt-core/src/aspects/intellij_info_impl_bundled.bzl b/bazel-jdt-bridge/crates/bazel-jdt-core/src/aspects/intellij_info_impl_bundled.bzl index 5b177fc..d1c9212 100644 --- a/bazel-jdt-bridge/crates/bazel-jdt-core/src/aspects/intellij_info_impl_bundled.bzl +++ b/bazel-jdt-bridge/crates/bazel-jdt-core/src/aspects/intellij_info_impl_bundled.bzl @@ -483,6 +483,11 @@ def collect_java_info(target, ctx, semantics, ide_info, ide_info_file, output_gr if hasattr(java_info, "transitive_compile_time_jars"): update_set_in_dict(output_groups, "intellij-resolve-java-direct-deps", java_info.transitive_compile_time_jars) update_set_in_dict(output_groups, "intellij-resolve-java-direct-deps", java_info.transitive_source_jars) + + # full JARs for complete IDE experience (only built when output group is requested) + if hasattr(java_info, "transitive_runtime_jars"): + update_set_in_dict(output_groups, "intellij-resolve-java-full-jars", java_info.transitive_runtime_jars) + return True def collect_c_toolchain_info(target, ctx, semantics, ide_info, ide_info_file, output_groups): diff --git a/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs b/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs index b6b7d17..60f1ae9 100644 --- a/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs +++ b/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs @@ -110,7 +110,8 @@ pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeInitialize( // Initialize stderr logger (controlled via RUST_LOG env var, default=warn). // try_init is idempotent — safe if called multiple times. let _ = env_logger::Builder::from_env( - env_logger::Env::default().default_filter_or("warn,bazel_jdt_core=info,bazel_query=info"), + env_logger::Env::default() + .default_filter_or("warn,bazel_jdt_core=info,bazel_query=info,bazel_graph=info"), ) .try_init(); @@ -398,12 +399,20 @@ pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeRunAspectBuild( handle: jlong, targets: JObjectArray, build_flags: JObjectArray, + sync_mode: JString, ) -> jobjectArray { let state = match get_state(&mut env, handle) { Some(s) => s, None => return std::ptr::null_mut(), }; + let full_jars = env + .get_string(&sync_mode) + .ok() + .map(String::from) + .map(|s| s == "full") + .unwrap_or(false); + let target_vec = match parse_java_string_array(&mut env, &targets) { Some(t) => t, None => { @@ -418,12 +427,13 @@ pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeRunAspectBuild( let build_flags_ref: Option<&[String]> = build_flags_vec.as_deref(); log::info!( - "nativeRunAspectBuild: starting batch aspect build for {} targets", - target_vec.len() + "nativeRunAspectBuild: starting batch aspect build for {} targets (full_jars={})", + target_vec.len(), + full_jars ); match state .invoker - .resolve_full_classpath_sync(&target_vec, build_flags_ref) + .resolve_full_classpath_sync(&target_vec, build_flags_ref, full_jars) { Ok(aspect_results) => { log::info!( @@ -840,7 +850,7 @@ fn run_full_resolution( log::info!("run_full_resolution for '{}'", target_label); let aspect_results = state .invoker - .resolve_full_classpath_sync(&targets, build_flags) + .resolve_full_classpath_sync(&targets, build_flags, false) .map_err(|e| { let err_str = format!("{}", e); let is_aspect_not_found = (err_str.contains("repository") diff --git a/bazel-jdt-bridge/crates/bazel-jdt-core/src/state.rs b/bazel-jdt-bridge/crates/bazel-jdt-core/src/state.rs index 7a8ac2e..78d9489 100644 --- a/bazel-jdt-bridge/crates/bazel-jdt-core/src/state.rs +++ b/bazel-jdt-bridge/crates/bazel-jdt-core/src/state.rs @@ -305,7 +305,7 @@ impl BazelJdtState { targets: &[String], ) -> Result, String> { self.invoker - .resolve_full_classpath_sync(targets, None) + .resolve_full_classpath_sync(targets, None, false) .map_err(|e| format!("Aspect build failed: {}", e)) } } diff --git a/bazel-jdt-bridge/crates/bazel-jdt-core/tests/integration.rs b/bazel-jdt-bridge/crates/bazel-jdt-core/tests/integration.rs index 63faa53..95165f4 100644 --- a/bazel-jdt-bridge/crates/bazel-jdt-core/tests/integration.rs +++ b/bazel-jdt-bridge/crates/bazel-jdt-core/tests/integration.rs @@ -1,5 +1,5 @@ use bazel_cache::BazelCache; -use bazel_graph::{ComputedClasspath, DependencyGraph, TargetKind}; +use bazel_graph::{ComputedClasspath, DependencyGraph, ResolvedJar, TargetKind}; use bazel_parser::{BuildFileParser, RuleType}; #[test] @@ -63,9 +63,27 @@ fn dependency_chain_classpath() { graph.add_dep("//app:app", "//service:service"); graph.add_dep("//service:service", "//utils:utils"); - graph.set_target_jars("//utils:utils", vec!["utils.jar".to_string()]); - graph.set_target_jars("//service:service", vec!["service.jar".to_string()]); - graph.set_target_jars("//app:app", vec!["app.jar".to_string()]); + graph.set_target_jars( + "//utils:utils", + vec![ResolvedJar { + classpath_path: "utils.jar".to_string(), + source_path: None, + }], + ); + graph.set_target_jars( + "//service:service", + vec![ResolvedJar { + classpath_path: "service.jar".to_string(), + source_path: None, + }], + ); + graph.set_target_jars( + "//app:app", + vec![ResolvedJar { + classpath_path: "app.jar".to_string(), + source_path: None, + }], + ); let classpath = ComputedClasspath::compute_for(&graph, "//app:app", TargetKind::JavaBinary, None).unwrap(); @@ -171,8 +189,20 @@ fn pipe_delimited_classpath_output() { graph.add_target("//lib:lib"); graph.add_target("//app:app"); graph.add_dep("//app:app", "//lib:lib"); - graph.set_target_jars("//lib:lib", vec!["lib.jar".to_string()]); - graph.set_target_jars("//app:app", vec!["app.jar".to_string()]); + graph.set_target_jars( + "//lib:lib", + vec![ResolvedJar { + classpath_path: "lib.jar".to_string(), + source_path: None, + }], + ); + graph.set_target_jars( + "//app:app", + vec![ResolvedJar { + classpath_path: "app.jar".to_string(), + source_path: None, + }], + ); let classpath = ComputedClasspath::compute_for(&graph, "//app:app", TargetKind::JavaBinary, None).unwrap(); diff --git a/bazel-jdt-bridge/crates/bazel-query/src/command.rs b/bazel-jdt-bridge/crates/bazel-query/src/command.rs index 68111c9..98049b3 100644 --- a/bazel-jdt-bridge/crates/bazel-query/src/command.rs +++ b/bazel-jdt-bridge/crates/bazel-query/src/command.rs @@ -141,13 +141,20 @@ impl BazelInvoker { targets: &[String], aspect_file: &str, build_flags: Option<&[String]>, + full_jars: bool, ) -> Result { let mut args = vec!["build".to_string()]; if let Some(flags) = build_flags { args.extend(flags.iter().map(|s| s.to_string())); } args.push(format!("--aspects={}", aspect_file)); - args.push("--output_groups=intellij-info-java,intellij-info-generic,intellij-resolve-java-direct-deps".to_string()); + let mut output_groups = + "intellij-info-java,intellij-info-generic,intellij-resolve-java-direct-deps" + .to_string(); + if full_jars { + output_groups.push_str(",intellij-resolve-java-full-jars"); + } + args.push(format!("--output_groups={}", output_groups)); args.push("--keep_going".to_string()); args.push("--show_result=2147483647".to_string()); args.extend(targets.iter().cloned()); @@ -170,13 +177,14 @@ impl BazelInvoker { &self, targets: &[String], build_flags: Option<&[String]>, + full_jars: bool, ) -> Result, BazelError> { if targets.is_empty() { return Ok(Vec::new()); } let aspect_output = - self.build_with_aspects_sync(targets, &self.aspect_label, build_flags)?; + self.build_with_aspects_sync(targets, &self.aspect_label, build_flags, full_jars)?; log::info!("Discovering aspect output files..."); let stderr_files = crate::output::parse_aspect_output_locations(&aspect_output); @@ -232,6 +240,7 @@ impl BazelInvoker { targets: &[String], aspect_file: &str, build_flags: Option<&[String]>, + full_jars: bool, ) -> Result { let bazel_path = self.bazel_path.clone(); let workspace_root = self.workspace_root.clone(); @@ -240,7 +249,13 @@ impl BazelInvoker { args.extend(flags.iter().map(|s| s.to_string())); } args.push(format!("--aspects={}", aspect_file)); - args.push("--output_groups=intellij-info-java,intellij-info-generic,intellij-resolve-java-direct-deps".to_string()); + let mut output_groups = + "intellij-info-java,intellij-info-generic,intellij-resolve-java-direct-deps" + .to_string(); + if full_jars { + output_groups.push_str(",intellij-resolve-java-full-jars"); + } + args.push(format!("--output_groups={}", output_groups)); args.push("--keep_going".to_string()); args.push("--show_result=2147483647".to_string()); args.extend(targets.iter().cloned()); @@ -269,20 +284,22 @@ impl BazelInvoker { &self, targets: &[String], ) -> Result, BazelError> { - self.resolve_full_classpath_with_flags(targets, None).await + self.resolve_full_classpath_with_flags(targets, None, false) + .await } pub async fn resolve_full_classpath_with_flags( &self, targets: &[String], build_flags: Option<&[String]>, + full_jars: bool, ) -> Result, BazelError> { if targets.is_empty() { return Ok(Vec::new()); } let aspect_output = self - .build_with_aspects(targets, &self.aspect_label, build_flags) + .build_with_aspects(targets, &self.aspect_label, build_flags, full_jars) .await?; let stderr_files = crate::output::parse_aspect_output_locations(&aspect_output); diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java index d368632..fdbc39f 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java @@ -16,6 +16,7 @@ public final class BazelBridge { private String lastCacheDir; private volatile String dependencyResolutionMode = "transitive"; private volatile String dependencySourceLoadingMode = "full-project"; + private volatile String syncMode = "fast"; private volatile String[] cachedDependencyPackages = new String[0]; private static ExecutorService createExecutor() { @@ -129,8 +130,9 @@ public void populateGraph() { public String[] runAspectBuild(String[] targets, String[] buildFlags) { long h = snapshotHandle(); + String mode = this.syncMode; try { - return jniExecutor.submit(() -> nativeRunAspectBuild(h, targets, buildFlags)) + return jniExecutor.submit(() -> nativeRunAspectBuild(h, targets, buildFlags, mode)) .get(DISCOVER_TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -216,6 +218,14 @@ public String getDependencySourceLoadingMode() { return this.dependencySourceLoadingMode; } + public void setSyncMode(String mode) { + this.syncMode = mode; + } + + public String getSyncMode() { + return this.syncMode; + } + public void setCachedDependencyPackages(String[] packages) { this.cachedDependencyPackages = packages != null ? packages : new String[0]; } @@ -326,7 +336,7 @@ private long snapshotHandleNullable() { private native void nativeUpdateWatchPaths(long handle, String[] watchPaths); private native String[] nativeQueryTargets(long handle, String[] scopePatterns); private native void nativePopulateGraph(long handle); - private native String[] nativeRunAspectBuild(long handle, String[] targets, String[] buildFlags); + private native String[] nativeRunAspectBuild(long handle, String[] targets, String[] buildFlags, String syncMode); private native String[] nativeComputeClasspath(long handle, String targetLabel, String[] buildFlags); private native String[] nativeComputeClasspathMerged(long handle, String[] labels); private native int nativeGetSyncState(long handle); diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java index 3d8183f..67b741b 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java @@ -79,6 +79,13 @@ private Object handleImportProject(List arguments) { "Dependency source loading mode set to: " + loadingMode)); } + if (arguments.size() > 7 && arguments.get(7) instanceof String) { + String syncMode = (String) arguments.get(7); + bridge.setSyncMode(syncMode); + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Sync mode set to: " + syncMode)); + } + String[] targets = bridge.discoverTargets(scopePatterns, buildFlags); BazelClasspathManager.refreshClasspath(); return null; diff --git a/bazel-jdt-bridge/scripts/build-for-debug.sh b/bazel-jdt-bridge/scripts/build-for-debug.sh index 5cae6e2..ddf6a91 100755 --- a/bazel-jdt-bridge/scripts/build-for-debug.sh +++ b/bazel-jdt-bridge/scripts/build-for-debug.sh @@ -184,6 +184,27 @@ if [[ "$SKIP_RUST" == false ]]; then echo "" fi +# --- Step 1b: Create placeholder native libs for other platforms --- +# bnd validates that all Bundle-NativeCode entries exist in the JAR. +# For debug builds we only have the current platform's real library, +# so create empty placeholders for the others. +echo "--- [1b] Ensuring native library placeholders for all platforms ---" +native_base="$PROJECT_ROOT/java-bridge/src/main/resources/native" +for entry in \ + "linux-x86_64/libbazel_jdt_core.so" \ + "linux-aarch64/libbazel_jdt_core.so" \ + "darwin-x86_64/libbazel_jdt_core.dylib" \ + "darwin-aarch64/libbazel_jdt_core.dylib" \ + "windows-x86_64/bazel_jdt_core.dll"; do + lib_file="$native_base/$entry" + if [[ ! -f "$lib_file" ]]; then + mkdir -p "$(dirname "$lib_file")" + touch "$lib_file" + echo " Created placeholder: native/$entry" + fi +done +echo "" + # --- Step 2: Java OSGi bundle --- step_num=$([ "$SKIP_RUST" == false ] && echo "2/3" || echo "1/2") echo "--- [$step_num] Building Java OSGi bundle ---" diff --git a/bazel-jdt-bridge/vscode-extension/package.json b/bazel-jdt-bridge/vscode-extension/package.json index efb513b..8d4c5a7 100644 --- a/bazel-jdt-bridge/vscode-extension/package.json +++ b/bazel-jdt-bridge/vscode-extension/package.json @@ -114,6 +114,16 @@ ], "description": "How to handle inter-project dependencies when not all workspace targets are imported. 'transitive' auto-imports all dependencies; 'optional' suppresses errors for missing projects." }, + "bazel-jdt.syncMode": { + "type": "string", + "default": "fast", + "enum": ["fast", "full"], + "enumDescriptions": [ + "Use header JARs (hjars) for fast sync — best for quick iteration", + "Build full JARs for complete IDE experience — slower initial sync but no missing imports" + ], + "description": "Controls whether aspect build requests full JARs or header JARs. 'fast' uses hjars (quick sync), 'full' builds complete JARs (slower but resolves all imports and InlayHints)." + }, "bazel-jdt.dependencySourceLoading": { "type": "string", "default": "full-project", diff --git a/bazel-jdt-bridge/vscode-extension/src/commands.ts b/bazel-jdt-bridge/vscode-extension/src/commands.ts index 683f093..fd3b1f5 100644 --- a/bazel-jdt-bridge/vscode-extension/src/commands.ts +++ b/bazel-jdt-bridge/vscode-extension/src/commands.ts @@ -32,7 +32,8 @@ export function registerImportCommand(context: vscode.ExtensionContext) { progress.report({ message: 'Discovering Java targets...' }); await vscode.commands.executeCommand('java.execute.workspaceCommand', 'bazel-jdt.importProject', workspaceRoot, bazelPath, config.cacheDir, - scopePatterns, buildFlags, config.dependencyResolution, config.dependencySourceLoading); + scopePatterns, buildFlags, config.dependencyResolution, config.dependencySourceLoading, + config.syncMode); } ); vscode.window.showInformationMessage('Bazel project imported successfully'); diff --git a/bazel-jdt-bridge/vscode-extension/src/config.ts b/bazel-jdt-bridge/vscode-extension/src/config.ts index f2497dd..946481c 100644 --- a/bazel-jdt-bridge/vscode-extension/src/config.ts +++ b/bazel-jdt-bridge/vscode-extension/src/config.ts @@ -13,6 +13,7 @@ export interface BazelConfig { deriveTargets: boolean; dependencyResolution: string; dependencySourceLoading: string; + syncMode: string; } export function getConfig(): BazelConfig { @@ -30,5 +31,6 @@ export function getConfig(): BazelConfig { deriveTargets: config.get('deriveTargets', true), dependencyResolution: config.get('dependencyResolution', 'transitive'), dependencySourceLoading: config.get('dependencySourceLoading', 'full-project'), + syncMode: config.get('syncMode', 'fast'), }; }