From 2a5e12d3ba5150ba2a21ff39b3cf781e0b8859a1 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 15:57:47 +0200 Subject: [PATCH 1/2] Add tests for Bazel sourceroot inference, fixes #444 --- scip-javac/build.gradle.kts | 7 + .../scip_java/javac/ScipTaskListener.java | 29 ++-- .../scip_java/javac/BazelSourcerootTest.java | 155 ++++++++++++++++++ 3 files changed, 180 insertions(+), 11 deletions(-) create mode 100644 scip-javac/src/test/java/org/scip_code/scip_java/javac/BazelSourcerootTest.java diff --git a/scip-javac/build.gradle.kts b/scip-javac/build.gradle.kts index 2716d8c2b..dd5dd8fe6 100644 --- a/scip-javac/build.gradle.kts +++ b/scip-javac/build.gradle.kts @@ -22,6 +22,13 @@ tasks.named("compileJava") { } } +tasks.named("compileTestJava") { + // Tests use `@ClientCodeWrapper.Trusted` (an internal javac API) to construct file objects + // that mimic Bazel's compiler. `--add-exports` is incompatible with `--release`. + options.release.set(null as Int?) + options.compilerArgs.addAll(JavacInternals.jvmOptions(rootDir)) +} + tasks.named("test") { jvmArgs(JavacInternals.jvmOptions(rootDir)) } diff --git a/scip-javac/src/main/java/org/scip_code/scip_java/javac/ScipTaskListener.java b/scip-javac/src/main/java/org/scip_code/scip_java/javac/ScipTaskListener.java index f779d00ab..6c0bc2d9b 100644 --- a/scip-javac/src/main/java/org/scip_code/scip_java/javac/ScipTaskListener.java +++ b/scip-javac/src/main/java/org/scip_code/scip_java/javac/ScipTaskListener.java @@ -177,15 +177,17 @@ public static Path absolutePathFromUri(ScipJavacOptions options, JavaFileObject } // Infers the `-sourceroot:` flag from the provided file. - // FIXME: add unit tests https://github.com/scip-code/scip-java/issues/444 private void inferBazelSourceroot(JavaFileObject file) { if (options.uriScheme != UriScheme.BAZEL || options.sourceroot != null) return; - Path absolutePath = absolutePathFromUri(options, file); - Path uriPath = Paths.get(file.toUri()); - // absolutePath is the "human-readable" original path, e.g. /home/repo/com/example/Hello.java. - // uriPath is the sandbox/temporary file path, e.g. /private/var/tmp/com/example/Hello.java. - // Infer sourceroot by iterating the names of both files in reverse order and stopping at - // the first entry where the two paths differ. + options.sourceroot = + inferBazelSourceroot(absolutePathFromUri(options, file), Paths.get(file.toUri())); + } + + // absolutePath is the "human-readable" original path, e.g. /home/repo/com/example/Hello.java. + // uriPath is the sandbox/temporary file path, e.g. /private/var/tmp/com/example/Hello.java. + // Infer sourceroot by iterating the names of both files in reverse order and stopping at + // the first entry where the two paths differ. + private static Path inferBazelSourceroot(Path absolutePath, Path uriPath) { int relativePathDepth = 0; int uriPathDepth = uriPath.getNameCount(); int absolutePathDepth = absolutePath.getNameCount(); @@ -195,10 +197,15 @@ private void inferBazelSourceroot(JavaFileObject file) { if (!uriName.equals(pathName)) break; relativePathDepth++; } - options.sourceroot = - absolutePath - .getRoot() - .resolve(absolutePath.subpath(0, absolutePathDepth - relativePathDepth)); + if (relativePathDepth == absolutePathDepth) { + // Every name of absolutePath is a suffix of uriPath (for example, when both paths are + // identical because the file object's toString() didn't match a known Bazel pattern). + // There is no prefix left to use as the sourceroot, so fall back to the filesystem root. + return absolutePath.getRoot(); + } + return absolutePath + .getRoot() + .resolve(absolutePath.subpath(0, absolutePathDepth - relativePathDepth)); } private Result scipShardOutputPath(TaskEvent e) { diff --git a/scip-javac/src/test/java/org/scip_code/scip_java/javac/BazelSourcerootTest.java b/scip-javac/src/test/java/org/scip_code/scip_java/javac/BazelSourcerootTest.java new file mode 100644 index 000000000..605d45c81 --- /dev/null +++ b/scip-javac/src/test/java/org/scip_code/scip_java/javac/BazelSourcerootTest.java @@ -0,0 +1,155 @@ +package org.scip_code.scip_java.javac; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.sun.tools.javac.api.ClientCodeWrapper; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.ToolProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.scip_code.scip.Document; +import org.scip_code.scip.Index; + +/** + * Tests the automatic sourceroot inference of {@code -build-tool:bazel} compilations (see {@code + * ScipTaskListener.inferBazelSourceroot}) by driving the real compiler with file objects that mimic + * the ones constructed by Bazel's Java compiler. + */ +class BazelSourcerootTest { + + /** + * Mimics the {@code com.sun.tools.javac.file.PathFileObject.SimpleFileObject} instances that the + * plugin sees under Bazel: {@code toUri()} points to the file in a sandbox/temporary directory + * while {@code toString()} renders a "human-readable" path pointing to the original source file. + * + *

