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..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 @@ -177,7 +177,6 @@ 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); @@ -195,6 +194,13 @@ private void inferBazelSourceroot(JavaFileObject file) { if (!uriName.equals(pathName)) break; 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. + options.sourceroot = absolutePath.getRoot(); + return; + } options.sourceroot = absolutePath .getRoot() 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()); + } +}