Skip to content

Commit 4403eac

Browse files
committed
Add option --allow-exporting-global-symbols-from-directory-entries
Previously, scip-java only allowed cross-index (or cross-repo) navigation between symbols that are compiled to classfiles inside jar files. This behavior mixed an intentional design decision with an accidental implementation detail. The intentional design decision was that the ideal cross-repo navigation experience is from repositories to packages. To achieve this behavior, scip-java didn't assign a package name and package version to symbols that are compiled to directory entries on the classpath. The accidental implementation detail was that Gradle, Maven, and sbt compile to directories while packages only have jar files. This commit changes the behavior to treat symbols the same regardless if they are compiled to jar files or directories. The accidental implemenation detail was a hack, and this behavior has come up as a surprise in at least twice cases of trying to integrate scip-java with big propriatery codebases. To get the default old behavior, the flag `--allow-exporting-global-symbols-from-directory-entries` can be set to false.
1 parent b3d6ae2 commit 4403eac

File tree

9 files changed

+176
-34
lines changed

9 files changed

+176
-34
lines changed

scip-java/src/main/scala/com/sourcegraph/scip_java/buildtools/ClasspathEntry.scala

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import java.nio.file.Files
66
import java.nio.file.Path
77
import java.nio.file.Paths
88

9+
import scala.annotation.tailrec
910
import scala.jdk.CollectionConverters._
1011

1112
import com.sourcegraph.scip_semanticdb.MavenPackage
@@ -15,15 +16,15 @@ import com.sourcegraph.scip_semanticdb.MavenPackage
1516
* "packageInformation" nodes.
1617
*/
1718
case class ClasspathEntry(
18-
jar: Path,
19+
entry: Path,
1920
sources: Option[Path],
2021
groupId: String,
2122
artifactId: String,
2223
version: String
2324
) {
2425
def toPackageHubId: String = s"maven:$groupId:$artifactId:$version"
2526
def toPackageInformation: MavenPackage =
26-
new MavenPackage(jar, groupId, artifactId, version)
27+
new MavenPackage(entry, groupId, artifactId, version)
2728
}
2829