The {@link ClientCodeWrapper.Trusted} annotation stops javac from wrapping this file object + * in a {@code WrappedJavaFileObject}, which would change the result of {@code toString()}. + * Bazel's file objects are never wrapped because javac-internal classes are trusted. + */ + @ClientCodeWrapper.Trusted + private static final class BazelSourceFile extends SimpleJavaFileObject { + private final Path humanReadablePath; + private final String content; + + BazelSourceFile(Path sandboxPath, Path humanReadablePath, String content) { + super(sandboxPath.toUri(), Kind.SOURCE); + this.humanReadablePath = humanReadablePath; + this.content = content; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return content; + } + + @Override + public String toString() { + return "SimpleFileObject[" + humanReadablePath + "]"; + } + } + + private static void compile(Path targetroot, List compilationUnits) { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + SimpleFileManager fileManager = + new SimpleFileManager(compiler.getStandardFileManager(null, null, null), targetroot); + StringWriter output = new StringWriter(); + List arguments = + Arrays.asList( + "-processorpath", + TestCompiler.PROCESSOR_PATH, + "-classpath", + TestCompiler.PROCESSOR_PATH, + "-Xplugin:scip -build-tool:bazel"); + JavaCompiler.CompilationTask task = + compiler.getTask(output, fileManager, null, arguments, null, compilationUnits); + boolean isSuccess = task.call(); + assertTrue(isSuccess, () -> "compilation should succeed, compiler output:\n" + output); + } + + private static Document readShardDocument(Path shardPath) { + assertTrue(Files.isRegularFile(shardPath), () -> "expected SCIP shard at " + shardPath); + try { + Index shard = Index.parseFrom(Files.readAllBytes(shardPath)); + assertEquals(1, shard.getDocumentsCount()); + return shard.getDocuments(0); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Test + void sandboxedCompilation( + @TempDir Path workspace, @TempDir Path sandbox, @TempDir Path targetroot) { + // The typical case: javac sees the source files in a sandbox directory while their + // human-readable paths point into the workspace. The sourceroot is inferred from the + // first compilation unit (the longest common suffix of the two paths is + // src/com/example/Hello.java, what remains is the workspace directory) and shards are + // written under the workspace-relative path of each source file. + Path execroot = sandbox.resolve("execroot").resolve("_main"); + compile( + targetroot, + Arrays.asList( + new BazelSourceFile( + execroot.resolve("src/com/example/Hello.java"), + workspace.resolve("src/com/example/Hello.java"), + "package com.example;\npublic class Hello {}"), + new BazelSourceFile( + execroot.resolve("src/com/example/inner/World.java"), + workspace.resolve("src/com/example/inner/World.java"), + "package com.example.inner;\npublic class World {}"))); + Path scipRoot = targetroot.resolve("META-INF").resolve("scip"); + Document hello = readShardDocument(scipRoot.resolve("src/com/example/Hello.java.scip")); + assertEquals("src/com/example/Hello.java", hello.getRelativePath()); + Document world = readShardDocument(scipRoot.resolve("src/com/example/inner/World.java.scip")); + assertEquals("src/com/example/inner/World.java", world.getRelativePath()); + } + + @Test + void onlyFileNameInCommon( + @TempDir Path workspace, @TempDir Path sandbox, @TempDir Path targetroot) { + // When the sandbox layout shares nothing with the workspace layout except the file + // name, the inferred sourceroot is the parent directory of the source file. + compile( + targetroot, + Collections.singletonList( + new BazelSourceFile( + sandbox.resolve("Hello.java"), + workspace.resolve("nested/dir/Hello.java"), + "public class Hello {}"))); + Document document = + readShardDocument( + targetroot.resolve("META-INF").resolve("scip").resolve("Hello.java.scip")); + assertEquals("Hello.java", document.getRelativePath()); + } + + @Test + void unrecognizedFileObjectsFallBackToFilesystemRoot( + @TempDir Path sandbox, @TempDir Path targetroot) { + // SimpleSourceFile's toString() doesn't match any of the known Bazel file object + // patterns, so the plugin falls back to using the URI path as the human-readable path. + // The two paths are then identical and the inferred sourceroot degenerates to the + // filesystem root: shards are written under the full path of each source file. + Path source = sandbox.resolve("com/example/Hello.java"); + compile( + targetroot, + Collections.singletonList( + new SimpleSourceFile(source, "package com.example;\npublic class Hello {}"))); + Path relative = source.getRoot().relativize(source); + Document document = + readShardDocument( + targetroot.resolve("META-INF").resolve("scip").resolve(relative + ".scip")); + assertEquals(relative.toString().replace(File.separatorChar, '/'), document.getRelativePath()); + } +} From 86a2a2643f8a8645d67c8713d85ddc922a242f91 Mon Sep 17 00:00:00 2001 From: jupblb Date: Thu, 2 Jul 2026 16:16:01 +0200 Subject: [PATCH 2/2] Minimize diff in ScipTaskListener --- .../scip_java/javac/ScipTaskListener.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/scip-javac/src/main/java/org/scip_code/scip_java/javac/ScipTaskListener.java b/scip-javac/src/main/java/org/scip_code/scip_java/javac/ScipTaskListener.java index 6c0bc2d9b..ece3ca6b2 100644 --- a/scip-javac/src/main/java/org/scip_code/scip_java/javac/ScipTaskListener.java +++ b/scip-javac/src/main/java/org/scip_code/scip_java/javac/ScipTaskListener.java @@ -179,15 +179,12 @@ public static Path absolutePathFromUri(ScipJavacOptions options, JavaFileObject // Infers the `-sourceroot:` flag from the provided file. private void inferBazelSourceroot(JavaFileObject file) { if (options.uriScheme != UriScheme.BAZEL || options.sourceroot != null) return; - options.sourceroot = - inferBazelSourceroot(absolutePathFromUri(options, file), Paths.get(file.toUri())); - } - - // absolutePath is the "human-readable" original path, e.g. /home/repo/com/example/Hello.java. - // uriPath is the sandbox/temporary file path, e.g. /private/var/tmp/com/example/Hello.java. - // Infer sourceroot by iterating the names of both files in reverse order and stopping at - // the first entry where the two paths differ. - private static Path inferBazelSourceroot(Path absolutePath, Path uriPath) { + Path absolutePath = absolutePathFromUri(options, file); + Path uriPath = Paths.get(file.toUri()); + // absolutePath is the "human-readable" original path, e.g. /home/repo/com/example/Hello.java. + // uriPath is the sandbox/temporary file path, e.g. /private/var/tmp/com/example/Hello.java. + // Infer sourceroot by iterating the names of both files in reverse order and stopping at + // the first entry where the two paths differ. int relativePathDepth = 0; int uriPathDepth = uriPath.getNameCount(); int absolutePathDepth = absolutePath.getNameCount(); @@ -201,11 +198,13 @@ private static Path inferBazelSourceroot(Path absolutePath, Path uriPath) { // Every name of absolutePath is a suffix of uriPath (for example, when both paths are // identical because the file object's toString() didn't match a known Bazel pattern). // There is no prefix left to use as the sourceroot, so fall back to the filesystem root. - return absolutePath.getRoot(); + options.sourceroot = absolutePath.getRoot(); + return; } - return absolutePath - .getRoot() - .resolve(absolutePath.subpath(0, absolutePathDepth - relativePathDepth)); + options.sourceroot = + absolutePath + .getRoot() + .resolve(absolutePath.subpath(0, absolutePathDepth - relativePathDepth)); } private Result scipShardOutputPath(TaskEvent e) {