Skip to content

Commit 6a315ff

Browse files
authored
Merge pull request #5209 from dotty-staging/topic/ide-multi-project-references
Support for multi-project `find all references`
2 parents 9eed6e6 + d5280a6 commit 6a315ff

File tree

8 files changed

+589
-153
lines changed

8 files changed

+589
-153
lines changed

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,4 +490,33 @@ 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+
if (prefix.exists) prefix.info.member(symbolName).symbol
516+
else NoSymbol
517+
}
518+
}
519+
}
520+
}
521+
493522
}

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

Lines changed: 170 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -36,78 +36,27 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
3636
}
3737

3838
private[this] var myCtx: Context = myInitCtx
39-
4039
def currentCtx: Context = myCtx
4140

41+
private val compiler: Compiler = new InteractiveCompiler
42+
4243
private val myOpenedFiles = new mutable.LinkedHashMap[URI, SourceFile] {
4344
override def default(key: URI) = NoSource
4445
}
46+
def openedFiles: Map[URI, SourceFile] = myOpenedFiles
4547

4648
private val myOpenedTrees = new mutable.LinkedHashMap[URI, List[SourceTree]] {
4749
override def default(key: URI) = Nil
4850
}
51+
def openedTrees: Map[URI, List[SourceTree]] = myOpenedTrees
4952

5053
private val myCompilationUnits = new mutable.LinkedHashMap[URI, CompilationUnit]
51-
52-
def openedFiles: Map[URI, SourceFile] = myOpenedFiles
53-
def openedTrees: Map[URI, List[SourceTree]] = myOpenedTrees
5454
def compilationUnits: Map[URI, CompilationUnit] = myCompilationUnits
5555

56-
def allTrees(implicit ctx: Context): List[SourceTree] = allTreesContaining("")
57-
58-
def allTreesContaining(id: String)(implicit ctx: Context): List[SourceTree] = {
59-
val fromSource = openedTrees.values.flatten.toList
60-
val fromClassPath = (dirClassPathClasses ++ zipClassPathClasses).flatMap { cls =>
61-
val className = cls.toTypeName
62-
List(tree(className, id), tree(className.moduleClassName, id)).flatten
63-
}
64-
(fromSource ++ fromClassPath).distinct
65-
}
66-
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
75-
}
76-
}
77-
7856
// Presence of a file with one of these suffixes indicates that the
7957
// corresponding class has been pickled with TASTY.
8058
private val tastySuffixes = List(".hasTasty", ".tasty")
8159

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-
11160
// FIXME: All the code doing classpath handling is very fragile and ugly,
11261
// improving this requires changing the dotty classpath APIs to handle our usecases.
11362
// We also need something like sbt server-mode to be informed of changes on
@@ -128,46 +77,173 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
12877
}
12978

13079
// 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)
80+
private val zipClassPathClasses: Seq[TypeName] = {
81+
val names = new mutable.ListBuffer[TypeName]
82+
zipClassPaths.foreach { zipCp =>
83+
val zipFile = new ZipFile(zipCp.zipFile)
84+
classesFromZip(zipFile, names)
85+
}
86+
names
87+
}
88+
89+
initialize()
90+
91+
/**
92+
* The trees for all the source files in this project.
93+
*
94+
* This includes the trees for the buffers that are presently open in the IDE, and the trees
95+
* from the target directory.
96+
*/
97+
def sourceTrees(implicit ctx: Context): List[SourceTree] = sourceTreesContaining("")
98+
99+
/**
100+
* The trees for all the source files in this project that contain `id`.
101+
*
102+
* This includes the trees for the buffers that are presently open in the IDE, and the trees
103+
* from the target directory.
104+
*/
105+
def sourceTreesContaining(id: String)(implicit ctx: Context): List[SourceTree] = {
106+
val fromBuffers = openedTrees.values.flatten.toList
107+
val fromCompilationOutput = {
108+
val classNames = new mutable.ListBuffer[TypeName]
109+
val output = ctx.settings.outputDir.value
110+
if (output.isDirectory) {
111+
classesFromDir(output.jpath, classNames)
112+
} else {
113+
val zipFile = new ZipFile(output.file)
114+
classesFromZip(zipFile, classNames)
115+
}
116+
classNames.flatMap { cls =>
117+
treesFromClassName(cls, id)
118+
}
119+
}
120+
(fromBuffers ++ fromCompilationOutput).distinct
121+
}
133122

