Skip to content

Commit edc3480

Browse files
committed
Support for multi-project find all references
This commit adapts the `find all references` feature so that it is able to find references to given symbols across projects. For symbols that come from the classpath, all the projects in the build will be inspected. If we're able to find a source for the symbol, only the projects that depend on the projects where we found the definition need to be inspected.
1 parent 5e36b01 commit edc3480

File tree

7 files changed

+358
-91
lines changed

7 files changed

+358
-91
lines changed

compiler/src/dotty/tools/dotc/interactive/Interactive.scala

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,4 +490,32 @@ object Interactive {
490490
}
491491
}
492492

493+
/**
494+
* Given `sym`, originating from `sourceDriver`, find its representation in
495+
* `targetDriver`.
496+
*
497+
* @param symbol The symbol to expression in the new driver.
498+
* @param sourceDriver The driver from which `symbol` originates.
499+
* @param targetDriver The driver in which we want to get a representation of `symbol`.
500+
* @return A representation of `symbol` in `targetDriver`.
501+
*/
502+
def localize(symbol: Symbol, sourceDriver: InteractiveDriver, targetDriver: InteractiveDriver): Symbol = {
503+
504+
def in[T](driver: InteractiveDriver)(fn: Context => T): T =
505+
fn(driver.currentCtx)
506+
507+
if (sourceDriver == targetDriver) symbol
508+
else {
509+
val owners = in(sourceDriver) { implicit ctx =>
510+
symbol.ownersIterator.toList.reverse.map(_.name)
511+
}
512+
in(targetDriver) { implicit ctx =>
513+
val base: Symbol = ctx.definitions.RootClass
514+
owners.tail.foldLeft(base) { (prefix, symbolName) =>
515+
prefix.info.member(symbolName).symbol
516+
}
517+
}
518+
}
519+
}
520+
493521
}

compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala

Lines changed: 102 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -53,61 +53,85 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
5353
def openedTrees: Map[URI, List[SourceTree]] = myOpenedTrees
5454
def compilationUnits: Map[URI, CompilationUnit] = myCompilationUnits
5555

56+
/**
57+
* The trees for all the source files in this project.
58+
*
59+
* This includes the trees for the buffers that are presently open in the IDE, and the trees
60+
* from the target directory.
61+
*/
62+
def sourceTrees(implicit ctx: Context): List[SourceTree] = sourceTreesContaining("")
63+
64+
/**
65+
* The trees for all the source files in this project that contain `id`.
66+
*
67+
* This includes the trees for the buffers that are presently open in the IDE, and the trees
68+
* from the target directory.
69+
*/
70+
def sourceTreesContaining(id: String)(implicit ctx: Context): List[SourceTree] = {
71+
val fromBuffers = openedTrees.values.flatten.toList
72+
val fromCompilationOutput = {
73+
val classNames = new mutable.ListBuffer[String]
74+
val output = ctx.settings.outputDir.value
75+
if (output.isDirectory) {
76+
classesFromDir(output.jpath, classNames)
77+
} else {
78+
val zipFile = new ZipFile(output.file)
79+
classesFromZip(zipFile, classNames)
80+
}
81+
classNames.flatMap { cls =>
82+
val className = cls.toTypeName
83+
treesFromClassName(className, id = "")
84+
}
85+
}
86+
(fromBuffers ++ fromCompilationOutput).distinct
87+
}
88+
89+
/**
90+
* All the trees for this project.
91+
*
92+
* This includes the trees of the sources of this project, along with the trees that are found
93+
* on this project's classpath.
94+
*/
5695
def allTrees(implicit ctx: Context): List[SourceTree] = allTreesContaining("")
5796

