From d489377658a479d805915f9a48314398acfe537d Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Tue, 26 May 2026 18:50:57 +0800 Subject: [PATCH 1/2] fix: JAR fallback resolution for 3rdparty wrapper targets Resolve missing Bazel build-output JARs (e.g., bazel-out/.../bin/3rdparty/lib.jar) to their actual JAR files in the external repository cache. This fixes the issue where 3rdparty java_library wrapper targets (like //3rdparty:junit) produce output JARs that don't exist after aspect-only builds, causing them to be silently dropped from the classpath. Changes: - Add resolveBuildOutputJar() to handle lib.jar pattern matching and repo search - Add extractArtifactNameFromLibJar() to extract artifact name from filename - Add findCandidateRepoDir() to search external/ with priority: exact_double > exact_single > prefix > bzlmod - Update resolveFallbackJar() to call new build-output resolution when /external/ path resolution fails - Add clearActiveDebugProject command handler to clear debug session gating - Add onDidTerminateDebugSession handler to call clearActiveDebugProject on debug end - Add comprehensive unit tests (15 new test cases) This ensures that tests can find JUnit and other 3rdparty dependencies even when the aspect build doesn't materialize the wrapper JAR on disk. Co-Authored-By: Claude Haiku 4.5 --- .../com/bazel/jdt/BazelCommandHandler.java | 7 + .../bazel/jdt/BazelExternalRepoResolver.java | 72 ++++++-- .../java-bridge/src/main/resources/plugin.xml | 1 + .../jdt/BazelExternalRepoResolverTest.java | 163 +++++++++++++++++- .../vscode-extension/src/extension.ts | 15 ++ 5 files changed, 245 insertions(+), 13 deletions(-) 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 a2f25e5..e81613a 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 @@ -39,11 +39,18 @@ public Object executeCommand(String commandId, List arguments, IProgress return handleBuildTarget(arguments); case "bazel-jdt.setActiveDebugProject": return handleSetActiveDebugProject(arguments); + case "bazel-jdt.clearActiveDebugProject": + return handleClearActiveDebugProject(); default: return null; } } + private Object handleClearActiveDebugProject() { + BazelRuntimeClasspathEntryResolver.clearActiveDebugProject(); + return null; + } + private Object handleImportProject(List arguments) { try { BazelBridge bridge = BazelBridge.getInstance(); diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelExternalRepoResolver.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelExternalRepoResolver.java index 4bf0e54..d380f4d 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelExternalRepoResolver.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelExternalRepoResolver.java @@ -64,22 +64,72 @@ static String resolveFallbackJar(String missingPath, String workspacePath) { if (outputBase == null) return null; String repoName = extractRepoName(path); - if (repoName == null) return null; - - File repoDir = new File(outputBase, "external/" + repoName); - if (!repoDir.isDirectory()) { - repoDir = findBzlmodRepoDir(outputBase, repoName); + if (repoName != null) { + File repoDir = new File(outputBase, "external/" + repoName); + if (!repoDir.isDirectory()) { + repoDir = findBzlmodRepoDir(outputBase, repoName); + } + if (repoDir != null && repoDir.isDirectory()) { + String found = findJarInDirectory(repoDir, MAX_JAR_SEARCH_DEPTH); + if (found != null) { + LOG.info("Fallback JAR resolved: " + path + " -> " + found); + return found; + } + } + return null; } - if (repoDir == null || !repoDir.isDirectory()) return null; - String found = findJarInDirectory(repoDir, MAX_JAR_SEARCH_DEPTH); - if (found != null) { - LOG.info("Fallback JAR resolved: " + path + " -> " + found); - } - return found; + return resolveBuildOutputJar(path, outputBase); }); } + static String resolveBuildOutputJar(String missingPath, String outputBase) { + String artifactName = extractArtifactNameFromLibJar(missingPath); + if (artifactName == null) return null; + + File repoDir = findCandidateRepoDir(outputBase, artifactName); + if (repoDir == null) return null; + + String found = findJarInDirectory(repoDir, MAX_JAR_SEARCH_DEPTH); + if (found != null) { + LOG.info("Build-output JAR resolved: " + missingPath + " -> " + found); + } + return found; + } + + static String extractArtifactNameFromLibJar(String path) { + String fileName = path; + int lastSlash = path.lastIndexOf('/'); + if (lastSlash >= 0) { + fileName = path.substring(lastSlash + 1); + } + if (!fileName.startsWith("lib") || !fileName.endsWith(".jar")) return null; + String name = fileName.substring(3, fileName.length() - 4); + if (name.isEmpty()) return null; + return name; + } + + static File findCandidateRepoDir(String outputBase, String artifactName) { + File externalRoot = new File(outputBase, "external"); + if (!externalRoot.isDirectory()) return null; + + File exact1 = new File(externalRoot, artifactName + "_" + artifactName); + if (exact1.isDirectory()) return exact1; + + File exact2 = new File(externalRoot, artifactName); + if (exact2.isDirectory()) return exact2; + + File[] prefixMatches = externalRoot.listFiles((dir, name) -> + name.startsWith(artifactName + "_")); + if (prefixMatches != null && prefixMatches.length > 0) { + for (File candidate : prefixMatches) { + if (candidate.isDirectory()) return candidate; + } + } + + return findBzlmodRepoDir(outputBase, artifactName); + } + static String extractRepoName(String path) { int idx = path.indexOf("/external/"); if (idx < 0) return null; diff --git a/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml b/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml index 59de6c0..e2dd134 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml +++ b/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml @@ -26,6 +26,7 @@ + diff --git a/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelExternalRepoResolverTest.java b/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelExternalRepoResolverTest.java index ede62b7..3f61b7c 100644 --- a/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelExternalRepoResolverTest.java +++ b/bazel-jdt-bridge/java-bridge/src/test/java/com/bazel/jdt/BazelExternalRepoResolverTest.java @@ -126,12 +126,171 @@ public void resolveFallbackJarReturnsNullWhenNoJarExists() throws IOException { } @Test - public void resolveFallbackJarReturnsNullForNonExternalPath() { + public void resolveFallbackJarResolvesLibJarViaBuildOutput() throws IOException { + String outputBase = tempDir.toString(); + File repoDir = new File(outputBase, "external/junit_junit/jar"); + assertTrue(repoDir.mkdirs()); + File jar = new File(repoDir, "junit-4.13.2.jar"); + assertTrue(jar.createNewFile()); + + String wsPath = tempDir.resolve("workspace").toString(); + BazelExternalRepoResolver.setOutputBaseForTest(wsPath, outputBase); + + String result = BazelExternalRepoResolver.resolveFallbackJar( + "/workspace/bazel-out/darwin_arm64-fastbuild/bin/3rdparty/libjunit.jar", wsPath); + assertNotNull("Should resolve lib.jar via build-output fallback", result); + assertEquals(jar.getAbsolutePath(), result); + } + + @Test + public void resolveFallbackJarReturnsNullForNonExternalNonLibPath() { String wsPath = tempDir.resolve("workspace").toString(); BazelExternalRepoResolver.setOutputBaseForTest(wsPath, tempDir.toString()); String result = BazelExternalRepoResolver.resolveFallbackJar( - "/workspace/bazel-out/bin/3rdparty/libjunit.jar", wsPath); + "/workspace/bazel-out/bin/3rdparty/junit.jar", wsPath); + assertNull(result); + } + + // --- extractArtifactNameFromLibJar tests --- + + @Test + public void extractArtifactNameFromValidLibJar() { + assertEquals("junit", + BazelExternalRepoResolver.extractArtifactNameFromLibJar( + "/workspace/bazel-out/bin/3rdparty/libjunit.jar")); + } + + @Test + public void extractArtifactNameFromLibJarWithUnderscore() { + assertEquals("hamcrest_core", + BazelExternalRepoResolver.extractArtifactNameFromLibJar( + "bazel-out/bin/3rdparty/libhamcrest_core.jar")); + } + + @Test + public void extractArtifactNameReturnsNullForNonLibPrefix() { + assertNull(BazelExternalRepoResolver.extractArtifactNameFromLibJar( + "bazel-out/bin/3rdparty/junit.jar")); + } + + @Test + public void extractArtifactNameReturnsNullForNonJarExtension() { + assertNull(BazelExternalRepoResolver.extractArtifactNameFromLibJar( + "bazel-out/bin/3rdparty/libfoo.txt")); + } + + @Test + public void extractArtifactNameReturnsNullForEmptyName() { + assertNull(BazelExternalRepoResolver.extractArtifactNameFromLibJar( + "bazel-out/bin/3rdparty/lib.jar")); + } + + @Test + public void extractArtifactNameFromFileNameOnly() { + assertEquals("guava", + BazelExternalRepoResolver.extractArtifactNameFromLibJar("libguava.jar")); + } + + // --- findCandidateRepoDir tests --- + + @Test + public void findCandidateRepoDirExactDoubleMatch() throws IOException { + String outputBase = tempDir.toString(); + File exact = new File(outputBase, "external/junit_junit"); + assertTrue(exact.mkdirs()); + + File result = BazelExternalRepoResolver.findCandidateRepoDir(outputBase, "junit"); + assertNotNull(result); + assertEquals(exact.getAbsolutePath(), result.getAbsolutePath()); + } + + @Test + public void findCandidateRepoDirExactSingleMatch() throws IOException { + String outputBase = tempDir.toString(); + File exact = new File(outputBase, "external/guava"); + assertTrue(exact.mkdirs()); + + File result = BazelExternalRepoResolver.findCandidateRepoDir(outputBase, "guava"); + assertNotNull(result); + assertEquals(exact.getAbsolutePath(), result.getAbsolutePath()); + } + + @Test + public void findCandidateRepoDirPrefersDoubleOverSingle() throws IOException { + String outputBase = tempDir.toString(); + File single = new File(outputBase, "external/junit"); + assertTrue(single.mkdirs()); + File double_ = new File(outputBase, "external/junit_junit"); + assertTrue(double_.mkdirs()); + + File result = BazelExternalRepoResolver.findCandidateRepoDir(outputBase, "junit"); + assertNotNull(result); + assertEquals(double_.getAbsolutePath(), result.getAbsolutePath()); + } + + @Test + public void findCandidateRepoDirPrefixMatch() throws IOException { + String outputBase = tempDir.toString(); + File prefixed = new File(outputBase, "external/guava_guava_jre"); + assertTrue(prefixed.mkdirs()); + + File result = BazelExternalRepoResolver.findCandidateRepoDir(outputBase, "guava"); + assertNotNull(result); + assertEquals(prefixed.getAbsolutePath(), result.getAbsolutePath()); + } + + @Test + public void findCandidateRepoDirBzlmodMatch() throws IOException { + String outputBase = tempDir.toString(); + File bzlmod = new File(outputBase, "external/rules_jvm_external~~maven~junit"); + assertTrue(bzlmod.mkdirs()); + + File result = BazelExternalRepoResolver.findCandidateRepoDir(outputBase, "junit"); + assertNotNull(result); + assertEquals(bzlmod.getAbsolutePath(), result.getAbsolutePath()); + } + + @Test + public void findCandidateRepoDirNoMatch() throws IOException { + String outputBase = tempDir.toString(); + File externalDir = new File(outputBase, "external"); + assertTrue(externalDir.mkdirs()); + + assertNull(BazelExternalRepoResolver.findCandidateRepoDir(outputBase, "nonexistent")); + } + + // --- resolveBuildOutputJar end-to-end tests --- + + @Test + public void resolveBuildOutputJarEndToEnd() throws IOException { + String outputBase = tempDir.toString(); + File repoDir = new File(outputBase, "external/hamcrest_core/jar"); + assertTrue(repoDir.mkdirs()); + File jar = new File(repoDir, "hamcrest-core-1.3.jar"); + assertTrue(jar.createNewFile()); + + String result = BazelExternalRepoResolver.resolveBuildOutputJar( + "bazel-out/k8-fastbuild/bin/3rdparty/libhamcrest_core.jar", outputBase); + assertNotNull(result); + assertEquals(jar.getAbsolutePath(), result); + } + + @Test + public void resolveBuildOutputJarReturnsNullForNonLibJar() { + String result = BazelExternalRepoResolver.resolveBuildOutputJar( + "bazel-out/bin/3rdparty/junit.jar", tempDir.toString()); + assertNull(result); + } + + @Test + public void resolveBuildOutputJarReturnsNullWhenNoRepoDir() throws IOException { + String outputBase = tempDir.toString(); + File externalDir = new File(outputBase, "external"); + assertTrue(externalDir.mkdirs()); + + String result = BazelExternalRepoResolver.resolveBuildOutputJar( + "bazel-out/bin/3rdparty/libunknown.jar", outputBase); assertNull(result); } diff --git a/bazel-jdt-bridge/vscode-extension/src/extension.ts b/bazel-jdt-bridge/vscode-extension/src/extension.ts index 14237c3..cdb9e8a 100644 --- a/bazel-jdt-bridge/vscode-extension/src/extension.ts +++ b/bazel-jdt-bridge/vscode-extension/src/extension.ts @@ -40,6 +40,21 @@ function activateFull(context: vscode.ExtensionContext, workspaceRoot: string) { ) ); + context.subscriptions.push( + vscode.debug.onDidTerminateDebugSession(async (session) => { + if (session.type === 'java') { + try { + await vscode.commands.executeCommand( + 'java.execute.workspaceCommand', + 'bazel-jdt.clearActiveDebugProject' + ); + } catch { + // LSP connection may be closed — safe to ignore + } + } + }) + ); + let dependencyPackageCache: string[] = []; const bazelprojectPattern = new vscode.RelativePattern(workspaceRoot, '.bazelproject'); From 2833e77a588a66d9c739dd9511b7b9d1c4ea705e Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Tue, 26 May 2026 22:30:31 +0800 Subject: [PATCH 2/2] feat(extension): auto-refresh test discovery after Bazel project import Trigger testing.refreshTests via vscjava.vscode-java-test after: - Initial JDT.LS import (via serverReady()) - Scope-change re-imports Co-Authored-By: Claude Sonnet 4.6 --- .../vscode-extension/src/extension.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bazel-jdt-bridge/vscode-extension/src/extension.ts b/bazel-jdt-bridge/vscode-extension/src/extension.ts index cdb9e8a..d53d63c 100644 --- a/bazel-jdt-bridge/vscode-extension/src/extension.ts +++ b/bazel-jdt-bridge/vscode-extension/src/extension.ts @@ -104,6 +104,7 @@ function activateFull(context: vscode.ExtensionContext, workspaceRoot: string) { } } + refreshTestDiscovery(); vscode.window.showInformationMessage('Bazel project re-imported (scope changed)'); } catch { // Silently ignore — re-import is best-effort @@ -118,6 +119,13 @@ function activateFull(context: vscode.ExtensionContext, workspaceRoot: string) { statusBarItem, ); + // Trigger test discovery after JDT.LS finishes initial import + const javaExt = vscode.extensions.getExtension('redhat.java'); + const javaApi = javaExt?.exports; + if (javaApi?.serverReady) { + javaApi.serverReady().then(() => refreshTestDiscovery()).catch(() => {}); + } + // On-demand dependency source loading: monitor opened Java files context.subscriptions.push( vscode.workspace.onDidOpenTextDocument(async (doc) => { @@ -171,6 +179,19 @@ function setupCreationOnlyWatcher(context: vscode.ExtensionContext, workspaceRoo context.subscriptions.push(watcher); } +async function refreshTestDiscovery(): Promise { + try { + const testExtension = vscode.extensions.getExtension('vscjava.vscode-java-test'); + if (!testExtension) return; + if (!testExtension.isActive) { + await testExtension.activate(); + } + await vscode.commands.executeCommand('testing.refreshTests'); + } catch { + // Test discovery is best-effort — never block import + } +} + export async function deactivate() { try { await vscode.commands.executeCommand(