Skip to content

Commit 0f88828

Browse files
committed
Improve imports handling in the IDE
Imports are hard to handle in the IDE, because one statement may import several different symbols, but the imports remain untyped. Considering `import Foo.Bar`, this can mean that we're importing a type, a term or both. When looking for trees that match a given symbol (to implement `documentHighlight` in LSP, for instance) and encounter an import statement, we now consider all the symbols that the import statement could introduce.
1 parent 9663752 commit 0f88828

File tree

8 files changed

+215
-50
lines changed

8 files changed

+215
-50
lines changed

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

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import scala.collection._
88
import ast.{NavigateAST, Trees, tpd, untpd}
99
import core._, core.Decorators.{sourcePos => _, _}
1010
import Contexts._, Flags._, Names._, NameOps._, Symbols._, SymDenotations._, Trees._, Types._
11-
import util.Positions._, util.SourcePosition
11+
import util.Positions._, util.SourceFile, util.SourcePosition
1212
import core.Denotations.SingleDenotation
1313
import NameKinds.SimpleNameKind
1414
import config.Printers.interactiv
@@ -273,29 +273,58 @@ object Interactive {
273273
(implicit ctx: Context): List[SourceNamedTree] = safely {
274274
val buf = new mutable.ListBuffer[SourceNamedTree]
275275

276-
trees foreach {
277-
case SourceNamedTree(topTree, source) =>
278-
new untpd.TreeTraverser {
279-
override def traverse(tree: untpd.Tree)(implicit ctx: Context) = {
280-
tree match {
281-
case utree: untpd.NameTree if tree.hasType =>
282-
val tree = utree.asInstanceOf[tpd.NameTree]
283-
if (tree.symbol.exists
284-
&& !tree.symbol.is(Synthetic)
285-
&& tree.pos.exists
286-
&& !tree.pos.isZeroExtent
287-
&& (includeReferences || isDefinition(tree))
288-
&& treePredicate(tree))
289-
buf += SourceNamedTree(tree, source)
290-
traverseChildren(tree)
291-
case tree: untpd.Inlined =>
292-
traverse(tree.call)
293-
case _ =>
294-
traverseChildren(tree)
276+
def traverser(source: SourceFile) = {
277+
new untpd.TreeTraverser {
278+
private def handleImport(imported: List[Symbol],
279+
uexpr: untpd.Tree,
280+
id: untpd.Ident,
281+
rename: Option[untpd.Ident]): Unit = {
282+
val expr = uexpr.asInstanceOf[tpd.Tree]
283+
imported.foreach { sym =>
284+
val tree = tpd.Select(expr, sym.name).withPos(id.pos)
285+
val renameTree = rename.map { r =>
286+
val name = if (sym.name.isTypeName) r.name.toTypeName else r.name
287+
RenameTree(name, tpd.Select(expr, sym.name)).withPos(r.pos)
295288
}
289+
290+
traverse(tree)
291+
renameTree.foreach(traverse)
292+
}
293+
294+
traverse(expr)
295+
}
296+
override def traverse(tree: untpd.Tree)(implicit ctx: Context) = {
297+
tree match {
298+
case imp @ Import(uexpr, (id: untpd.Ident) :: Nil) =>
299+
val imported = importedSymbols(imp.asInstanceOf[tpd.Import])
300+
handleImport(imported, uexpr, id, None)
301+
case imp @ Import(uexpr, Thicket((id: untpd.Ident) :: (rename: untpd.Ident) :: Nil) :: Nil) =>
302+
val imported = importedSymbols(imp.asInstanceOf[tpd.Import])
303+
handleImport(imported, uexpr, id, Some(rename))
304+
case utree: untpd.NameTree if tree.hasType =>
305+
val tree = utree.asInstanceOf[tpd.NameTree]
306+
if (tree.symbol.exists
307+
&& !tree.symbol.is(Synthetic)
308+
&& tree.pos.exists
309+
&& !tree.pos.isZeroExtent
310+
&& (includeReferences || isDefinition(tree))
311+
&& treePredicate(tree))
312+
buf += SourceNamedTree(tree, source)
313+
traverseChildren(tree)
314+
case tree: untpd.Inlined =>
315+
traverse(tree.call)
316+
case _ =>
317+
traverseChildren(tree)
296318
}
297-
}.traverse(topTree)
298-
case _: SourceImportTree => ()
319+
}
320+
}
321+
}
322+
323+
trees foreach {
324+
case SourceNamedTree(topTree, source) =>
325+
traverser(source).traverse(topTree)
326+
case SourceImportTree(imp, source) =>
327+
traverser(source).traverse(imp)
299328
}
300329

301330
buf.toList
@@ -381,4 +410,43 @@ object Interactive {
381410
/** The first tree in the path that is a definition. */
382411
def enclosingDefinitionInPath(path: List[Tree])(implicit ctx: Context): Tree =
383412
path.find(_.isInstanceOf[DefTree]).getOrElse(EmptyTree)
413+
414+
/** All the symbols that are imported by import statement `imp`, if it matches
415+
* the predicate `selectorPredicate`.
416+
*
417+
* @param imp The import statement to analyze
418+
* @param selectorPredicate A test to find the selector to use.
419+
* @return The symbols imported.
420+
*/
421+
def importedSymbols(imp: tpd.Import,
422+
selectorPredicate: untpd.Tree => Boolean = util.common.alwaysTrue)
423+
(implicit ctx: Context): List[Symbol] = {
424+
def lookup0(name: Name): Symbol = imp.expr.tpe.member(name).symbol
425+
def lookup(name: Name): List[Symbol] = {
426+
lookup0(name.toTermName) ::
427+
lookup0(name.toTypeName) ::
428+
lookup0(name.moduleClassName) ::
429+
lookup0(name.sourceModuleName) :: Nil
430+
}
431+
432+
val symbols = imp.selectors.find(selectorPredicate) match {
433+
case Some(id: untpd.Ident) =>
434+
lookup(id.name)
435+
case Some(Thicket((id: untpd.Ident) :: (_: untpd.Ident) :: Nil)) =>
436+
lookup(id.name)
437+
case _ => Nil
438+
}
439+
440+
symbols.filter(_.exists)
441+
}
442+
443+
/** Used to represent a renaming import `{foo => bar}`.
444+
* We need this because the name of the tree must be the new name, but the
445+
* denotation must be that of the importee.
446+
*/
447+
private case class RenameTree(name: Name, underlying: Tree) extends NameTree {
448+
override def denot(implicit ctx: Context) = underlying.denot
449+
myTpe = NoType
450+
}
451+
384452
}

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

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -259,19 +259,8 @@ class DottyLanguageServer extends LanguageServer
259259
Interactive.namedTrees(trees, Include.overriding | Include.overridden, sym)
260260

261261
case imp: Import =>
262-
def lookup(name: Name): Symbol = {
263-
imp.expr.tpe.member(name).symbol
264-
}
265-
val importedSyms = imp.selectors.find(_.pos.contains(pos.pos)) match {
266-
case Some(id: Ident) =>
267-
lookup(id.name.toTermName) :: lookup(id.name.toTypeName) :: Nil
268-
case Some(Thicket((id: Ident) :: (_: Ident) :: Nil)) =>
269-
lookup(id.name.toTermName) :: lookup(id.name.toTypeName) :: Nil
270-
case _ =>
271-
Nil
272-
}
273-
274-
importedSyms.filter(_.exists).flatMap { sym =>
262+
val importedSyms = Interactive.importedSymbols(imp, _.pos.contains(pos.pos))
263+
importedSyms.flatMap { sym =>
275264
val trees = driver.allTreesContaining(sym.name.sourceModuleName.toString)
276265
val defSymbol = if (sym is Flags.ModuleVal) sym.moduleClass else sym
277266
Interactive.namedTrees(trees, Include.overriding, defSymbol)
@@ -344,15 +333,22 @@ class DottyLanguageServer extends LanguageServer
344333

345334
val pos = sourcePosition(driver, uri, params.getPosition)
346335
val uriTrees = driver.openedTrees(uri)
347-
val sym = Interactive.enclosingSourceSymbol(uriTrees, pos)
336+
val enclTree = Interactive.enclosingTree(driver.openedTrees(uri), pos)
348337

349-
if (sym == NoSymbol) Nil.asJava
350-
else {
351-
val refs = Interactive.namedTrees(uriTrees, Include.references | Include.overriding, sym)
352-
( for (ref <- refs if ref.namePos.exists)
353-
yield new DocumentHighlight(range(ref.namePos), DocumentHighlightKind.Read)
354-
).asJava
338+
val syms =
339+
enclTree match {
340+
case imp: Import =>
341+
Interactive.importedSymbols(imp, _.pos.contains(pos.pos))
342+
case _ =>
343+
List(Interactive.enclosingSourceSymbol(uriTrees, pos)).filter(_.exists)
344+
}
345+
346+
val refs = syms.flatMap { sym =>
347+
Interactive.namedTrees(uriTrees, Include.references | Include.overriding, sym)
355348
}
349+
( for (ref <- refs if ref.namePos.exists)
350+
yield new DocumentHighlight(range(ref.namePos), DocumentHighlightKind.Read)
351+
).asJava
356352
}
357353

358354
override def hover(params: TextDocumentPositionParams) = computeAsync { cancelToken =>

language-server/test/dotty/tools/languageserver/HighlightTest.scala

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,103 @@ class HighlightTest {
1919
.highlight(xRef.range, (xDef.range, DocumentHighlightKind.Read), (xRef.range, DocumentHighlightKind.Read))
2020
}
2121

22+
@Test def importHighlight0: Unit = {
23+
code"""object ${m1}Foo${m2} { def ${m5}bar${m6}: Int = 0 }
24+
trait Bar { import ${m3}Foo${m4}._; def buzz = ${m7}bar${m8} }
25+
trait Baz { def ${m9}bar${m10}: Int = 1 }""".withSource
26+
27+
.highlight(m1 to m2, (m1 to m2, DocumentHighlightKind.Read), (m3 to m4, DocumentHighlightKind.Read))
28+
.highlight(m3 to m4, (m1 to m2, DocumentHighlightKind.Read), (m3 to m4, DocumentHighlightKind.Read))
29+
.highlight(m5 to m6, (m5 to m6, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
30+
.highlight(m7 to m8, (m5 to m6, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
31+
.highlight(m9 to m10, (m9 to m10, DocumentHighlightKind.Read))
32+
}
33+
34+
@Test def importHighlight1: Unit = {
35+
code"""import ${m1}Foo${m2}._
36+
object ${m3}Foo${m4} { def ${m5}bar${m6}: Int = 0 }
37+
trait Bar { def buzz = ${m7}bar${m8} }""".withSource
38+
39+
.highlight(m1 to m2, (m1 to m2, DocumentHighlightKind.Read), (m3 to m4, DocumentHighlightKind.Read))
40+
.highlight(m3 to m4, (m1 to m2, DocumentHighlightKind.Read), (m3 to m4, DocumentHighlightKind.Read))
41+
.highlight(m5 to m6, (m5 to m6, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
42+
.highlight(m7 to m8, (m5 to m6, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
43+
}
44+
45+
@Test def importHighlight2: Unit = {
46+
val m11 = new util.embedded.CodeMarker("m11")
47+
val m12 = new util.embedded.CodeMarker("m12")
48+
49+
code"""object ${m1}Foo${m2} { object ${m3}Bar${m4} { object ${m5}Baz${m6} } }
50+
trait Buzz { import ${m7}Foo${m8}.${m9}Bar${m10}.${m11}Baz${m12} }""".withSource
51+
52+
.highlight(m1 to m2, (m1 to m2, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
53+
.highlight(m3 to m4, (m3 to m4, DocumentHighlightKind.Read), (m9 to m10, DocumentHighlightKind.Read))
54+
.highlight(m5 to m6, (m5 to m6, DocumentHighlightKind.Read), (m11 to m12, DocumentHighlightKind.Read))
55+
.highlight(m7 to m8, (m1 to m2, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
56+
.highlight(m9 to m10, (m3 to m4, DocumentHighlightKind.Read), (m9 to m10, DocumentHighlightKind.Read))
57+
.highlight(m11 to m12, (m5 to m6, DocumentHighlightKind.Read), (m11 to m12, DocumentHighlightKind.Read))
58+
}
59+
60+
@Test def importHighlight3: Unit = {
61+
code"""import ${m1}Foo${m2}.${m3}Bar${m4}
62+
object ${m5}Foo${m6} { object ${m7}Bar${m8} }""".withSource
63+
64+
.highlight(m1 to m2, (m1 to m2, DocumentHighlightKind.Read), (m5 to m6, DocumentHighlightKind.Read))
65+
.highlight(m3 to m4, (m3 to m4, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
66+
.highlight(m5 to m6, (m1 to m2, DocumentHighlightKind.Read), (m5 to m6, DocumentHighlightKind.Read))
67+
.highlight(m7 to m8, (m3 to m4, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
68+
}
69+
70+
@Test def importHighlightClassAndCompanion: Unit = {
71+
code"""object Foo { object ${m1}Bar${m2}; class ${m3}Bar${m4} }
72+
trait Buzz { import Foo.${m5}Bar${m6} }""".withSource
73+
.highlight(m1 to m2, (m1 to m2, DocumentHighlightKind.Read), (m5 to m6, DocumentHighlightKind.Read))
74+
.highlight(m3 to m4, (m3 to m4, DocumentHighlightKind.Read), (m5 to m6, DocumentHighlightKind.Read))
75+
.highlight(m5 to m6, (m3 to m4, DocumentHighlightKind.Read), (m5 to m6, DocumentHighlightKind.Read), (m1 to m2, DocumentHighlightKind.Read))
76+
}
77+
78+
@Test def importHighlightWithRename: Unit = {
79+
val m11 = new util.embedded.CodeMarker("m11")
80+
val m12 = new util.embedded.CodeMarker("m12")
81+
val m13 = new util.embedded.CodeMarker("m11")
82+
val m14 = new util.embedded.CodeMarker("m12")
83+
84+
code"""object ${m1}Foo${m2} { object ${m3}Bar${m4} { object ${m5}Baz${m6} } }
85+
trait Buzz { import ${m7}Foo${m8}.${m9}Bar${m10}.{${m11}Baz${m12} => ${m13}Quux${m14}}""".withSource
86+
87+
.highlight(m1 to m2, (m1 to m2, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
88+
.highlight(m3 to m4, (m3 to m4, DocumentHighlightKind.Read), (m9 to m10, DocumentHighlightKind.Read))
89+
.highlight(m5 to m6, (m5 to m6, DocumentHighlightKind.Read), (m11 to m12, DocumentHighlightKind.Read), (m13 to m14, DocumentHighlightKind.Read))
90+
.highlight(m7 to m8, (m1 to m2, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
91+
.highlight(m9 to m10, (m3 to m4, DocumentHighlightKind.Read), (m9 to m10, DocumentHighlightKind.Read))
92+
.highlight(m11 to m12, (m5 to m6, DocumentHighlightKind.Read), (m11 to m12, DocumentHighlightKind.Read), (m13 to m14, DocumentHighlightKind.Read))
93+
.highlight(m13 to m14, (m5 to m6, DocumentHighlightKind.Read), (m11 to m12, DocumentHighlightKind.Read), (m13 to m14, DocumentHighlightKind.Read))
94+
}
95+
96+
@Test def importHighlightClassAndCompanionWithRename: Unit = {
97+
val m11 = new util.embedded.CodeMarker("m11")
98+
val m12 = new util.embedded.CodeMarker("m12")
99+
100+
code"""object ${m1}Foo${m2} { object ${m3}Bar${m4}; class ${m5}Bar${m6} }
101+
trait Buzz { import ${m7}Foo${m8}.{${m9}Bar${m10} => ${m11}Baz${m12}} }""".withSource
102+
103+
.highlight(m1 to m2, (m1 to m2, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
104+
.highlight(m3 to m4, (m3 to m4, DocumentHighlightKind.Read), (m9 to m10, DocumentHighlightKind.Read), (m11 to m12, DocumentHighlightKind.Read))
105+
.highlight(m5 to m6, (m5 to m6, DocumentHighlightKind.Read), (m9 to m10, DocumentHighlightKind.Read), (m11 to m12, DocumentHighlightKind.Read))
106+
.highlight(m7 to m8, (m1 to m2, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
107+
.highlight(m9 to m10, (m3 to m4, DocumentHighlightKind.Read), (m5 to m6, DocumentHighlightKind.Read), (m9 to m10, DocumentHighlightKind.Read), (m11 to m12, DocumentHighlightKind.Read))
108+
.highlight(m11 to m12, (m3 to m4, DocumentHighlightKind.Read), (m5 to m6, DocumentHighlightKind.Read), (m9 to m10, DocumentHighlightKind.Read), (m11 to m12, DocumentHighlightKind.Read))
109+
}
110+
111+
@Test def importHighlightMembers: Unit = {
112+
code"""object Foo { def ${m1}bar${m2} = 2; type ${m3}bar${m4} = fizz; class fizz }
113+
trait Quux { import Foo.{${m5}bar${m6} => ${m7}buzz${m8}} }""".withSource
114+
115+
.highlight(m1 to m2, (m1 to m2, DocumentHighlightKind.Read), (m5 to m6, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
116+
.highlight(m3 to m4, (m3 to m4, DocumentHighlightKind.Read), (m5 to m6, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
117+
.highlight(m5 to m6, (m1 to m2, DocumentHighlightKind.Read), (m3 to m4, DocumentHighlightKind.Read), (m5 to m6, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
118+
.highlight(m7 to m8, (m1 to m2, DocumentHighlightKind.Read), (m3 to m4, DocumentHighlightKind.Read), (m5 to m6, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
119+
}
120+
22121
}

language-server/test/dotty/tools/languageserver/util/Code.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ object Code {
2020
val m6 = new CodeMarker("m6")
2121
val m7 = new CodeMarker("m7")
2222
val m8 = new CodeMarker("m8")
23+
val m9 = new CodeMarker("m9")
24+
val m10 = new CodeMarker("m10")
2325

2426
implicit class CodeHelper(val sc: StringContext) extends AnyVal {
2527

language-server/test/dotty/tools/languageserver/util/actions/CodeDefinition.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import org.junit.Assert.assertEquals
1717
class CodeDefinition(override val range: CodeRange, expected: Seq[CodeRange]) extends ActionOnRange {
1818

1919
override def onMarker(marker: CodeMarker): Exec[Unit] = {
20-
val results = server.definition(marker.toTextDocumentPositionParams).get().asScala.toSeq
21-
val expectedLocations = expected.map(_.toLocation)
20+
val results = server.definition(marker.toTextDocumentPositionParams).get().asScala.toSet
21+
val expectedLocations = expected.map(_.toLocation).toSet
2222

2323
assertEquals(expectedLocations, results)
2424
}

language-server/test/dotty/tools/languageserver/util/actions/CodeDocumentHighlight.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ class CodeDocumentHighlight(override val range: CodeRange,
2020
expected: Seq[(CodeRange, DocumentHighlightKind)]) extends ActionOnRange {
2121

2222
override def onMarker(marker: CodeMarker): Exec[Unit] = {
23-
val expectedPairs = expected.map { case (codeRange, kind) => (codeRange.toRange, kind) }
23+
val expectedPairs = expected.map { case (codeRange, kind) => (codeRange.toRange, kind) }.toSet
2424
val results = server.documentHighlight(marker.toTextDocumentPositionParams).get()
25-
val resultPairs = results.asScala.map { result => (result.getRange, result.getKind) }
25+
val resultPairs = results.asScala.map { result => (result.getRange, result.getKind) }.toSet
2626

2727
assertEquals(expectedPairs, resultPairs)
2828
}

language-server/test/dotty/tools/languageserver/util/actions/CodeDocumentSymbol.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import scala.collection.JavaConverters._
1717
class CodeDocumentSymbol(override val marker: CodeMarker, expected: Seq[SymInfo]) extends ActionOnMarker {
1818

1919
override def execute(): Exec[Unit] = {
20-
val results = server.documentSymbol(marker.toDocumentSymbolParams).get().asScala
21-
val expectedSymInfos = expected.map(_.toSymInformation)
20+
val results = server.documentSymbol(marker.toDocumentSymbolParams).get().asScala.toSet
21+
val expectedSymInfos = expected.map(_.toSymInformation).toSet
2222

2323
assertEquals(expectedSymInfos, results)
2424
}

language-server/test/dotty/tools/languageserver/util/actions/CodeReferences.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ class CodeReferences(override val range: CodeRange,
2020
withDecl: Boolean) extends ActionOnRange {
2121

2222
override def onMarker(marker: CodeMarker): Exec[Unit] = {
23-
val expectedLocations = expected.map(_.toLocation)
24-
val results = server.references(marker.toReferenceParams(withDecl)).get().asScala
23+
val expectedLocations = expected.map(_.toLocation).toSet
24+
val results = server.references(marker.toReferenceParams(withDecl)).get().asScala.toSet
2525

2626
assertEquals(expectedLocations, results)
2727
}

0 commit comments

Comments
 (0)