123+
/**
124+
* All the trees for this project.
125+
*
126+
* This includes the trees of the sources of this project, along with the trees that are found
127+
* on this project's classpath.
128+
*/
129+
def allTrees(implicit ctx: Context): List[SourceTree] = allTreesContaining("")
130+
131+
/**
132+
* All the trees for this project that contain `id`.
133+
*
134+
* This includes the trees of the sources of this project, along with the trees that are found
135+
* on this project's classpath.
136+
*/
137+
def allTreesContaining(id: String)(implicit ctx: Context): List[SourceTree] = {
138+
val fromSource = openedTrees.values.flatten.toList
139+
val fromClassPath = (dirClassPathClasses ++ zipClassPathClasses).flatMap { cls =>
140+
treesFromClassName(cls, id)
141+
}
142+
(fromSource ++ fromClassPath).distinct
143+
}
144+
145+
def run(uri: URI, sourceCode: String): List[MessageContainer] = run(uri, toSource(uri, sourceCode))
146+
147+
def run(uri: URI, source: SourceFile): List[MessageContainer] = {
148+
val previousCtx = myCtx
134149
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)
150+
val reporter =
151+
new StoreReporter(null) with UniqueMessagePositions with HideNonSensicalMessages
152+
153+
val run = compiler.newRun(myInitCtx.fresh.setReporter(reporter))
154+
myCtx = run.runContext
155+
156+
implicit val ctx = myCtx
157+
158+
myOpenedFiles(uri) = source
159+
160+
run.compileSources(List(source))
161+
run.printSummary()
162+
val unit = ctx.run.units.head
163+
val t = unit.tpdTree
164+
cleanup(t)
165+
myOpenedTrees(uri) = topLevelClassTrees(t, source)
166+
myCompilationUnits(uri) = unit
167+
168+
reporter.removeBufferedMessages
169+
}
170+
catch {
171+
case ex: FatalError =>
172+
myCtx = previousCtx
173+
close(uri)
174+
Nil
175+
}
176+
}
177+
178+
def close(uri: URI): Unit = {
179+
myOpenedFiles.remove(uri)
180+
myOpenedTrees.remove(uri)
181+
myCompilationUnits.remove(uri)
182+
}
183+
184+
/**
185+
* The `SourceTree`s that define the class `className` and/or module `className`.
186+
*
187+
* @see SourceTree.fromSymbol
188+
*/
189+
private def treesFromClassName(className: TypeName, id: String)(implicit ctx: Context): List[SourceTree] = {
190+
def tree(className: TypeName, id: String): Option[SourceTree] = {
191+
val clsd = ctx.base.staticRef(className)
192+
clsd match {
193+
case clsd: ClassDenotation =>
194+
clsd.ensureCompleted()
195+
SourceTree.fromSymbol(clsd.symbol.asClass, id)
196+
case _ =>
197+
None
198+
}
140199
}
141-
finally zipFile.close()
200+
List(tree(className, id), tree(className.moduleClassName, id)).flatten
142201
}
143202

