Skip to content

Commit 3f84ea0

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 c79a2ce commit 3f84ea0

File tree

7 files changed

+213
-48
lines changed

7 files changed

+213
-48
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
@@ -280,29 +280,58 @@ object Interactive {
280280
(implicit ctx: Context): List[SourceNamedTree] = safely {
281281
val buf = new mutable.ListBuffer[SourceNamedTree]
282282

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

308337
buf.toList
@@ -417,4 +446,43 @@ object Interactive {
417446
/** The first tree in the path that is a definition. */
418447
def enclosingDefinitionInPath(path: List[Tree])(implicit ctx: Context): Tree =
419448
path.find(_.isInstanceOf[DefTree]).getOrElse(EmptyTree)
449+
450+
/** All the symbols that are imported by import statement `imp`, if it matches
451+
* the predicate `selectorPredicate`.
452+
*
453+
* @param imp The import statement to analyze
454+
* @param selectorPredicate A test to find the selector to use.
455+
* @return The symbols imported.
456+
*/
457+
def importedSymbols(imp: tpd.Import,
458+
selectorPredicate: untpd.Tree => Boolean = util.common.alwaysTrue)
459+
(implicit ctx: Context): List[Symbol] = {
460+
def lookup0(name: Name): Symbol = imp.expr.tpe.member(name).symbol
461+
def lookup(name: Name): List[Symbol] = {
462+
lookup0(name.toTermName) ::
463+
lookup0(name.toTypeName) ::
464+
lookup0(name.moduleClassName) ::
465+
lookup0(name.sourceModuleName) :: Nil
466+
}
467+
468+
val symbols = imp.selectors.find(selectorPredicate) match {
469+
case Some(id: untpd.Ident) =>
470+
lookup(id.name)
471+
case Some(Thicket((id: untpd.Ident) :: (_: untpd.Ident) :: Nil)) =>
472+
lookup(id.name)
473+
case _ => Nil
474+
}
475+
476+
symbols.filter(_.exists)
477+
}
478+
479+
/** Used to represent a renaming import `{foo => bar}`.
480+
* We need this because the name of the tree must be the new name, but the
481+
* denotation must be that of the importee.
482+
*/
483+
private case class RenameTree(name: Name, underlying: Tree) extends NameTree {
484+
override def denot(implicit ctx: Context) = underlying.denot
485+
myTpe = NoType
486+
}
487+
420488
}

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

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

284284
case imp: Import =>
285-
def lookup(name: Name): Symbol = {
286-
imp.expr.tpe.member(name).symbol
287-
}
288-
val importedSyms = imp.selectors.find(_.pos.contains(pos.pos)) match {
289-
case Some(id: Ident) =>
290-
lookup(id.name.toTermName) :: lookup(id.name.toTypeName) :: Nil
291-
case Some(Thicket((id: Ident) :: (_: Ident) :: Nil)) =>
292-
lookup(id.name.toTermName) :: lookup(id.name.toTypeName) :: Nil
293-
case _ =>
294-
Nil
295-
}
296-
297-
importedSyms.filter(_.exists).flatMap { sym =>
285+
val importedSyms = Interactive.importedSymbols(imp, _.pos.contains(pos.pos))
286+
importedSyms.flatMap { sym =>
298287
val trees = driver.allTreesContaining(sym.name.sourceModuleName.toString)
299288
val defSymbol = if (sym is Flags.ModuleVal) sym.moduleClass else sym
300289
Interactive.namedTrees(trees, Include.overriding, defSymbol)
@@ -364,15 +353,22 @@ class DottyLanguageServer extends LanguageServer
364353

365354
val pos = sourcePosition(driver, uri, params.getPosition)
366355
val uriTrees = driver.openedTrees(uri)
367-
val sym = Interactive.enclosingSourceSymbol(uriTrees, pos)
356+
val enclTree = Interactive.enclosingTree(driver.openedTrees(uri), pos)
368357

369-
if (sym == NoSymbol) Nil.asJava
370-
else {
371-
val refs = Interactive.namedTrees(uriTrees, Include.references | Include.overriding, sym)
372-
( for (ref <- refs if ref.namePos.exists)
373-
yield new DocumentHighlight(range(ref.namePos), DocumentHighlightKind.Read)
374-
).asJava
358+
val syms =
359+
enclTree match {
360+
case imp: Import =>
361+
Interactive.importedSymbols(imp, _.pos.contains(pos.pos))
362+
case _ =>
363+
List(Interactive.enclosingSourceSymbol(uriTrees, pos)).filter(_.exists)
364+
}
365+
366+
val refs = syms.flatMap { sym =>
367+
Interactive.namedTrees(uriTrees, Include.references | Include.overriding, sym)
375368
}
369+
( for (ref <- refs if ref.namePos.exists)
370+
yield new DocumentHighlight(range(ref.namePos), DocumentHighlightKind.Read)
371+
).asJava
376372
}
377373

378374
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/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)