Skip to content

Commit a1d4ff2

Browse files
authored
Merge pull request #616 from sourcegraph/olafurpg/classes-directory
Enable cross-repository navigation between Gradle/Maven codebases
2 parents 50fd968 + 7a095af commit a1d4ff2

File tree

9 files changed

+187
-36
lines changed

9 files changed

+187
-36
lines changed

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

+60-18
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,26 @@ 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
1213

1314
/**
14-
* Represents a single jar file on the classpath of a project, used to emit SCIP
15-
* "packageInformation" nodes.
15+
* Represents a single classpath entry on the classpath of a project, used to
16+
* emit SCIP "packageInformation" nodes. A classpath entry can either be a jar
17+
* file or a directory path.
1618
*/
1719
case class ClasspathEntry(
18-
jar: Path,
20+
entry: Path,
1921
sources: Option[Path],
2022
groupId: String,
2123
artifactId: String,
2224
version: String
2325
) {
2426
def toPackageHubId: String = s"maven:$groupId:$artifactId:$version"
2527
def toPackageInformation: MavenPackage =
26-
new MavenPackage(jar, groupId, artifactId, version)
28+
new MavenPackage(entry, groupId, artifactId, version)
2729
}
2830

2931
object ClasspathEntry {
@@ -39,13 +41,16 @@ object ClasspathEntry {
3941
* @param targetroot
4042
* @return
4143
*/
42-
def fromTargetroot(targetroot: Path): List[ClasspathEntry] = {
44+
def fromTargetroot(
45+
targetroot: Path,
46+
sourceroot: Path
47+
): List[ClasspathEntry] = {
4348
val javacopts = targetroot.resolve("javacopts.txt")
4449
val dependencies = targetroot.resolve("dependencies.txt")
4550
if (Files.isRegularFile(dependencies)) {
4651
fromDependencies(dependencies)
4752
} else if (Files.isRegularFile(javacopts)) {
48-
fromJavacopts(javacopts)
53+
fromJavacopts(javacopts, sourceroot)
4954
} else {
5055
Nil
5156
}
@@ -55,17 +60,18 @@ object ClasspathEntry {
5560
* Parses ClasspathEntry from a "dependencies.txt" file in the targetroot.
5661
*
5762
* Every line of the file is a tab separated value with the following columns:
58-
* groupId, artifactId, version, path to the jar file.
63+
* groupId, artifactId, version, path to the jar file OR classes directory
64+
* path.
5965
*/
6066
private def fromDependencies(dependencies: Path): List[ClasspathEntry] = {
6167
Files
6268
.readAllLines(dependencies, StandardCharsets.UTF_8)
6369
.asScala
6470
.iterator
6571
.map(_.split("\t"))
66-
.collect { case Array(groupId, artifactId, version, jar) =>
72+
.collect { case Array(groupId, artifactId, version, entry) =>
6773
ClasspathEntry(
68-
jar = Paths.get(jar),
74+
entry = Paths.get(entry),
6975
sources = None,
7076
groupId = groupId,
7177
artifactId = artifactId,
@@ -81,36 +87,70 @@ object ClasspathEntry {
8187
* Every line of the file represents a Java compiler options, such as
8288
* "-classpath" or "-encoding".
8389
*/
84-
private def fromJavacopts(javacopts: Path): List[ClasspathEntry] = {
90+
private def fromJavacopts(
91+
javacopts: Path,
92+
sourceroot: Path
93+
): List[ClasspathEntry] = {
8594
Files
8695
.readAllLines(javacopts, StandardCharsets.UTF_8)
8796
.asScala
8897
.iterator
8998
.map(_.stripPrefix("\"").stripSuffix("\""))
9099
.sliding(2)
91-
.collect { case Seq("-cp" | "-classpath", classpath) =>
92-
classpath.split(File.pathSeparator).iterator
100+
.collect {
101+
case Seq("-d", classesDirectory) =>
102+
fromClassesDirectory(Paths.get(classesDirectory), sourceroot).toList
103+
case Seq("-cp" | "-classpath", classpath) =>
104+
classpath
105+
.split(File.pathSeparator)
106+
.iterator
107+
.map(Paths.get(_))
108+
.flatMap(ClasspathEntry.fromClasspathJarFile)
109+
.toList
93110
}
94111
.flatten
95-
.map(Paths.get(_))
96-
.toSet
97-
.iterator
98-
.flatMap(ClasspathEntry.fromPom)
99112
.toList
100113
}
101114

115+
private def fromClassesDirectory(
116+
classesDirectory: Path,
117+
sourceroot: Path
118+
): Option[ClasspathEntry] = {
119+
@tailrec
120+
def loop(dir: Path): Option[ClasspathEntry] = {
121+
if (dir == null || !dir.startsWith(sourceroot))
122+
None
123+
else
124+
fromPomXml(dir.resolve("pom.xml"), classesDirectory, None) match {
125+
case None =>
126+
loop(dir.getParent())
127+
case Some(value) =>
128+
Some(value)
129+
}
130+
}
131+
loop(classesDirectory.getParent())
132+
}
133+
102134
/**
103135
* Tries to parse a ClasspathEntry from the POM file that lies next to the
104136
* given jar file.
105137
*/
106-
private def fromPom(jar: Path): Option[ClasspathEntry] = {
138+
private def fromClasspathJarFile(jar: Path): Option[ClasspathEntry] = {
107139
val pom = jar
108140
.resolveSibling(jar.getFileName.toString.stripSuffix(".jar") + ".pom")
109141
val sources = Option(
110142
jar.resolveSibling(
111143
jar.getFileName.toString.stripSuffix(".jar") + ".sources"
112144
)
113145
).filter(Files.isRegularFile(_))
146+
fromPomXml(pom, jar, sources)
147+
}
148+
149+
private def fromPomXml(
150+
pom: Path,
151+
classpathEntry: Path,
152+
sources: Option[Path]
153+
): Option[ClasspathEntry] = {
114154
if (Files.isRegularFile(pom)) {
115155
val xml = scala.xml.XML.loadFile(pom.toFile)
116156
def xmlValue(key: String): String = {
@@ -123,7 +163,9 @@ object ClasspathEntry {
123163
val groupId = xmlValue("groupId")
124164
val artifactId = xmlValue("artifactId")
125165
val version = xmlValue("version")
126-
Some(ClasspathEntry(jar, sources, groupId, artifactId, version))
166+
Some(
167+
ClasspathEntry(classpathEntry, sources, groupId, artifactId, version)
168+
)
127169
} else {
128170
None
129171
}

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

+12-2
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 inside 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

+3-1
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

+28-11
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

+5-1
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

+47-1
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.control.NonFatal
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,50 @@ 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+
for {
410+
classesDirectory <- project
411+
.getExtensions()
412+
.getByType(classOf[SourceSetContainer])
413+
.getByName("main")
414+
.getOutput()
415+
.getClassesDirs()
416+
.getFiles()
417+
.asScala
418+
.toList
419+
.map(_.getAbsolutePath())
420+
.sorted
421+
.take(1)
422+
publication <-
423+
project
424+
.getExtensions()
425+
.findByType(classOf[PublishingExtension])
426+
.getPublications()
427+
.withType(classOf[MavenPublication])
428+
.asScala
429+
} {
430+
deps +=
431+
List(
432+
publication.getGroupId(),
433+
publication.getArtifactId(),
434+
publication.getVersion(),
435+
classesDirectory
436+
).mkString("\t")
437+
}
438+
} catch {
439+
case NonFatal(ex) =>
440+
println(
441+
s"Failed to extract publication from project ${project.getName()}"
442+
)
443+
ex.printStackTrace()
444+
}
399445

400-
getProject()
446+
project
401447
.getConfigurations()
402448
.forEach { conf =>
403449
if (conf.isCanBeResolved()) {

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

+1-1
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)