144203
// FIXME: classfiles in directories may change at any point, so we retraverse
145204
// the directories each time, if we knew when classfiles changed (sbt
146205
// server-mode might help here), we could do cache invalidation instead.
147-
private def dirClassPathClasses: Seq[String] = {
148-
val names = new mutable.ListBuffer[String]
206+
private def dirClassPathClasses: Seq[TypeName] = {
207+
val names = new mutable.ListBuffer[TypeName]
149208
dirClassPaths.foreach { dirCp =>
150209
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-
}
210+
classesFromDir(root, names)
211+
}
212+
names
213+
}
214+
215+
/** Adds the names of the classes that are defined in `zipFile` to `buffer`. */
216+
private def classesFromZip(zipFile: ZipFile, buffer: mutable.ListBuffer[TypeName]): Unit = {
217+
try {
218+
for {
219+
entry <- zipFile.stream.toArray((size: Int) => new Array[ZipEntry](size))
220+
name = entry.getName
221+
tastySuffix <- tastySuffixes.find(name.endsWith)
222+
} buffer += name.replace("/", ".").stripSuffix(tastySuffix).toTypeName
223+
}
224+
finally zipFile.close()
225+
}
226+
227+
/** Adds the names of the classes that are defined in `dir` to `buffer`. */
228+
private def classesFromDir(dir: Path, buffer: mutable.ListBuffer[TypeName]): Unit = {
229+
try
230+
Files.walkFileTree(dir, new SimpleFileVisitor[Path] {
231+
override def visitFile(path: Path, attrs: BasicFileAttributes) = {
232+
if (!attrs.isDirectory) {
233+
val name = path.getFileName.toString
234+
for {
235+
tastySuffix <- tastySuffixes
236+
if name.endsWith(tastySuffix)
237+
} {
238+
buffer += dir.relativize(path).toString.replace("/", ".").stripSuffix(tastySuffix).toTypeName
162239
}
163-
FileVisitResult.CONTINUE
164240
}
165-
})
166-
catch {
167-
case _: NoSuchFileException =>
168-
}
241+
FileVisitResult.CONTINUE
242+
}
243+
})
244+
catch {
245+
case _: NoSuchFileException =>
169246
}
170-
names.toList
171247
}
172248

173249
private def topLevelClassTrees(topTree: Tree, source: SourceFile): List[SourceTree] = {
@@ -185,8 +261,6 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
185261
trees.toList
186262
}
187263

188-
private val compiler: Compiler = new InteractiveCompiler
189-
190264
/** Remove attachments and error out completers. The goal is to avoid
191265
* having a completer hanging in a typed tree which can capture the context
192266
* of a previous run. Note that typed trees can have untyped or partially
@@ -224,44 +298,20 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
224298
new SourceFile(virtualFile, Codec.UTF8)
225299
}
226300

227-
def run(uri: URI, sourceCode: String): List[MessageContainer] = run(uri, toSource(uri, sourceCode))
228-
229-
def run(uri: URI, source: SourceFile): List[MessageContainer] = {
230-
val previousCtx = myCtx
231-
try {
232-
val reporter =
233-
new StoreReporter(null) with UniqueMessagePositions with HideNonSensicalMessages
234-
235-
val run = compiler.newRun(myInitCtx.fresh.setReporter(reporter))
236-
myCtx = run.runContext
237-
238-
implicit val ctx = myCtx
239-
240-
myOpenedFiles(uri) = source
241-
242-
run.compileSources(List(source))
243-
run.printSummary()
244-
val unit = ctx.run.units.head
245-
val t = unit.tpdTree
246-
cleanup(t)
247-
myOpenedTrees(uri) = topLevelClassTrees(t, source)
248-
myCompilationUnits(uri) = unit
249-
250-
reporter.removeBufferedMessages
251-
}
252-
catch {
253-
case ex: FatalError =>
254-
myCtx = previousCtx
255-
close(uri)
256-
Nil
257-
}
301+
/**
302+
* Initialize this driver and compiler.
303+
*
304+
* This is necessary because an `InteractiveDriver` can be put to work without having
305+
* compiled anything (for instance, resolving a symbol coming from a different compiler in
306+
* this compiler). In those cases, an un-initialized compiler may crash (for instance if
307+
* late-compilation is needed).
308+
*/
309+
private[this] def initialize(): Unit = {
310+
val run = compiler.newRun(myInitCtx.fresh)
311+
myCtx = run.runContext
312+
run.compileUnits(Nil, myCtx)
258313
}
259314

260-
def close(uri: URI): Unit = {
261-
myOpenedFiles.remove(uri)
262-
myOpenedTrees.remove(uri)
263-
myCompilationUnits.remove(uri)
264-
}
265315
}
266316

267317
object InteractiveDriver {

0 commit comments

Comments
 (0)