From 88afb53e5692e119e5059215df81306385728c55 Mon Sep 17 00:00:00 2001 From: chenrun666 <7417245+chenrun666@user.noreply.gitee.com> Date: Sun, 24 May 2026 13:45:39 +0800 Subject: [PATCH] Revert "fix: debug source lookup for Bazel Java workspaces (#25)" This reverts commit 7d4d1a6fabf3c477c9a9a6ce5fd0e6ec04b506f3. --- .../crates/bazel-graph/src/classpath.rs | 94 ------ .../crates/bazel-graph/src/graph.rs | 298 +----------------- .../crates/bazel-jdt-core/src/jni_exports.rs | 19 -- .../crates/bazel-jdt-core/src/state.rs | 63 ---- .../java/com/bazel/jdt/BazelActivator.java | 16 - .../com/bazel/jdt/BazelProjectImporter.java | 4 - .../BazelRuntimeClasspathEntryResolver.java | 62 +--- .../com/bazel/jdt/BazelSourceLookupFix.java | 162 ---------- .../jdt/JavaDebugSourceLookupPatcher.java | 126 -------- .../com/bazel/jdt/JdkSourceLookupPatcher.java | 133 -------- .../bazel/jdt/BazelSourceLookupFixTest.java | 77 ----- docs/debug-source-lookup-fix.md | 124 -------- docs/design/debug-source-lookup-fix.md | 188 ----------- examples/maven-deps-project/.gitignore | 3 +- 14 files changed, 24 insertions(+), 1345 deletions(-) delete mode 100644 bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelSourceLookupFix.java delete mode 100644 bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/JavaDebugSourceLookupPatcher.java delete mode 100644 bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/JdkSourceLookupPatcher.java delete mode 100644 bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelSourceLookupFixTest.java delete mode 100644 docs/debug-source-lookup-fix.md delete mode 100644 docs/design/debug-source-lookup-fix.md diff --git a/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs b/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs index c59f3f3..ac46d1e 100644 --- a/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs +++ b/bazel-jdt-bridge/crates/bazel-graph/src/classpath.rs @@ -1978,98 +1978,4 @@ mod tests { "java_library output_jars should NOT contain dep jars" ); } - - #[test] - fn test_processed_jar_skipped_in_favor_of_runtime_jar() { - let tmp = tempfile::tempdir().unwrap(); - let workspace = tmp.path().join("workspace"); - let jar_dir = workspace.join("external/maven/guava/33.4.0-jre"); - std::fs::create_dir_all(&jar_dir).unwrap(); - - let processed_jar = jar_dir.join("processed_guava-33.4.0-jre.jar"); - let full_jar = jar_dir.join("guava-33.4.0-jre.jar"); - let source_jar = jar_dir.join("guava-33.4.0-jre-sources.jar"); - std::fs::write(&processed_jar, [0u8; 2048]).unwrap(); - std::fs::write(&full_jar, [0u8; 2048]).unwrap(); - std::fs::write(&source_jar, [0u8; 2048]).unwrap(); - - let processed_path = processed_jar.to_string_lossy().into_owned(); - let full_path = full_jar.to_string_lossy().into_owned(); - let source_path = source_jar.to_string_lossy().into_owned(); - - let mut graph = DependencyGraph::new(); - let results = vec![ - TargetIdeInfo { - label: "//app:app".to_string(), - kind: "java_library".to_string(), - build_file: None, - java_info: Some(JavaIdeInfo { - jars: vec![JarInfo { - jar: ArtifactLocation { - absolute_path: Some("/workspace/app.jar".to_string()), - ..Default::default() - }, - ..Default::default() - }], - ..Default::default() - }), - deps: vec!["@maven//:guava".to_string()], - runtime_deps: Vec::new(), - exports: Vec::new(), - }, - TargetIdeInfo { - label: "@maven//:guava".to_string(), - kind: "java_import".to_string(), - build_file: None, - java_info: Some(JavaIdeInfo { - jars: vec![], - compile_jars: vec![ArtifactLocation { - absolute_path: Some(processed_path.clone()), - ..Default::default() - }], - runtime_jars: vec![ArtifactLocation { - absolute_path: Some(full_path.clone()), - ..Default::default() - }], - source_jars: vec![ArtifactLocation { - absolute_path: Some(source_path.clone()), - ..Default::default() - }], - ..Default::default() - }), - deps: vec![], - runtime_deps: Vec::new(), - exports: Vec::new(), - }, - ]; - - graph.populate_from_aspects(&results, &workspace); - - let cp = ComputedClasspath::compute_for(&graph, "//app:app", TargetKind::JavaLibrary, None) - .unwrap(); - - let guava_entry = cp - .entries - .iter() - .find(|e| e.entry_type == ClasspathEntryType::Library && (e.path.contains("guava"))); - - assert!(guava_entry.is_some(), "Expected LIB entry for guava"); - - let entry = guava_entry.unwrap(); - assert!( - !entry.path.contains("processed_"), - "LIB entry should reference the real JAR, not processed_ header JAR. Got: {}", - entry.path - ); - assert!( - entry.path.contains("guava-33.4.0-jre.jar"), - "LIB entry should reference the full/runtime JAR. Got: {}", - entry.path - ); - assert_eq!( - entry.source_attachment_path.as_deref(), - Some(source_path.as_str()), - "Source attachment should point to the sources JAR" - ); - } } diff --git a/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs b/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs index cec9575..c3e663d 100644 --- a/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs +++ b/bazel-jdt-bridge/crates/bazel-graph/src/graph.rs @@ -15,31 +15,19 @@ pub struct ResolvedJar { pub source_path: Option, } -fn is_header_jar(path: &str) -> bool { - if path.contains("/_ijar/") { - return true; - } - // Bazel rules_jvm_external produces processed_ prefix header JARs - // that contain only class signatures (no method bodies), similar to - // ijar outputs but with a different naming convention. - std::path::Path::new(path) - .file_name() - .map(|name| name.to_string_lossy().starts_with("processed_")) - .unwrap_or(false) +fn is_ijar_path(path: &str) -> bool { + path.contains("/_ijar/") } impl ResolvedJar { pub fn effective_path(&self) -> &str { - if std::path::Path::new(&self.full_jar_path).exists() && !is_header_jar(&self.full_jar_path) - { + if std::path::Path::new(&self.full_jar_path).exists() { &self.full_jar_path } else if let Some(ref runtime) = self.runtime_jar_path { if std::path::Path::new(runtime).exists() { runtime - } else if is_header_jar(&self.full_jar_path) { - &self.full_jar_path } else if let Some(ref compile) = self.compile_jar_path { - if is_header_jar(compile) { + if is_ijar_path(compile) { &self.full_jar_path } else { compile @@ -47,10 +35,8 @@ impl ResolvedJar { } else { &self.full_jar_path } - } else if is_header_jar(&self.full_jar_path) { - &self.full_jar_path } else if let Some(ref compile) = self.compile_jar_path { - if is_header_jar(compile) { + if is_ijar_path(compile) { &self.full_jar_path } else { compile @@ -850,33 +836,13 @@ fn build_resolved_jars( with_source += 1; } - let runtime_jar_path = { - let fname = std::path::Path::new(jar_path) - .file_name() - .map(|f| f.to_string_lossy().into_owned()) - .unwrap_or_default(); - let lookup_name = fname.strip_prefix("processed_").unwrap_or(&fname); - let matched = runtime_jar_by_filename - .get(lookup_name) - .and_then(|opt| opt.clone()) - .or_else(|| { - runtime_jar_by_filename - .get(&fname) - .and_then(|opt| opt.clone()) - }); - - if let Some(ref rt) = matched { - if is_header_jar(rt) { - resolve_original_jar_path(jar_path, workspace_root).or(matched) - } else { - matched - } - } else if is_header_jar(jar_path) { - resolve_original_jar_path(jar_path, workspace_root) - } else { - matched - } - }; + let runtime_jar_path = std::path::Path::new(jar_path) + .file_name() + .and_then(|fname| { + runtime_jar_by_filename + .get(fname.to_string_lossy().as_ref()) + .and_then(|opt| opt.clone()) + }); ResolvedJar { full_jar_path: jar_path.clone(), @@ -1036,55 +1002,6 @@ impl Default for DependencyGraph { } } -/// Resolve a `processed_` header JAR path to the original full JAR in Bazel's execroot. -/// -/// Transforms: -/// `/bazel-out//bin/external//processed_.jar` -/// → `/external//.jar` -/// -/// Returns `None` if: -/// - The path is not under `bazel-out//bin/external/` -/// - The filename doesn't start with `processed_` -/// - The execroot symlink can't be resolved -/// - The original JAR doesn't exist on disk -fn resolve_original_jar_path( - processed_path: &str, - workspace_root: &std::path::Path, -) -> Option { - let relative = if processed_path.contains("/bin/external/") { - let idx = processed_path.find("/bin/external/")?; - processed_path[idx + 5..].to_string() - } else { - return None; - }; - - let path = std::path::Path::new(&relative); - let filename = path.file_name()?.to_string_lossy(); - let original_filename = filename.strip_prefix("processed_")?; - let original_relative = if let Some(parent) = path.parent() { - format!("{}/{}", parent.to_string_lossy(), original_filename) - } else { - original_filename.to_string() - }; - - let bazel_out = workspace_root.join("bazel-out"); - let resolved = std::fs::canonicalize(&bazel_out).ok()?; - let execroot = resolved.parent()?; - - let candidate = execroot.join(&original_relative); - if candidate.exists() { - let result = candidate.to_string_lossy().into_owned(); - log::info!( - "[bazel-jdt] Resolved processed header JAR to original: '{}' -> '{}'", - processed_path, - result - ); - Some(result) - } else { - None - } -} - fn resolve_external_path(path: &str, workspace_root: &std::path::Path) -> Option { let relative = if path.starts_with("external/") { path.to_string() @@ -1732,197 +1649,6 @@ mod tests { ); } - #[test] - fn test_is_header_jar_detects_processed_prefix() { - assert!( - is_header_jar( - "/bazel-out/bin/external/rules_jvm_external++maven+maven/v1/https/repo1.maven.org/maven2/com/google/guava/guava/33.4.0-jre/processed_guava-33.4.0-jre.jar" - ), - "processed_ prefix should be detected as header JAR" - ); - } - - #[test] - fn test_is_header_jar_detects_ijar_path() { - assert!( - is_header_jar("/bazel-out/bin/external/maven/foo/_ijar/downloaded-ijar.jar"), - "/_ijar/ path should be detected as header JAR" - ); - } - - #[test] - fn test_is_header_jar_rejects_normal_jar() { - assert!( - !is_header_jar("/path/to/guava-33.4.0-jre.jar"), - "normal JAR should not be detected as header JAR" - ); - } - - #[test] - fn test_is_header_jar_rejects_processed_in_middle() { - assert!( - !is_header_jar("/path/to/my-processed-lib.jar"), - "processed in middle of filename should not be falsely detected" - ); - } - - #[test] - fn test_resolve_original_jar_path_standard() { - let tmp = tempfile::tempdir().unwrap(); - let workspace = tmp.path().join("workspace"); - let execroot = tmp.path().join("execroot"); - std::fs::create_dir_all(&workspace).unwrap(); - std::fs::create_dir_all(&execroot).unwrap(); - - let real_bazel_out = execroot.join("bazel-out"); - std::fs::create_dir_all(&real_bazel_out).unwrap(); - std::os::unix::fs::symlink(&real_bazel_out, workspace.join("bazel-out")).unwrap(); - - let jar_dir = execroot.join("external/rules_jvm_external++maven+maven/v1/https/repo1.maven.org/maven2/com/google/guava/guava/33.4.0-jre"); - std::fs::create_dir_all(&jar_dir).unwrap(); - std::fs::write(jar_dir.join("guava-33.4.0-jre.jar"), b"original").unwrap(); - - let processed_path = format!( - "{}/bazel-out/k8-fastbuild/bin/external/rules_jvm_external++maven+maven/v1/https/repo1.maven.org/maven2/com/google/guava/guava/33.4.0-jre/processed_guava-33.4.0-jre.jar", - workspace.display() - ); - - let result = resolve_original_jar_path(&processed_path, &workspace); - assert!(result.is_some(), "Should resolve to original JAR"); - let resolved = result.unwrap(); - assert!( - resolved.contains("guava-33.4.0-jre.jar"), - "Should contain original filename" - ); - assert!( - !resolved.contains("processed_"), - "Should not contain processed_ prefix" - ); - } - - #[test] - fn test_resolve_original_jar_path_not_bazel_out() { - let tmp = tempfile::tempdir().unwrap(); - let result = resolve_original_jar_path("/some/random/path/processed_foo.jar", tmp.path()); - assert!(result.is_none(), "Non bazel-out path should return None"); - } - - #[test] - fn test_resolve_original_jar_path_no_bin_external() { - let tmp = tempfile::tempdir().unwrap(); - let result = resolve_original_jar_path( - &format!( - "{}/bazel-out/k8-fastbuild/bin/lib/processed_foo.jar", - tmp.path().display() - ), - tmp.path(), - ); - assert!( - result.is_none(), - "Path without /bin/external/ should return None" - ); - } - - #[test] - fn test_resolve_original_jar_path_not_processed_prefix() { - let tmp = tempfile::tempdir().unwrap(); - let workspace = tmp.path().join("workspace"); - std::fs::create_dir_all(&workspace).unwrap(); - let result = resolve_original_jar_path( - &format!( - "{}/bazel-out/k8-fastbuild/bin/external/maven/guava.jar", - workspace.display() - ), - &workspace, - ); - assert!( - result.is_none(), - "Non-processed filename should return None" - ); - } - - #[test] - fn test_resolve_original_jar_path_original_not_found() { - let tmp = tempfile::tempdir().unwrap(); - let workspace = tmp.path().join("workspace"); - let execroot = tmp.path().join("execroot"); - std::fs::create_dir_all(&workspace).unwrap(); - std::fs::create_dir_all(&execroot).unwrap(); - let real_bazel_out = execroot.join("bazel-out"); - std::fs::create_dir_all(&real_bazel_out).unwrap(); - std::os::unix::fs::symlink(&real_bazel_out, workspace.join("bazel-out")).unwrap(); - - let result = resolve_original_jar_path( - &format!( - "{}/bazel-out/k8-fastbuild/bin/external/maven/processed_foo.jar", - workspace.display() - ), - &workspace, - ); - assert!( - result.is_none(), - "Should return None when original JAR doesn't exist" - ); - } - - #[test] - fn test_effective_path_skips_processed_compile_jar_when_full_exists() { - let tmp_dir = std::env::temp_dir(); - let full_file = tmp_dir.join("test_processed_full_guava.jar"); - std::fs::write(&full_file, b"fake full jar").unwrap(); - - let jar = ResolvedJar { - full_jar_path: full_file.to_string_lossy().into_owned(), - compile_jar_path: Some("/path/to/processed_guava-33.4.0-jre.jar".to_string()), - runtime_jar_path: None, - source_path: None, - }; - - assert_eq!( - jar.effective_path(), - full_file.to_string_lossy().as_ref(), - "effective_path() should return full_jar when it exists, skipping processed_ compile_jar" - ); - let _ = std::fs::remove_file(&full_file); - } - - #[test] - fn test_effective_path_skips_processed_compile_jar_when_runtime_exists() { - let tmp_dir = std::env::temp_dir(); - let runtime_file = tmp_dir.join("test_processed_runtime_guava.jar"); - std::fs::write(&runtime_file, b"fake runtime jar").unwrap(); - - let jar = ResolvedJar { - full_jar_path: "/nonexistent/processed_guava-33.4.0-jre.jar".to_string(), - compile_jar_path: Some("/path/to/processed_guava-33.4.0-jre.jar".to_string()), - runtime_jar_path: Some(runtime_file.to_string_lossy().into_owned()), - source_path: None, - }; - - assert_eq!( - jar.effective_path(), - runtime_file.to_string_lossy().as_ref(), - "effective_path() should return runtime_jar when it exists, skipping processed_ compile_jar" - ); - let _ = std::fs::remove_file(&runtime_file); - } - - #[test] - fn test_effective_path_skips_processed_compile_jar_as_last_fallback() { - let jar = ResolvedJar { - full_jar_path: "/nonexistent/guava-33.4.0-jre.jar".to_string(), - compile_jar_path: Some("/path/to/processed_guava-33.4.0-jre.jar".to_string()), - runtime_jar_path: None, - source_path: None, - }; - - assert_eq!( - jar.effective_path(), - "/nonexistent/guava-33.4.0-jre.jar", - "effective_path() should skip processed_ compile_jar even as last fallback" - ); - } - #[test] fn test_effective_path_runtime_jar_unaffected_by_ijar_filter() { let tmp_dir = std::env::temp_dir(); 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 8bda030..2602ca8 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 @@ -194,25 +194,6 @@ pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeInitialize( reg.insert(key, Box::new(state)); } - // Populate dependency graph from BUILD files at initialization time. - // This ensures the graph is never empty, even when Java side takes the - // fast-reload path (tryFastReload) which skips populateGraph(). - { - let reg = registry().lock().unwrap_or_else(|e| e.into_inner()); - if let Some(state) = reg.get(&key) { - match state.populate_graph_from_build_files() { - Ok(count) => log::info!( - "nativeInitialize: populated graph from {} BUILD files", - count - ), - Err(e) => log::warn!( - "nativeInitialize: failed to populate graph from BUILD files: {}", - e - ), - } - } - } - key as jlong } 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 65d1cf3..e280cd9 100644 --- a/bazel-jdt-bridge/crates/bazel-jdt-core/src/state.rs +++ b/bazel-jdt-bridge/crates/bazel-jdt-core/src/state.rs @@ -37,8 +37,6 @@ pub struct BazelJdtState { pub watcher_join_handle: Mutex>>, pub shutdown_flag: AtomicBool, pub pending_changes: Mutex>, - /// Whether the dependency graph has been populated from BUILD files - pub graph_populated: AtomicBool, /// Timeout for `bazel query` operations (default: 120s) pub query_timeout: Duration, /// Timeout for `bazel build --aspects` operations (default: 300s) @@ -92,7 +90,6 @@ impl BazelJdtState { watcher_join_handle: Mutex::new(None), shutdown_flag: AtomicBool::new(false), pending_changes: Mutex::new(Vec::new()), - graph_populated: AtomicBool::new(false), query_timeout: Duration::from_secs(120), aspect_timeout: Duration::from_secs(300), shutdown_tx, @@ -114,10 +111,6 @@ impl BazelJdtState { self.shutdown_flag.load(Ordering::Acquire) } - pub fn is_graph_populated(&self) -> bool { - self.graph_populated.load(Ordering::Acquire) - } - pub fn set_sync_state(&self, state: SyncState) { self.sync_state.store(state as i32, Ordering::SeqCst); } @@ -144,7 +137,6 @@ impl BazelJdtState { let count = parsed.len(); let mut graph = self.graph.lock().unwrap_or_else(|e| e.into_inner()); graph.populate_from_parsed_batch(&parsed, &self.workspace_root); - self.graph_populated.store(true, Ordering::Release); Ok(count) } @@ -319,58 +311,3 @@ impl BazelJdtState { .map_err(|e| format!("Aspect build failed: {}", e)) } } - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::atomic::Ordering; - - #[test] - fn test_new_state_graph_not_populated() { - let tmp = tempfile::tempdir().unwrap(); - let state = BazelJdtState::new( - tmp.path().to_path_buf(), - "bazel", - tmp.path().join("cache").as_path(), - ) - .unwrap(); - assert!(!state.is_graph_populated()); - assert!(!state.graph_populated.load(Ordering::SeqCst)); - } - - #[test] - fn test_populate_sets_flag_on_success() { - let tmp = tempfile::tempdir().unwrap(); - let workspace = tmp.path().join("ws"); - std::fs::create_dir_all(&workspace).unwrap(); - - let state = BazelJdtState::new( - workspace.clone(), - "bazel", - tmp.path().join("cache").as_path(), - ) - .unwrap(); - - assert!(!state.is_graph_populated()); - - let result = state.populate_graph_from_build_files(); - assert!(result.is_ok()); - assert!(state.is_graph_populated()); - } - - #[test] - fn test_populate_graph_from_empty_workspace() { - let tmp = tempfile::tempdir().unwrap(); - let nonexistent = tmp.path().join("nonexistent_ws"); - - let state = - BazelJdtState::new(nonexistent, "bazel", tmp.path().join("cache").as_path()).unwrap(); - - assert!(!state.is_graph_populated()); - - let result = state.populate_graph_from_build_files(); - assert!(result.is_ok()); - assert!(state.is_graph_populated()); - assert_eq!(result.unwrap(), 0); - } -} diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelActivator.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelActivator.java index 21c7117..350a17a 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelActivator.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelActivator.java @@ -29,8 +29,6 @@ public class BazelActivator implements BundleActivator { private IResourceChangeListener invisibleProjectListener; private ServiceRegistration weavingHookRegistration; - private ServiceRegistration debugPatcherRegistration; - private ServiceRegistration jdkLookupPatcherRegistration; @Override public void start(BundleContext context) throws Exception { @@ -40,12 +38,6 @@ public void start(BundleContext context) throws Exception { weavingHookRegistration = context.registerService( WeavingHook.class, new JDTUtilsPatcher(), null); - debugPatcherRegistration = context.registerService( - WeavingHook.class, new JavaDebugSourceLookupPatcher(), null); - - jdkLookupPatcherRegistration = context.registerService( - WeavingHook.class, new JdkSourceLookupPatcher(), null); - invisibleProjectListener = this::checkForInvisibleProjects; ResourcesPlugin.getWorkspace().addResourceChangeListener( invisibleProjectListener, IResourceChangeEvent.POST_CHANGE); @@ -57,14 +49,6 @@ public void stop(BundleContext context) throws Exception { weavingHookRegistration.unregister(); weavingHookRegistration = null; } - if (debugPatcherRegistration != null) { - debugPatcherRegistration.unregister(); - debugPatcherRegistration = null; - } - if (jdkLookupPatcherRegistration != null) { - jdkLookupPatcherRegistration.unregister(); - jdkLookupPatcherRegistration = null; - } if (invisibleProjectListener != null) { ResourcesPlugin.getWorkspace().removeResourceChangeListener(invisibleProjectListener); invisibleProjectListener = null; diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java index 8684080..5cff04a 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java @@ -272,10 +272,6 @@ private boolean tryFastReload(IProgressMonitor monitor) throws CoreException { bridge.setProjectView(projectView); } - LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Fast reload: populating dependency graph from BUILD files")); - bridge.populateGraph(); - ensureBazelProjectsGitignore(workspacePath); if (projectView != null && !projectView.getDirectories().isEmpty()) { diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelRuntimeClasspathEntryResolver.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelRuntimeClasspathEntryResolver.java index fa1ad0e..24bc16f 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelRuntimeClasspathEntryResolver.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelRuntimeClasspathEntryResolver.java @@ -4,8 +4,6 @@ import java.util.List; import java.util.concurrent.ConcurrentHashMap; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.ILog; import org.eclipse.core.runtime.IStatus; @@ -78,60 +76,22 @@ private IRuntimeClasspathEntry[] buildEntries(IJavaProject project) { } List result = new ArrayList<>(); - int libraryCount = 0; - int projectCount = 0; - int sourceCount = 0; - for (IClasspathEntry cpEntry : container.getClasspathEntries()) { - switch (cpEntry.getEntryKind()) { - case IClasspathEntry.CPE_LIBRARY: - libraryCount++; - IRuntimeClasspathEntry archiveEntry = - JavaRuntime.newArchiveRuntimeClasspathEntry(cpEntry.getPath()); - if (cpEntry.getSourceAttachmentPath() != null) { - archiveEntry.setSourceAttachmentPath(cpEntry.getSourceAttachmentPath()); - } - if (cpEntry.getSourceAttachmentRootPath() != null) { - archiveEntry.setSourceAttachmentRootPath( - cpEntry.getSourceAttachmentRootPath()); - } - result.add(archiveEntry); - break; - - case IClasspathEntry.CPE_PROJECT: - projectCount++; - IProject depProject = - ResourcesPlugin.getWorkspace().getRoot().getProject( - cpEntry.getPath().segment(0)); - IJavaProject depJavaProject = JavaCore.create(depProject); - if (depJavaProject != null && depJavaProject.exists()) { - result.add( - JavaRuntime.newProjectRuntimeClasspathEntry(depJavaProject)); - } - break; - - case IClasspathEntry.CPE_SOURCE: - sourceCount++; - IProject srcProject = - ResourcesPlugin.getWorkspace().getRoot().getProject( - cpEntry.getPath().segment(0)); - IJavaProject srcJavaProject = JavaCore.create(srcProject); - if (srcJavaProject != null && srcJavaProject.exists()) { - result.add( - JavaRuntime.newProjectRuntimeClasspathEntry(srcJavaProject)); - } - break; - - default: - break; + if (cpEntry.getEntryKind() != IClasspathEntry.CPE_LIBRARY) { + continue; + } + IRuntimeClasspathEntry rte = JavaRuntime.newArchiveRuntimeClasspathEntry(cpEntry.getPath()); + if (cpEntry.getSourceAttachmentPath() != null) { + rte.setSourceAttachmentPath(cpEntry.getSourceAttachmentPath()); + } + if (cpEntry.getSourceAttachmentRootPath() != null) { + rte.setSourceAttachmentRootPath(cpEntry.getSourceAttachmentRootPath()); } + result.add(rte); } LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Resolved " + result.size() + " runtime classpath entries for " - + project.getElementName() - + " (library=" + libraryCount + ", project=" + projectCount - + ", source=" + sourceCount + ")")); + "Resolved " + result.size() + " runtime classpath entries for " + project.getElementName())); return result.toArray(EMPTY); } catch (Exception e) { LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelSourceLookupFix.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelSourceLookupFix.java deleted file mode 100644 index 2d993de..0000000 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelSourceLookupFix.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.bazel.jdt; - -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.eclipse.core.resources.IWorkspaceRoot; -import org.eclipse.core.resources.ResourcesPlugin; -import org.eclipse.debug.core.sourcelookup.ISourceContainer; -import org.eclipse.jdt.core.IClassFile; -import org.eclipse.jdt.core.IJavaProject; -import org.eclipse.jdt.core.IType; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.JavaModelException; -import org.eclipse.jdt.core.IPackageFragmentRoot; - -public class BazelSourceLookupFix { - - private static final Logger LOG = Logger.getLogger(BazelSourceLookupFix.class.getName()); - - private static final String PFRSC_CLASS = - "org.eclipse.jdt.launching.sourcelookup.containers.PackageFragmentRootSourceContainer"; - - private static final ConcurrentHashMap URI_CACHE = new ConcurrentHashMap<>(); - - private static volatile java.lang.reflect.Method jdtUtilsToUri; - - private BazelSourceLookupFix() {} - - // --- Source container deduplication --- - - public static ISourceContainer[] deduplicateContainers(ISourceContainer[] containers) { - if (containers == null || containers.length <= 1) { - return containers; - } - - try { - return doDeduplicate(containers); - } catch (Exception e) { - LOG.log(Level.WARNING, - "Source container deduplication failed, returning original array", e); - return containers; - } - } - - private static ISourceContainer[] doDeduplicate(ISourceContainer[] containers) throws Exception { - Set seenKeys = new LinkedHashSet<>(); - List result = new ArrayList<>(); - int removed = 0; - - for (ISourceContainer container : containers) { - if (isPfrsc(container)) { - String key = buildDedupKey(container); - if (key != null) { - if (seenKeys.contains(key)) { - removed++; - continue; - } - seenKeys.add(key); - } - } - result.add(container); - } - - if (removed > 0) { - LOG.info(String.format( - "Deduplicated source containers: removed %d duplicate(s), %d remaining", - removed, result.size())); - } - - return result.toArray(new ISourceContainer[0]); - } - - // --- Source file URI normalization --- - - public static String resolveSourceFileURI(String fqn, String sourcePath, String originalUri) { - if (fqn == null || fqn.isEmpty()) { - return originalUri; - } - - String cached = URI_CACHE.get(fqn); - if (cached != null) { - return cached; - } - - try { - IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); - for (IJavaProject project : JavaCore.create(workspaceRoot).getJavaProjects()) { - if (project == null || !project.exists()) { - continue; - } - IType type = project.findType(fqn); - if (type != null && type.isBinary()) { - IClassFile classFile = type.getClassFile(); - if (classFile != null) { - String normalizedUri = invokeJdtUtilsToUri(classFile); - if (normalizedUri != null) { - URI_CACHE.put(fqn, normalizedUri); - return normalizedUri; - } - } - } - } - } catch (Exception e) { - LOG.log(Level.FINE, "Source URI normalization failed for '" + fqn + "'", e); - } - - return originalUri; - } - - private static String invokeJdtUtilsToUri(IClassFile classFile) { - try { - java.lang.reflect.Method method = jdtUtilsToUri; - if (method == null) { - Class clazz = Class.forName("org.eclipse.jdt.ls.core.internal.JDTUtils"); - method = clazz.getMethod("toUri", IClassFile.class); - jdtUtilsToUri = method; - } - return (String) method.invoke(null, classFile); - } catch (Exception e) { - LOG.log(Level.FINE, "JDTUtils.toUri unavailable", e); - return null; - } - } - - // --- Internal helpers --- - - private static String buildDedupKey(ISourceContainer container) { - try { - Object root = container.getClass().getMethod("getPackageFragmentRoot").invoke(container); - if (root instanceof IPackageFragmentRoot) { - IPackageFragmentRoot pfr = (IPackageFragmentRoot) root; - String path = pfr.getPath().toString(); - if (isJdkContainer(pfr, path)) { - return "JDK|" + path; - } - String project = pfr.getJavaProject() != null - ? pfr.getJavaProject().getElementName() : ""; - return project + "|" + path; - } - } catch (Exception e) { - LOG.log(Level.FINE, - "Could not extract path from " + container.getClass().getName(), e); - } - return null; - } - - static boolean isJdkContainer(IPackageFragmentRoot pfr, String path) { - return path.contains("jrt-fs") - || path.contains("/rt.jar") - || path.contains("/jre/lib/") - || path.contains("/Classes/"); - } - - private static boolean isPfrsc(Object obj) { - return obj != null && PFRSC_CLASS.equals(obj.getClass().getName()); - } -} diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/JavaDebugSourceLookupPatcher.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/JavaDebugSourceLookupPatcher.java deleted file mode 100644 index 0b77b89..0000000 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/JavaDebugSourceLookupPatcher.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.bazel.jdt; - -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; -import org.osgi.framework.hooks.weaving.WeavingHook; -import org.osgi.framework.hooks.weaving.WovenClass; - -public class JavaDebugSourceLookupPatcher implements WeavingHook, Opcodes { - - private static final Logger LOG = Logger.getLogger(JavaDebugSourceLookupPatcher.class.getName()); - - private static final String TARGET_BUNDLE = "com.microsoft.java.debug.plugin"; - private static final String JDTUTILS_INTERNAL = "com/microsoft/java/debug/plugin/internal/JdtUtils"; - - private static final String TARGET_METHOD = "getSourceContainers"; - private static final String TARGET_DESC = - "(Lorg/eclipse/jdt/core/IJavaProject;Ljava/util/Set;)[Lorg/eclipse/debug/core/sourcelookup/ISourceContainer;"; - private static final String FIX_INTERNAL = "com/bazel/jdt/BazelSourceLookupFix"; - private static final String FIX_METHOD = "deduplicateContainers"; - private static final String FIX_DESC = - "([Lorg/eclipse/debug/core/sourcelookup/ISourceContainer;)[Lorg/eclipse/debug/core/sourcelookup/ISourceContainer;"; - - @Override - public void weave(WovenClass wovenClass) { - if (!TARGET_BUNDLE.equals(wovenClass.getBundleWiring().getBundle().getSymbolicName())) { - return; - } - - if (!JDTUTILS_INTERNAL.equals(wovenClass.getClassName().replace('.', '/'))) { - if (!JDTUTILS_INTERNAL.equals(wovenClass.getClassName())) { - return; - } - } - - byte[] original = wovenClass.getBytes(); - try { - byte[] patched = patchGetSourceContainers(original); - if (patched != null) { - wovenClass.setBytes(patched); - wovenClass.getDynamicImports().add("com.bazel.jdt"); - LOG.info("Patched JdtUtils.getSourceContainers: injected source container deduplication"); - } else { - LOG.warning("JdtUtils found but getSourceContainers(IJavaProject,Set) not matched - skipping patch"); - } - } catch (Exception e) { - LOG.log(Level.WARNING, - "Failed to patch JdtUtils, leaving class unmodified", e); - } - } - - byte[] patchGetSourceContainers(byte[] classBytes) { - ClassReader reader = new ClassReader(classBytes); - ClassWriter writer = new SafeClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); - DeduplicateInjector[] injectorHolder = {null}; - - ClassVisitor visitor = new ClassVisitor(ASM9, writer) { - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, - String signature, String[] exceptions) { - MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); - if (TARGET_METHOD.equals(name) && TARGET_DESC.equals(descriptor)) { - DeduplicateInjector injector = new DeduplicateInjector(mv); - injectorHolder[0] = injector; - return injector; - } - return mv; - } - }; - - reader.accept(visitor, 0); - if (injectorHolder[0] != null && injectorHolder[0].getInjectionCount() > 0) { - LOG.info("Patched JdtUtils.getSourceContainers: injected deduplication at " - + injectorHolder[0].getInjectionCount() + " ARETURN points"); - return writer.toByteArray(); - } - return null; - } - - static class DeduplicateInjector extends MethodVisitor { - private int injectionCount = 0; - - DeduplicateInjector(MethodVisitor mv) { - super(ASM9, mv); - } - - @Override - public void visitInsn(int opcode) { - if (opcode == ARETURN) { - super.visitMethodInsn( - INVOKESTATIC, - FIX_INTERNAL, - FIX_METHOD, - FIX_DESC, - false - ); - injectionCount++; - } - super.visitInsn(opcode); - } - - int getInjectionCount() { - return injectionCount; - } - } - - private static class SafeClassWriter extends ClassWriter { - SafeClassWriter(ClassReader classReader, int flags) { - super(classReader, flags); - } - - @Override - protected String getCommonSuperClass(String type1, String type2) { - try { - return super.getCommonSuperClass(type1, type2); - } catch (Exception e) { - return "java/lang/Object"; - } - } - } -} diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/JdkSourceLookupPatcher.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/JdkSourceLookupPatcher.java deleted file mode 100644 index b5bf893..0000000 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/JdkSourceLookupPatcher.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.bazel.jdt; - -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; -import org.osgi.framework.hooks.weaving.WeavingHook; -import org.osgi.framework.hooks.weaving.WovenClass; - -public class JdkSourceLookupPatcher implements WeavingHook, Opcodes { - - private static final Logger LOG = Logger.getLogger(JdkSourceLookupPatcher.class.getName()); - - private static final String TARGET_BUNDLE = "com.microsoft.java.debug.plugin"; - private static final String TARGET_CLASS = - "com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider"; - - private static final String TARGET_METHOD = "getSourceFileURI"; - private static final String TARGET_DESC = - "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"; - - private static final String FIX_INTERNAL = "com/bazel/jdt/BazelSourceLookupFix"; - private static final String FIX_METHOD = "resolveSourceFileURI"; - private static final String FIX_DESC = - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"; - - @Override - public void weave(WovenClass wovenClass) { - if (!TARGET_BUNDLE.equals(wovenClass.getBundleWiring().getBundle().getSymbolicName())) { - return; - } - - String className = wovenClass.getClassName().replace('.', '/'); - if (!TARGET_CLASS.equals(className)) { - return; - } - - byte[] original = wovenClass.getBytes(); - try { - byte[] patched = patchGetSourceFileURI(original); - if (patched != null) { - wovenClass.setBytes(patched); - wovenClass.getDynamicImports().add("com.bazel.jdt"); - LOG.info("Patched JdtSourceLookUpProvider.getSourceFileURI: injected JDK source lookup fallback"); - } else { - LOG.warning("JdtSourceLookUpProvider found but getSourceFileURI not matched - skipping patch"); - } - } catch (Exception e) { - LOG.log(Level.WARNING, - "Failed to patch JdtSourceLookUpProvider, leaving class unmodified", e); - } - } - - byte[] patchGetSourceFileURI(byte[] classBytes) { - ClassReader reader = new ClassReader(classBytes); - ClassWriter writer = new SafeClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); - FallbackInjector[] injectorHolder = {null}; - - ClassVisitor visitor = new ClassVisitor(ASM9, writer) { - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, - String signature, String[] exceptions) { - MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); - if (TARGET_METHOD.equals(name) && TARGET_DESC.equals(descriptor)) { - FallbackInjector injector = new FallbackInjector(mv); - injectorHolder[0] = injector; - return injector; - } - return mv; - } - }; - - reader.accept(visitor, 0); - if (injectorHolder[0] != null && injectorHolder[0].getInjectionCount() > 0) { - LOG.info("Patched JdtSourceLookUpProvider.getSourceFileURI: injected fallback at " - + injectorHolder[0].getInjectionCount() + " ARETURN points"); - return writer.toByteArray(); - } - return null; - } - - static class FallbackInjector extends MethodVisitor { - private int injectionCount = 0; - - FallbackInjector(MethodVisitor mv) { - super(ASM9, mv); - } - - @Override - public void visitInsn(int opcode) { - if (opcode == ARETURN) { - // Stack: [original return value] - // Slot 0=this, 1=fqn, 2=sourcePath, 3=free for temp - super.visitVarInsn(ASTORE, 3); - super.visitVarInsn(ALOAD, 1); - super.visitVarInsn(ALOAD, 2); - super.visitVarInsn(ALOAD, 3); - super.visitMethodInsn( - INVOKESTATIC, - FIX_INTERNAL, - FIX_METHOD, - FIX_DESC, - false - ); - injectionCount++; - } - super.visitInsn(opcode); - } - - int getInjectionCount() { - return injectionCount; - } - } - - private static class SafeClassWriter extends ClassWriter { - SafeClassWriter(ClassReader classReader, int flags) { - super(classReader, flags); - } - - @Override - protected String getCommonSuperClass(String type1, String type2) { - try { - return super.getCommonSuperClass(type1, type2); - } catch (Exception e) { - return "java/lang/Object"; - } - } - } -} diff --git a/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelSourceLookupFixTest.java b/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelSourceLookupFixTest.java deleted file mode 100644 index 570521e..0000000 --- a/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelSourceLookupFixTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.bazel.jdt; - -import static org.junit.Assert.*; - -import org.junit.Test; - -public class BazelSourceLookupFixTest { - - @Test - public void testNullInput() { - assertNull(BazelSourceLookupFix.deduplicateContainers(null)); - } - - @Test - public void testIsJdkContainerJrtFs() { - assertTrue(BazelSourceLookupFix.isJdkContainer(null, - "/usr/lib/jvm/java-21-openjdk-amd64/lib/jrt-fs.jar")); - } - - @Test - public void testIsJdkContainerRtJar() { - assertTrue(BazelSourceLookupFix.isJdkContainer(null, - "/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/rt.jar")); - } - - @Test - public void testIsJdkContainerJreLib() { - assertTrue(BazelSourceLookupFix.isJdkContainer(null, - "/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar")); - } - - @Test - public void testIsJdkContainerMacOsClasses() { - assertTrue(BazelSourceLookupFix.isJdkContainer(null, - "/System/Library/Frameworks/Classes/classes.jar")); - } - - @Test - public void testIsNotJdkContainerGuava() { - assertFalse(BazelSourceLookupFix.isJdkContainer(null, - "/home/user/.cache/bazel/external/maven/processed_guava-33.4.0-jre.jar")); - } - - @Test - public void testIsNotJdkContainerJUnit() { - assertFalse(BazelSourceLookupFix.isJdkContainer(null, - "/home/user/.m2/repository/junit/junit/4.13.2/junit-4.13.2.jar")); - } - - @Test - public void testIsNotJdkContainerProjectJar() { - assertFalse(BazelSourceLookupFix.isJdkContainer(null, - "/home/user/workspace/bazel-out/bin/utils/string_utils.jar")); - } - - @Test - public void testResolveSourceFileURINullFqnReturnsOriginal() { - String original = "jdt://contents/something"; - assertEquals(original, - BazelSourceLookupFix.resolveSourceFileURI(null, "path", original)); - } - - @Test - public void testResolveSourceFileURIEmptyFqnReturnsOriginal() { - String original = "jdt://contents/something"; - assertEquals(original, - BazelSourceLookupFix.resolveSourceFileURI("", "path", original)); - } - - @Test - public void testResolveSourceFileURINoWorkspaceFallsBackToOriginal() { - String original = "jdt://contents/test.jar/pkg/Foo.class?=project/..."; - String result = BazelSourceLookupFix.resolveSourceFileURI( - "com.example.Nonexistent", "com/example/Nonexistent.java", original); - assertEquals(original, result); - } -} diff --git a/docs/debug-source-lookup-fix.md b/docs/debug-source-lookup-fix.md deleted file mode 100644 index 54e757d..0000000 --- a/docs/debug-source-lookup-fix.md +++ /dev/null @@ -1,124 +0,0 @@ -# Debug Source Lookup Fix - -## Problem - -When debugging Java applications in Bazel workspaces via VS Code, source file lookup fails for library code (both JDK and third-party JARs like Guava), causing: - -1. **"Unknown Source"** displayed in call stack instead of actual source file -2. **Breakpoint red dot missing** — debug stops at a file but breakpoint markers don't appear -3. **Two different files opened** — Ctrl+Click navigation and debug stack trace open different editor tabs for the same class - -### Root Cause Chain - -Three distinct bugs contribute, each layered on the next: - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Bug 1: Source Container Duplication │ -│ ──────────────────────────────────── │ -│ JDT.LS's JdtUtils.getSourceContainers() returns 64 containers │ -│ (50 are jrt-fs.jar duplicates from modular JDK). This causes │ -│ excessive iteration but is not the primary failure. │ -│ │ -│ Bug 2: PFRSC fails for modular JDK classes │ -│ ──────────────────────────────────────── │ -│ PackageFragmentRootSourceContainer.findSourceElements() uses │ -│ IPackageFragmentRoot.getPackageFragment("java.lang") to locate │ -│ JDK sources. In modular JDK (9+), "java.lang" lives under the │ -│ "java.base" module, so fragment.exists() returns false. Result: │ -│ getSourceFileURI() returns null → "Unknown Source". │ -│ │ -│ Bug 3: URI mismatch between debug and editor │ -│ ───────────────────────────────────── │ -│ java-debug's getFileURI(IClassFile) produces: │ -│ jdt://contents/jar/pkg/Foo.class?handleId │ -│ JDT.LS's JDTUtils.toUri(IClassFile) produces: │ -│ jdt://contents/jar/pkg/Foo.java?handleId&element=Foo.class │ -│ Different path (.class vs .java) + missing &element suffix + │ -│ different URL encoding → VS Code treats them as different files. │ -│ This affects ALL binary dependencies (JDK + third-party). │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -## Solution - -Three fixes applied via OSGi WeavingHook bytecode injection: - -### Fix 1: Source Container Deduplication - -**File**: `BazelSourceLookupFix.deduplicateContainers()` -**Hook**: `JavaDebugSourceLookupPatcher` (weaves `JdtUtils.getSourceContainers`) - -Deduplicates `PackageFragmentRootSourceContainer` instances by their `IPackageFragmentRoot` path. JDK containers (identified by `jrt-fs`, `rt.jar`, `jre/lib` paths) use a global key; project-scoped containers include the project name. Reduces 64 → 14 containers. - -### Fix 2: JDK Modular Source Lookup Fallback - -**File**: `BazelSourceLookupFix.resolveSourceFileURI()` (originally `resolveJdkSourceFileURI`) -**Hook**: `JdkSourceLookupPatcher` (weaves `JdtSourceLookUpProvider.getSourceFileURI`) - -When the original `getSourceFileURI` returns null or a mismatched URI, resolves the type via `IJavaProject.findType(fqn)`, obtains the `IClassFile`, and generates a proper URI via `JDTUtils.toUri()`. - -### Fix 3: URI Normalization (all binary classes) - -**File**: `BazelSourceLookupFix.resolveSourceFileURI()` -**Hook**: `JdkSourceLookupPatcher` (same hook as Fix 2) - -Instead of building URIs manually, **always delegates to `JDTUtils.toUri(IClassFile)`** via reflection. This ensures debug URIs are identical to editor navigation URIs for: -- JDK classes (java.lang.String, java.util.Locale, etc.) -- Third-party JAR classes (com.google.common.base.Joiner, org.junit.Assert, etc.) -- Workspace binary dependencies - -FQN-based `ConcurrentHashMap` cache avoids redundant `findType()` + `toUri()` calls. - -## Architecture - -``` - OSGi Bundle Loading - │ - BazelActivator.start() - │ - ┌──────────┴──────────┐ - │ │ - JavaDebugSourceLookup- JdkSourceLookup- - Patcher (WeavingHook) Patcher (WeavingHook) - │ │ - Weaves JdtUtils Weaves JdtSourceLookUpProvider - .getSourceContainers .getSourceFileURI - │ │ - Calls deduplicate- Calls resolveSourceFileURI - Containers() (fqn, sourcePath, originalUri) - │ - ┌──────┴──────┐ - │ Cache hit? │ - └──────┬──────┘ - No │ - findType(fqn) → IClassFile - │ - JDTUtils.toUri(classFile) - (via reflection) - │ - Cache + return -``` - -## Files - -| File | Role | -|------|------| -| `BazelSourceLookupFix.java` | Deduplication + URI normalization + JDTUtils bridge | -| `JavaDebugSourceLookupPatcher.java` | WeavingHook for `JdtUtils.getSourceContainers` dedup | -| `JdkSourceLookupPatcher.java` | WeavingHook for `JdtSourceLookUpProvider.getSourceFileURI` fallback | -| `BazelActivator.java` | Registers both WeavingHooks as OSGi services | -| `BazelSourceLookupFixTest.java` | Unit tests (11 tests) | - -## Testing - -```bash -cd bazel-jdt-bridge/java-bridge -mvn test -Dtest=BazelSourceLookupFixTest # 11 tests, 0 failures -mvn clean package -DskipTests # Build OSGi bundle -``` - -Manual verification: -1. Debug into `java.lang.String.charAt()` → source file opens with breakpoint red dot -2. Debug into `com.google.common.base.Joiner.on()` → same file as Ctrl+Click navigation -3. Debug into workspace module code → unaffected (file:// URIs) diff --git a/docs/design/debug-source-lookup-fix.md b/docs/design/debug-source-lookup-fix.md deleted file mode 100644 index 40e8829..0000000 --- a/docs/design/debug-source-lookup-fix.md +++ /dev/null @@ -1,188 +0,0 @@ -# Debug Source Lookup Fix — Architecture Analysis - -**Date**: 2026-05-23 -**Branch**: 001-bazel-java-resolver -**Status**: Implementation Complete (3 layers: label-normalization, graph-lifecycle, runtime-classpath) - -## Problem Statement - -When debugging a Bazel Java project in VS Code: -- **F3 navigation works**: Clicking `userService.getUserName()` correctly opens the `.java` source file -- **Debug fails**: Setting a breakpoint on `getUserName()` stops execution correctly, but opens the **decompiled `.class` file** instead of the `.java` source - -## Root Cause: Three-Layer Architecture Defect - -### Layer 1: State Lifecycle — Empty Graph After Fast Reload - -**Root Cause**: `BazelProjectImporter.tryFastReload()` calls `bridge.initialize()` which creates a new `BazelJdtState` with an **empty** `DependencyGraph`, but never calls `populateGraph()` or `runAspectBuild()`. - -**Impact**: At debug time, `setMergedClasspathContainer()` → `computeClasspathMerged()` → `transitive_deps()` fails with `Target not found: //app:application` because the graph is empty. - -**Three caches with independent lifecycles:** - -| Cache | Location | Persistence | Populated By | -|-------|----------|-------------|-------------| -| redb KV | `~/.cache/bazel-jdt/` | Survives restart | `nativeComputeClasspath()` on cache miss | -| In-memory graph | `BazelJdtState.graph` | **Dies with process** | `populate_from_parsed_batch()`, `populate_from_aspects()` | -| Java file cache | `.bazel-projects/` per project | Survives restart | `batchSetClasspathContainers(fromCache=false)` | - -**The gap**: Fast reload uses Java file cache (survives) but graph (dies) is never repopulated. - -**Key files:** -- `BazelProjectImporter.java:244-325` — `tryFastReload()` skips `populateGraph()` + `runAspectBuild()` -- `BazelClasspathManager.java:30-76` — `setMergedClasspathContainer()` fails on empty graph -- `BazelCommandHandler.java:173-219` — `handleBuildTarget()` triggers the failing path -- `state.rs:48-98` — `BazelJdtState::new()` creates empty graph -- `graph.rs:122-128` — `transitive_deps()` crashes on empty graph - -### Layer 2: Label Normalization — No Single Source of Truth - -**Root Cause**: 4 different label transformation functions with 6 inconsistencies. 4 graph lookup methods bypass alias resolution. - -**Label entry points produce different formats:** - -| Entry Point | Output Format | Normalization | -|-------------|--------------|---------------| -| `bazel query --output=label` | `//app:application` or `@//app:application` (Bazel 7+) | trim only | -| BUILD file parsing | `//app:application` | derived from file path | -| Aspect output (BZL) | `//app:application` (strips `@//` and `@@//`) | BZL strips `@` | -| JNI from Java | whatever `TargetProjectMapping` stores | `normalize_label()` only adds implicit target | - -**Functions:** -- `normalize_label()` (graph.rs:604-616): adds implicit target name, ignores `@` prefix -- `normalize_dep_label()` (graph.rs:587-596): resolves relative deps, delegates to `normalize_label()` -- `canonical_to_apparent_label()` (ide_info.rs:100-113): `@@module~ext~repo` → `@repo` only -- Two `compute_package_label_from_*` functions with different behavior - -**4 lookup methods that bypass alias resolution:** - -| Method | Location | Risk | -|--------|----------|------| -| `transitive_deps()` | graph.rs:123 | **Crashes** — `TargetNotFound` | -| `direct_dependers()` | graph.rs:328 | Silently returns `[]` | -| `reverse_transitive_deps()` | graph.rs:342 | Silently returns `[]` | -| `get_target_kind()` | graph.rs:204 | Silently returns `Unknown` | - -**Contrast with correct methods:** `has_target()` (line 168) and `get_target_jars()` (line 178) DO check aliases. - -**Additional issue:** `add_dep()` (line 106) uses direct indexing `self.label_to_index[from]` — potential panic. - -### Layer 3: Runtime Classpath Resolution — CPE_PROJECT Gap - -**Root Cause**: `BazelRuntimeClasspathEntryResolver.buildEntries()` only processes `CPE_LIBRARY` entries, skipping `CPE_PROJECT` (workspace-internal deps) entirely. - -**The resolver at line 80:** -```java -if (cpEntry.getEntryKind() != IClasspathEntry.CPE_LIBRARY) { - continue; // SKIPS CPE_PROJECT and CPE_SOURCE! -} -``` - -**What the Rust side emits for workspace-internal deps:** -- `PROJ|//service:user_service|...` → Java creates `CPE_PROJECT` pointing to workspace project -- `LIB|/path/to/service.jar|/source/path|...` → Java creates `CPE_LIBRARY` with source attachment - -**What happens during debug:** -1. Resolver skips `CPE_PROJECT` entries → workspace-internal deps invisible to debug source lookup -2. `CPE_LIBRARY` source attachment may or may not be correct (depends on `infer_source_attachment()`) -3. Debugger falls back to decompiled `.class` file - -**Missing in `plugin.xml`:** -- No `sourcePathComputer` extension -- No `sourceContainerResolvers` extension -- No `sourceLookupParticipants` extension - -## Causal Chain (Full Picture) - -``` -VS Code restart - → tryFastReload() → bridge.initialize() → empty graph - → batchSetClasspathContainers(fromCache=true) → works from cache - → User starts debug session - → handleBuildTarget(["app"]) - → buildTargets(["//app:application"]) → bazel build → SUCCESS - → clearCacheForProject("app") - → setMergedClasspathContainer(project, false) - → computeClasspathMerged(["//app:application"]) - → transitive_deps("//app:application") - → label_to_index.get("//app:application") → None (empty graph!) - → ERROR: Target not found: //app:application - → catch block → container NOT updated → stale import-time container - → BazelRuntimeClasspathEntryResolver.buildEntries() - → CPE_LIBRARY entries → processed with source attachment - → CPE_PROJECT entries → SKIPPED - → Debug source lookup can't find workspace-internal .java sources - → Falls back to decompiled .class file -``` - -## Design Decisions - -### Decision 1: Label Normalization — "Normalize at Ingestion" - -**Choice**: Single `canonicalize_label()` function applied at every entry point. Graph stores only apparent form. - -**Rationale**: More efficient than normalizing at lookup time. Eliminates alias bypass problems. Graph-internal code never needs to worry about label format. - -**Canonical form:** -- `//pkg:target` for workspace-internal targets -- `@repo//pkg:target` for external repo targets (apparent form) -- `@@canonical//pkg:target` converted to `@repo//pkg:target` at ingestion - -### Decision 2: Graph Lifecycle — "Always Parse BUILD Files on Init" - -**Choice**: `nativeInitialize()` always calls `populate_graph_from_build_files()`. Aspect build runs async. - -**Rationale**: BUILD file parsing is pure file I/O (< 1 second), no Bazel invocation. Provides immediate graph structure for dependency queries. Aspect data (JARs, sources) populated async. - -**Why not persist graph to redb:** -- petgraph `NodeIndex` not stable across serialization -- BUILD file parsing already fast enough -- Avoids schema migration complexity - -### Decision 3: Runtime Classpath — "Handle All Entry Types" - -**Choice**: Resolver processes `CPE_PROJECT` using `JavaRuntime.newProjectRuntimeClasspathEntry()`. - -**Rationale**: Standard Eclipse JDT pattern. Project runtime entries automatically include source folders in debug source lookup. - -## Implementation Order - -``` -Layer 2 (Label Normalization) — Infrastructure, all layers depend on it - ↓ -Layer 1 (Graph Lifecycle) — State management, depends on correct labels - ↓ -Layer 3 (Runtime Classpath) — Top layer, depends on correct container -``` - -### Layer 2 Implementation Scope - -1. New `canonicalize_label()` function in `graph.rs` -2. Replace all `normalize_label()` calls with `canonicalize_label()` at entry points -3. Add `resolve_index()` helper with alias fallback -4. Fix `add_dep()` to use safe lookup -5. Unify `compute_package_label_from_*` functions -6. Add unit tests for all label formats - -### Layer 1 Implementation Scope - -1. Call `populate_graph_from_build_files()` in `nativeInitialize()` -2. Update `tryFastReload()` to call `populateGraph()` -3. Add `graph_populated: AtomicBool` to `BazelJdtState` -4. Graceful degradation when aspect data not yet available - -### Layer 3 Implementation Scope - -1. Handle `CPE_PROJECT` in `BazelRuntimeClasspathEntryResolver.buildEntries()` -2. Handle `CPE_SOURCE` entries -3. Test with workspace-internal dependency breakpoints - -## Success Criteria - -| Criteria | How to Verify | -|----------|--------------| -| `cargo test --workspace` passes | All existing + new tests green | -| `cargo clippy --workspace` clean | No warnings | -| Fast reload + debug works | Breakpoint in workspace-internal dep opens `.java` not `.class` | -| Label format resilient to Bazel 7+ bzlmod | `@//` and `@@//` labels correctly normalized | -| No regression in F3 navigation | Go-to-definition still works | diff --git a/examples/maven-deps-project/.gitignore b/examples/maven-deps-project/.gitignore index 61481c3..9abe632 100644 --- a/examples/maven-deps-project/.gitignore +++ b/examples/maven-deps-project/.gitignore @@ -1,2 +1 @@ -.bazelproject -.bazel-projects/ +.bazelproject \ No newline at end of file