97+
/**
98+
* All the trees for this project that contain `id`.
99+
*
100+
* This includes the trees of the sources of this project, along with the trees that are found
101+
* on this project's classpath.
102+
*/
58103
def allTreesContaining(id: String)(implicit ctx: Context): List[SourceTree] = {
59104
val fromSource = openedTrees.values.flatten.toList
60105
val fromClassPath = (dirClassPathClasses ++ zipClassPathClasses).flatMap { cls =>
61106
val className = cls.toTypeName
62-
List(tree(className, id), tree(className.moduleClassName, id)).flatten
107+
treesFromClassName(className, id)
63108
}
64109
(fromSource ++ fromClassPath).distinct
65110
}
66111

67-
private def tree(className: TypeName, id: String)(implicit ctx: Context): Option[SourceTree] = {
68-
val clsd = ctx.base.staticRef(className)
69-
clsd match {
70-
case clsd: ClassDenotation =>
71-
clsd.ensureCompleted()
72-
SourceTree.fromSymbol(clsd.symbol.asClass, id)
73-
case _ =>
74-
None
112+
/**
113+
* The `SourceTree`s that define the class `className` and/or module `className`.
114+
*
115+
* @see SourceTree.fromSymbol
116+
*/
117+
private def treesFromClassName(className: TypeName, id: String)(implicit ctx: Context): List[SourceTree] = {
118+
def tree(className: TypeName, id: String): Option[SourceTree] = {
119+
val clsd = ctx.base.staticRef(className)
120+
clsd match {
121+
case clsd: ClassDenotation =>
122+
clsd.ensureCompleted()
123+
SourceTree.fromSymbol(clsd.symbol.asClass, id)
124+
case _ =>
125+
None
126+
}
75127
}
128+
List(tree(className, id), tree(className.moduleClassName, id)).flatten
76129
}
77130

78131
// Presence of a file with one of these suffixes indicates that the
79132
// corresponding class has been pickled with TASTY.
80133
private val tastySuffixes = List(".hasTasty", ".tasty")
81134

82-
private def classNames(cp: ClassPath, packageName: String): List[String] = {
83-
def className(classSegments: List[String]) =
84-
classSegments.mkString(".").stripSuffix(".class")
85-
86-
val ClassPathEntries(pkgs, classReps) = cp.list(packageName)
87-
88-
classReps
89-
.filter((classRep: ClassRepresentation) => classRep.binary match {
90-
case None =>
91-
true
92-
case Some(binFile) =>
93-
val prefix =
94-
if (binFile.name.endsWith(".class"))
95-
binFile.name.stripSuffix(".class")
96-
else
97-
null
98-
prefix != null && {
99-
binFile match {
100-
case pf: PlainFile =>
101-
tastySuffixes.map(suffix => pf.givenPath.parent / (prefix + suffix)).exists(_.exists)
102-
case _ =>
103-
sys.error(s"Unhandled file type: $binFile [getClass = ${binFile.getClass}]")
104-
}
105-
}
106-
})
107-
.map(classRep => (packageName ++ (if (packageName != "") "." else "") ++ classRep.name)).toList ++
108-
pkgs.flatMap(pkg => classNames(cp, pkg.name))
109-
}
110-
111135
// FIXME: All the code doing classpath handling is very fragile and ugly,
112136
// improving this requires changing the dotty classpath APIs to handle our usecases.
113137
// We also need something like sbt server-mode to be informed of changes on
@@ -128,17 +152,13 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
128152
}
129153

130154
// Like in `ZipArchiveFileLookup` we assume that zips are immutable
131-
private val zipClassPathClasses: Seq[String] = zipClassPaths.flatMap { zipCp =>
132-
val zipFile = new ZipFile(zipCp.zipFile)
133-
134-
try {
135-
for {
136-
entry <- zipFile.stream.toArray((size: Int) => new Array[ZipEntry](size))
137-
name = entry.getName
138-
tastySuffix <- tastySuffixes.find(name.endsWith)
139-
} yield name.replace("/", ".").stripSuffix(tastySuffix)
155+
private val zipClassPathClasses: Seq[String] = {
156+
val names = new mutable.ListBuffer[String]
157+
zipClassPaths.foreach { zipCp =>
158+
val zipFile = new ZipFile(zipCp.zipFile)
159+
classesFromZip(zipFile, names)
140160
}
141-
finally zipFile.close()
161+
names
142162
}
143163

144164
// FIXME: classfiles in directories may change at any point, so we retraverse
@@ -148,26 +168,43 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
148168
val names = new mutable.ListBuffer[String]
149169
dirClassPaths.foreach { dirCp =>
150170
val root = dirCp.dir.toPath
151-
try
152-
Files.walkFileTree(root, new SimpleFileVisitor[Path] {
153-
override def visitFile(path: Path, attrs: BasicFileAttributes) = {
154-
if (!attrs.isDirectory) {
155-
val name = path.getFileName.toString
156-
for {
157-
tastySuffix <- tastySuffixes
158-
if name.endsWith(tastySuffix)
159-
} {
160-
names += root.relativize(path).toString.replace("/", ".").stripSuffix(tastySuffix)
161-
}
171+
classesFromDir(root, names)
172+
}
173+
names.toList
174+
}
175+
176+
/** Adds the names of the classes that are defined in `zipFile` to `buffer`. */
177+
private def classesFromZip(zipFile: ZipFile, buffer: mutable.ListBuffer[String]): Unit = {
178+
try {
179+
for {
180+
entry <- zipFile.stream.toArray((size: Int) => new Array[ZipEntry](size))
181+
name = entry.getName
182+
tastySuffix <- tastySuffixes.find(name.endsWith)
183+
} buffer += name.replace("/", ".").stripSuffix(tastySuffix)
184+
}
185+
finally zipFile.close()
186+
}
187+
188+
/** Adds the names of the classes that are defined in `dir` to `buffer`. */
189+
private def classesFromDir(dir: Path, buffer: mutable.ListBuffer[String]): Unit = {
190+
try
191+
Files.walkFileTree(dir, new SimpleFileVisitor[Path] {
192+
override def visitFile(path: Path, attrs: BasicFileAttributes) = {
193+
if (!attrs.isDirectory) {
194+
val name = path.getFileName.toString
195+
for {
196+
tastySuffix <- tastySuffixes
197+
if name.endsWith(tastySuffix)
198+
} {
199+
buffer += dir.relativize(path).toString.replace("/", ".").stripSuffix(tastySuffix)
162200
}
163-
FileVisitResult.CONTINUE
164201
}
165-
})
166-
catch {
167-
case _: NoSuchFileException =>
168-
}
202+
FileVisitResult.CONTINUE
203+
}
204+
})
205+
catch {
206+
case _: NoSuchFileException =>
169207
}
170-
names.toList
171208
}
172209

173210
private def topLevelClassTrees(topTree: Tree, source: SourceFile): List[SourceTree] = {

language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class DottyLanguageServer extends LanguageServer
8080
val settings =
8181
defaultFlags ++
8282
config.compilerArguments.toList
83+
.update("-d", config.classDirectory.getAbsolutePath)
8384
.update("-classpath", (config.classDirectory +: config.dependencyClasspath).mkString(File.pathSeparator))
8485
.update("-sourcepath", config.sourceDirectories.mkString(File.pathSeparator)) :+
8586
"-scansource"
@@ -106,21 +107,37 @@ class DottyLanguageServer extends LanguageServer
106107
private def checkMemory() =
107108
if (Memory.isCritical()) CompletableFutures.computeAsync { _ => restart() }
108109

109-
/** The driver instance responsible for compiling `uri` */
110-
def driverFor(uri: URI): InteractiveDriver = thisServer.synchronized {
111-
val matchingConfig =
110+
/** The configuration of the project that owns `uri`. */
111+
def configFor(uri: URI): ProjectConfig = thisServer.synchronized {
112+
val config =
112113
drivers.keys.find(config => config.sourceDirectories.exists(sourceDir =>
113114
new File(uri.getPath).getCanonicalPath.startsWith(sourceDir.getCanonicalPath)))
114-
matchingConfig match {
115-
case Some(config) =>
116-
drivers(config)
117-
case None =>
118-
val config = drivers.keys.head
119-
// println(s"No configuration contains $uri as a source file, arbitrarily choosing ${config.id}")
120-
drivers(config)
115+
116+
config.getOrElse {
117+
val config = drivers.keys.head
118+
// println(s"No configuration contains $uri as a source file, arbitrarily choosing ${config.id}")
119+
config
121120
}
122121
}
123122

123+
/** The driver instance responsible for compiling `uri` */
124+
def driverFor(uri: URI): InteractiveDriver = {
125+
drivers(configFor(uri))
126+
}
127+
128+
/** The set of projects that transitively depend on `config` */
129+
def transitivelyDependentProjects(config: ProjectConfig): immutable.Set[ProjectConfig] = {
130+
val allProjects = drivers.keySet.toSet
131+
allProjects.filter(transitiveDependencies(_).contains(config))
132+
}
133+
134+
/** The set of transitive dependencies of `config`. */
135+
def transitiveDependencies(config: ProjectConfig): immutable.Set[ProjectConfig] = {
136+
val idToConfig = drivers.keys.map(k => k.id -> k).toMap
137+
val dependencies = config.dependencies.map(idToConfig).toSet
138+
dependencies ++ dependencies.flatMap(transitiveDependencies)
139+
}
140+
124141
def connect(client: WorksheetClient): Unit = {
125142
myClient = client
126143
}
@@ -281,22 +298,41 @@ class DottyLanguageServer extends LanguageServer
281298
val driver = driverFor(uri)
282299
implicit val ctx = driver.currentCtx
283300

301+
val includes = {
302+
val includeDeclaration = params.getContext.isIncludeDeclaration
303+
Include.references | Include.overriding | (if (includeDeclaration) Include.definitions else 0)
304+
}
305+
284306
val pos = sourcePosition(driver, uri, params.getPosition)
285-
val sym = Interactive.enclosingSourceSymbol(driver.openedTrees(uri), pos)
307+
val path = Interactive.pathTo(driver.openedTrees(uri), pos)
286308

287-
if (sym == NoSymbol) Nil.asJava
288-
else {
289-
// FIXME: this will search for references in all trees on the classpath, but we really
290-
// only need to look for trees in the target directory if the symbol is defined in the
291-
// current project
292-
val trees = driver.allTreesContaining(sym.name.sourceModuleName.toString)
293-
val includeDeclaration = params.getContext.isIncludeDeclaration
294-
val includes =
295-
Include.references | Include.overriding | (if (includeDeclaration) Include.definitions else 0)
296-
val refs = Interactive.findTreesMatching(trees, includes, sym)
309+
// Find definitions of the symbol under the cursor, so that we can determine
310+
// what projects are worth exploring
311+
val definitions = Interactive.findDefinitions(path, driver)
312+
val projectsToInspect =
313+
if (definitions.isEmpty) {
314+
drivers.keySet
315+
} else {
316+
definitions.flatMap { definition =>
317+
val config = configFor(toUri(definition.pos.source))
318+
transitivelyDependentProjects(config) + config
319+
}
320+
}
297321

298-
refs.flatMap(ref => location(ref.namePos, positionMapperFor(ref.source))).asJava
299-
}
322+
val originalSymbol = Interactive.enclosingSourceSymbol(path)
323+
val symbolName = originalSymbol.name.sourceModuleName.toString
324+
val references =
325+
for { config <- projectsToInspect.toList
326+
remoteDriver = drivers(config)
327+
ctx = remoteDriver.currentCtx
328+
remoteDefinition = Interactive.localize(originalSymbol, driver, remoteDriver)
329+
trees = remoteDriver.sourceTreesContaining(symbolName)(ctx)
330+
reference <- Interactive.findTreesMatching(trees, includes, remoteDefinition)(ctx)
331+
} yield {
332+
reference
333+
}
334+
335+
references.flatMap(ref => location(ref.namePos, positionMapperFor(ref.source))).asJava
300336
}
301337

302338
override def rename(params: RenameParams) = computeAsync { cancelToken =>

0 commit comments

Comments
 (0)