2930
object ClasspathEntry {
@@ -39,13 +40,16 @@ object ClasspathEntry {
3940
* @param targetroot
4041
* @return
4142
*/
42-
def fromTargetroot(targetroot: Path): List[ClasspathEntry] = {
43+
def fromTargetroot(
44+
targetroot: Path,
45+
sourceroot: Path
46+
): List[ClasspathEntry] = {
4347
val javacopts = targetroot.resolve("javacopts.txt")
4448
val dependencies = targetroot.resolve("dependencies.txt")
4549
if (Files.isRegularFile(dependencies)) {
4650
fromDependencies(dependencies)
4751
} else if (Files.isRegularFile(javacopts)) {
48-
fromJavacopts(javacopts)
52+
fromJavacopts(javacopts, sourceroot)
4953
} else {
5054
Nil
5155
}
@@ -55,17 +59,18 @@ object ClasspathEntry {
5559
* Parses ClasspathEntry from a "dependencies.txt" file in the targetroot.
5660
*
5761
* Every line of the file is a tab separated value with the following columns:
58-
* groupId, artifactId, version, path to the jar file.
62+
* groupId, artifactId, version, path to the jar file OR classes directory
63+
* path.
5964
*/
6065
private def fromDependencies(dependencies: Path): List[ClasspathEntry] = {
6166
Files
6267
.readAllLines(dependencies, StandardCharsets.UTF_8)
6368
.asScala
6469
.iterator
6570
.map(_.split("\t"))
66-
.collect { case Array(groupId, artifactId, version, jar) =>
71+
.collect { case Array(groupId, artifactId, version, entry) =>
6772
ClasspathEntry(
68-
jar = Paths.get(jar),
73+
entry = Paths.get(entry),
6974
sources = None,
7075
groupId = groupId,
7176
artifactId = artifactId,
@@ -81,36 +86,69 @@ object ClasspathEntry {
8186
* Every line of the file represents a Java compiler options, such as
8287
* "-classpath" or "-encoding".
8388
*/
84-
private def fromJavacopts(javacopts: Path): List[ClasspathEntry] = {
89+
private def fromJavacopts(
90+
javacopts: Path,
91+
sourceroot: Path
92+
): List[ClasspathEntry] = {
8593
Files
8694
.readAllLines(javacopts, StandardCharsets.UTF_8)
8795
.asScala
8896
.iterator
8997
.map(_.stripPrefix("\"").stripSuffix("\""))
9098
.sliding(2)
91-
.collect { case Seq("-cp" | "-classpath", classpath) =>
92-
classpath.split(File.pathSeparator).iterator
99+
.collect {
100+
case Seq("-d", classesDirectory) =>
101+
fromClassesDirectory(Paths.get(classesDirectory), sourceroot).toList
102+
case Seq("-cp" | "-classpath", classpath) =>
103+
classpath
104+
.split(File.pathSeparator)
105+
.iterator
106+
.map(Paths.get(_))
107+
.flatMap(ClasspathEntry.fromClasspathJarFile)
108+
.toList
93109
}
94110
.flatten
95-
.map(Paths.get(_))
96-
.toSet
97-
.iterator
98-
.flatMap(ClasspathEntry.fromPom)
99111
.toList
100112
}
101113

114+
private def fromClassesDirectory(
115+
classesDirectory: Path,
116+
sourceroot: Path
117+
): Option[ClasspathEntry] = {
118+
@tailrec
119+
def loop(dir: Path): Option[ClasspathEntry] = {
120+
if (dir == null || !dir.startsWith(sourceroot))
121+
return None
122+
fromPomXml(dir.resolve("pom.xml"), classesDirectory, None) match {
123+
case None =>
124+
loop(dir.getParent())
125+
case Some(value) =>
126+
Some(value)
127+
}
128+
}
129+
loop(classesDirectory.getParent())
130+
}
131+
102132
/**
103133
* Tries to parse a ClasspathEntry from the POM file that lies next to the
104134
* given jar file.
105135
*/
106-
private def fromPom(jar: Path): Option[ClasspathEntry] = {
136+
private def fromClasspathJarFile(jar: Path): Option[ClasspathEntry] = {
107137
val pom = jar
108138
.resolveSibling(jar.getFileName.toString.stripSuffix(".jar") + ".pom")
109139
val sources = Option(
110140
jar.resolveSibling(
111141
jar.getFileName.toString.stripSuffix(".jar") + ".sources"
112142
)
113143
).filter(Files.isRegularFile(_))
144+
fromPomXml(pom, jar, sources)
145+
}
146+
147+
private def fromPomXml(
148+
pom: Path,
149+
classpathEntry: Path,
150+
sources: Option[Path]
151+
): Option[ClasspathEntry] = {
114152
if (Files.isRegularFile(pom)) {
115153
val xml = scala.xml.XML.loadFile(pom.toFile)
116154
def xmlValue(key: String): String = {
@@ -123,7 +161,9 @@ object ClasspathEntry {
123161
val groupId = xmlValue("groupId")
124162
val artifactId = xmlValue("artifactId")
125163
val version = xmlValue("version")
126-
Some(ClasspathEntry(jar, sources, groupId, artifactId, version))
164+
Some(
165+
ClasspathEntry(classpathEntry, sources, groupId, artifactId, version)
166+
)
127167
} else {
128168
None
129169
}

scip-java/src/main/scala/com/sourcegraph/scip_java/commands/IndexSemanticdbCommand.scala

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ final case class IndexSemanticdbCommand(
5252
"If true, don't report an error when no documents have been indexed. " +
5353
"The resulting SCIP index will silently be empty instead."
5454
) allowEmptyIndex: Boolean = false,
55+
@Description(
56+
"Determines how to index symbols that are compiled to classfiles inside directories. " +
57+
"If true, symbols inside directory entries are allowed to be publicly visible outside of the generated SCIP index. " +
58+
"If false, symbols inside directory entries are only visible inside the generated SCIP index. " +
59+
"The practical consequences of making this flag false is that cross-index (or cross-repository) navigation does not work between " +
60+
"Maven->Maven or Gradle->Gradle projects because those build tools compile sources to classfiles izelinside directories."
61+
) allowExportingGlobalSymbolsFromDirectoryEntries: Boolean = true,
5562
@Inline() app: Application = Application.default
5663
) extends Command {
5764
def sourceroot: Path = AbsolutePath.of(app.env.workingDirectory)
@@ -75,7 +82,9 @@ final case class IndexSemanticdbCommand(
7582
val packages =
7683
absoluteTargetroots
7784
.iterator
78-
.flatMap(ClasspathEntry.fromTargetroot)
85+
.flatMap(targetroot =>
86+
ClasspathEntry.fromTargetroot(targetroot, sourceroot)
87+
)
7988
.distinct
8089
.toList
8190
val options =
@@ -95,7 +104,8 @@ final case class IndexSemanticdbCommand(
95104
packages.map(_.toPackageInformation).asJava,
96105
buildKind,
97106
emitInverseRelationships,
98-
allowEmptyIndex
107+
allowEmptyIndex,
108+
allowExportingGlobalSymbolsFromDirectoryEntries
99109
)
100110
ScipSemanticdb.run(options)
101111
postPackages(packages)

scip-semanticdb/src/main/java/com/sourcegraph/scip_semanticdb/BazelBuildTool.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ public boolean hasErrors() {
6565
mavenPackages,
6666
/* buildKind */ "",
6767
/* emitInverseRelationships */ true,
68-
/* allowEmptyIndex */ true);
68+
/* allowEmptyIndex */ true,
69+
/* indexDirectoryEntries */ false // because Bazel only compiles to jar files.
70+
);
6971
ScipSemanticdb.run(scipOptions);
7072

7173
if (!scipOptions.reporter.hasErrors()) {

scip-semanticdb/src/main/java/com/sourcegraph/scip_semanticdb/PackageTable.java

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@
33
import java.io.File;
44
import java.io.IOException;
55
import java.net.URL;
6-
import java.nio.file.FileSystems;
7-
import java.nio.file.Files;
8-
import java.nio.file.Path;
9-
import java.nio.file.PathMatcher;
10-
import java.nio.file.Paths;
6+
import java.nio.file.*;
7+
import java.nio.file.attribute.BasicFileAttributes;
118
import java.util.Enumeration;
129
import java.util.HashMap;
1310
import java.util.HashSet;
@@ -28,13 +25,17 @@ public class PackageTable implements Function<Package, Integer> {
2825
private final Map<Package, Integer> scip = new ConcurrentHashMap<>();
2926
private final JavaVersion javaVersion;
3027
private final ScipWriter writer;
28+
private final boolean indexDirectoryEntries;
3129

30+
private static final PathMatcher CLASS_PATTERN =
31+
FileSystems.getDefault().getPathMatcher("glob:**.class");
3232
private static final PathMatcher JAR_PATTERN =
3333
FileSystems.getDefault().getPathMatcher("glob:**.jar");
3434

3535
public PackageTable(ScipSemanticdbOptions options, ScipWriter writer) throws IOException {
3636
this.writer = writer;
3737
this.javaVersion = new JavaVersion();
38+
this.indexDirectoryEntries = options.allowExportingGlobalSymbolsFromDirectoryEntries;
3839
// NOTE: it's important that we index the JDK before maven packages. Some maven packages
3940
// redefine classes from the JDK and we want those maven packages to take precedence over
4041
// the JDK. The motivation to prioritize maven packages over the JDK is that we only want
@@ -68,13 +69,29 @@ private Optional<Package> packageForClassfile(String classfile) {
6869
}
6970

7071
private void indexPackage(MavenPackage pkg) throws IOException {
71-
if (!JAR_PATTERN.matches(pkg.jar)) {
72-
return;
72+
if (JAR_PATTERN.matches(pkg.jar) && Files.isRegularFile(pkg.jar)) {
73+
indexJarFile(pkg.jar, pkg);
74+
} else if (this.indexDirectoryEntries && Files.isDirectory(pkg.jar)) {
75+
indexDirectoryPackage(pkg);
7376
}
74-
if (!Files.isRegularFile(pkg.jar)) {
75-
return;
76-
}
77-
indexJarFile(pkg.jar, pkg);
77+
}
78+
79+
private void indexDirectoryPackage(MavenPackage pkg) throws IOException {
80+
Files.walkFileTree(
81+
pkg.jar,
82+
new SimpleFileVisitor<Path>() {
83+
@Override
84+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
85+
throws IOException {
86+
if (CLASS_PATTERN.matches(file)) {
87+
String classfile = pkg.jar.relativize(file).toString();
88+
if (!classfile.contains("$")) {
89+
byClassfile.put(classfile, pkg);
90+
}
91+
}
92+
return super.visitFile(file, attrs);
93+
}
94+
});
7895
}
7996

8097
private void indexJarFile(Path file, Package pkg) throws IOException {

scip-semanticdb/src/main/java/com/sourcegraph/scip_semanticdb/ScipSemanticdbOptions.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class ScipSemanticdbOptions {
1919
public final String buildKind;
2020
public final boolean emitInverseRelationships;
2121
public final boolean allowEmptyIndex;
22+
public final boolean allowExportingGlobalSymbolsFromDirectoryEntries;
2223

2324
public ScipSemanticdbOptions(
2425
List<Path> targetroots,
@@ -32,7 +33,8 @@ public ScipSemanticdbOptions(
3233
List<MavenPackage> packages,
3334
String buildKind,
3435
boolean emitInverseRelationships,
35-
boolean allowEmptyIndex) {
36+
boolean allowEmptyIndex,
37+
boolean allowExportingGlobalSymbolsFromDirectoryEntries) {
3638
this.targetroots = targetroots;
3739
this.output = output;
3840
this.sourceroot = sourceroot;
@@ -45,5 +47,7 @@ public ScipSemanticdbOptions(
4547
this.buildKind = buildKind;
4648
this.emitInverseRelationships = emitInverseRelationships;
4749
this.allowEmptyIndex = allowEmptyIndex;
50+
this.allowExportingGlobalSymbolsFromDirectoryEntries =
51+
allowExportingGlobalSymbolsFromDirectoryEntries;
4852
}
4953
}

semanticdb-gradle-plugin/src/main/scala/SemanticdbGradlePlugin.scala

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ import java.nio.file.Paths
55
import java.{util => ju}
66

77
import scala.jdk.CollectionConverters._
8+
import scala.util.Try
89

910
import com.sourcegraph.scip_java.BuildInfo
1011
import org.gradle.api.DefaultTask
1112
import org.gradle.api.Plugin
1213
import org.gradle.api.Project
1314
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
1415
import org.gradle.api.provider.Property
16+
import org.gradle.api.publish.PublishingExtension
17+
import org.gradle.api.publish.maven.MavenPublication
18+
import org.gradle.api.tasks.SourceSetContainer
1519
import org.gradle.api.tasks.TaskAction
1620
import org.gradle.api.tasks.compile.JavaCompile
1721
import org.gradle.api.tasks.scala.ScalaCompile
@@ -396,8 +400,43 @@ class WriteDependencies extends DefaultTask {
396400
.foreach(path => java.nio.file.Files.createDirectories(path.getParent()))
397401

398402
val deps = List.newBuilder[String]
403+
val project = getProject()
404+
405+
// List the project itself as a dependency so that we can assign project name/version to symbols that are defined in this project.
406+
// The code below is roughly equivalent to the following with Groovy:
407+
// deps += "$publication.groupId $publication.artifactId $publication.version $sourceSets.main.output.classesDirectory"
408+
Try(
409+
project
410+
.getExtensions()
411+
.getByType(classOf[SourceSetContainer])
412+
.getByName("main")
413+
.getOutput()
414+
.getClassesDirs()
415+
.getFiles()
416+
.asScala
417+
.toList
418+
.map(_.getAbsolutePath())
419+
.sorted
420+
.headOption
421+
).collect { case Some(classesDirectory) =>
422+
project
423+
.getExtensions()
424+
.findByType(classOf[PublishingExtension])
425+
.getPublications()
426+
.withType(classOf[MavenPublication])
427+
.asScala
428+
.foreach { publication =>
429+
deps +=
430+
List(
431+
publication.getGroupId(),
432+
publication.getArtifactId(),
433+
publication.getVersion(),
434+
classesDirectory
435+
).mkString("\t")
436+
}
437+
}
399438

400-
getProject()
439+
project
401440
.getConfigurations()
402441
.forEach { conf =>
403442
if (conf.isCanBeResolved()) {

tests/buildTools/src/test/scala/tests/BaseBuildToolSuite.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ abstract class BaseBuildToolSuite extends MopedSuite(ScipJava.app) {
111111
}
112112
if (expectedPackages.nonEmpty) {
113113
val obtainedPackages = ClasspathEntry
114-
.fromTargetroot(targetroot)
114+
.fromTargetroot(targetroot, workingDirectory)
115115
.map(_.toPackageHubId)
116116
.sorted
117117
.distinct

0 commit comments

Comments
 (0)