Skip to content

Commit 01b55f8

Browse files
committed
IDE: Support renaming renaming imports
1 parent 148d3cc commit 01b55f8

File tree

4 files changed

+255
-19
lines changed

4 files changed

+255
-19
lines changed

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

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -617,10 +617,132 @@ object Interactive {
617617
* @return True, if this tree's name is different than its symbol's name, indicating that
618618
* it uses a renaming introduced by an import statement.
619619
*/
620-
private def isRenamed(tree: NameTree)(implicit ctx: Context): Boolean = {
620+
def isRenamed(tree: NameTree)(implicit ctx: Context): Boolean = {
621621
val symbol = tree.symbol
622-
symbol.exists &&
623-
tree.name.stripModuleClassSuffix != symbol.name.stripModuleClassSuffix
622+
symbol.exists && !sameName(tree.name, symbol.name)
623+
}
624+
625+
/** Are the two names the same? */
626+
def sameName(n0: Name, n1: Name): Boolean = {
627+
n0.stripModuleClassSuffix.toString == n1.stripModuleClassSuffix.toString
628+
}
629+
630+
/**
631+
* Is this tree immediately enclosing an import that renames a symbol to `toName`?
632+
*
633+
* @param toName The target name to check
634+
* @param tree The tree to check
635+
* @return True if this tree immediately encloses an import that renames a symbol to `toName`,
636+
* false otherwise.
637+
*/
638+
def immediatelyEnclosesRenaming(toName: Name, inTree: Tree)(implicit ctx: Context): Boolean = {
639+
def isImportRenaming(tree: Tree): Boolean = {
640+
tree match {
641+
case Import(_, selectors) =>
642+
selectors.exists {
643+
case Thicket(_ :: Ident(rename) :: Nil) =>
644+
rename.stripModuleClassSuffix.toString == toName.stripModuleClassSuffix.toString
645+
case _ =>
646+
false
647+
}
648+
case _ =>
649+
false
650+
}
651+
}
652+
653+
inTree match {
654+
case PackageDef(_, stats) =>
655+
stats.exists(isImportRenaming)
656+
case template: Template =>
657+
template.body.exists(isImportRenaming)
658+
case Block(stats, _) =>
659+
stats.exists(isImportRenaming)
660+
case _ =>
661+
false
662+
}
663+
}
664+
665+
/**
666+
* In `enclosing`, find all the references to any of `syms` that have been renamed to `toName`.
667+
*
668+
* If `enclosing` is empty, it means the renaming import was top-level and the whole source file
669+
* should be considered. Otherwise, we can restrict the search to this tree because renaming
670+
* imports are local.
671+
*
672+
* @param toName The name that is set by the renaming.
673+
* @param enclosing The tree that encloses the renaming import, if it exists.
674+
* @param syms The symbols to which we want to find renamed references.
675+
* @param allTrees All the trees in this source file, in case we can't find `enclosing`.
676+
* @param source The sourcefile that where to look for references.
677+
* @return All the references to the symbol under the cursor that are using `toName`.
678+
*/
679+
def findTreesMatchingRenaming(toName: Name,
680+
enclosing: Option[Tree],
681+
syms: List[Symbol],
682+
allTrees: List[Tree],
683+
source: SourceFile
684+
)(implicit ctx: Context): List[SourceNamedTree] = {
685+
686+
/**
687+
* Remove the blocks that immediately enclose a renaming to `toName` in `inTree`.
688+
*
689+
* @param toName The target name of renamings.
690+
* @param inTree The tree in which to remove the blocks that have such a renaming.
691+
* @return A tree that has no children containing a renaming to `toName`.
692+
*/
693+
def removeBlockWithRenaming(toName: Name, inTree: Tree): Tree = {
694+
new TreeMap {
695+
override def transform(tree: Tree)(implicit ctx: Context): Tree = tree match {
696+
case pkg: PackageDef if immediatelyEnclosesRenaming(toName, pkg) =>
697+
EmptyTree
698+
case template: Template if immediatelyEnclosesRenaming(toName, template) =>
699+
cpy.Template(template)(constr = DefDef(template.constr.symbol.asTerm), self = EmptyValDef, body = Nil)
700+
case block @ Block(stats, expr) if immediatelyEnclosesRenaming(toName, block) =>
701+
EmptyTree
702+
case other =>
703+
super.transform(other)
704+
}
705+
}.transform(inTree)
706+
}
707+
708+
val trees = {
709+
val trees = enclosing match {
710+
case Some(pkg: PackageDef) =>
711+
pkg.stats
712+
case Some(template: Template) =>
713+
template.body
714+
case Some(block: Block) =>
715+
block.expr :: block.stats
716+
case _ =>
717+
// No enclosing tree; we'll search in the whole file.
718+
allTrees
719+
}
720+
721+
// These trees may contain a new renaming of the same symbol to the same name, so we may
722+
// have to cut some branches
723+
val trimmedTrees = trees.map(removeBlockWithRenaming(toName, _))
724+
725+
// Some of these trees may not be `NameTrees`. Those that are not are wrapped in a
726+
// synthetic val def, so that everything can go inside `SourceNamedTree`s.
727+
trimmedTrees.map {
728+
case tree: NameTree =>
729+
SourceNamedTree(tree, source)
730+
case tree =>
731+
val valDef = tpd.SyntheticValDef(NameKinds.UniqueName.fresh(), tree)
732+
SourceNamedTree(valDef, source)
733+
}
734+
}
735+
736+
val includes =
737+
Include.references | Include.imports | Include.renamingImports
738+
739+
syms.flatMap { sym =>
740+
Interactive.namedTrees(trees,
741+
includes,
742+
tree =>
743+
Interactive.sameName(tree.name, toName) &&
744+
(Interactive.matchSymbol(tree, sym, includes) || Interactive.matchSymbol(tree, sym.linkedClass, includes)))
745+
}
624746
}
625747

626748
}

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

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -353,13 +353,39 @@ class DottyLanguageServer extends LanguageServer
353353
val path = Interactive.pathTo(uriTrees, pos)
354354
val syms = Interactive.enclosingSourceSymbols(path, pos)
355355
val newName = params.getNewName
356-
val includes =
357-
Include.references | Include.definitions | Include.linkedClass | Include.overriding | Include.imports
358356

359-
val refs = syms.flatMap { sym =>
360-
val trees = driver.allTreesContaining(sym.name.sourceModuleName.toString)
361-
Interactive.findTreesMatching(trees, includes, sym)
362-
}
357+
val refs =
358+
path match {
359+
// Selected a renaming in an import node
360+
case Thicket(_ :: (rename: Ident) :: Nil) :: (_: Import) :: rest if rename.pos.contains(pos.pos) =>
361+
val allTrees = uriTrees.map(_.tree)
362+
val source = uriTrees.head.source
363+
Interactive.findTreesMatchingRenaming(rename.name, rest.headOption, syms, allTrees, source)
364+
365+
// Selected a reference that has been renamed
366+
case (nameTree: NameTree) :: rest if Interactive.isRenamed(nameTree) =>
367+
val enclosing = rest.find {
368+
// If we selected one of the parents of this Template for doing the renaming, then this
369+
// Template cannot immediately enclose the rename we're interesting in (the renaming
370+
// happening inside its body cannot be used on the parents).
371+
case template: Template if template.parents.exists(_.pos.contains(pos.pos)) =>
372+
false
373+
case tree =>
374+
Interactive.immediatelyEnclosesRenaming(nameTree.name, tree)
375+
}
376+
val allTrees = uriTrees.map(_.tree)
377+
val source = uriTrees.head.source
378+
Interactive.findTreesMatchingRenaming(nameTree.name, enclosing, syms, allTrees, source)
379+
380+
case _ =>
381+
val includes =
382+
Include.references | Include.definitions | Include.linkedClass | Include.overriding | Include.imports
383+
384+
syms.flatMap { sym =>
385+
val trees = driver.allTreesContaining(sym.name.sourceModuleName.toString)
386+
Interactive.findTreesMatching(trees, includes, sym)
387+
}
388+
}
363389

364390
val changes =
365391
refs.groupBy(ref => toUriOption(ref.source))

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

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dotty.tools.languageserver
33
import org.junit.Test
44

55
import dotty.tools.languageserver.util.Code._
6+
import dotty.tools.languageserver.util.CodeRange
67
import dotty.tools.languageserver.util.embedded.CodeMarker
78

89
class RenameTest {
@@ -89,19 +90,104 @@ class RenameTest {
8990
}
9091

9192
@Test def renameRenamedImport: Unit = {
92-
def testRenameFrom(m: CodeMarker) =
93+
def sources =
9394
withSources(
9495
code"""object A { class ${m1}C${m2} }""",
95-
code"""import A.{${m3}C${m4} => D}
96-
object B { new ${m5}D${m6} }"""
97-
).rename(m, "NewName", Set(m1 to m2, m3 to m4))
96+
code"""import A.{${m3}C${m4} => ${m5}D${m6}}
97+
object B { new ${m7}D${m8} }"""
98+
)
99+
def testRename(m: CodeMarker, expectations: Set[CodeRange]) =
100+
sources.rename(m, "NewName", expectations)
101+
102+
testRename(m1, Set(m1 to m2, m3 to m4))
103+
testRename(m2, Set(m1 to m2, m3 to m4))
104+
testRename(m3, Set(m1 to m2, m3 to m4))
105+
testRename(m4, Set(m1 to m2, m3 to m4))
106+
testRename(m5, Set(m5 to m6, m7 to m8))
107+
testRename(m6, Set(m5 to m6, m7 to m8))
108+
testRename(m7, Set(m5 to m6, m7 to m8))
109+
testRename(m8, Set(m5 to m6, m7 to m8))
110+
}
98111

99-
testRenameFrom(m1)
100-
testRenameFrom(m2)
101-
testRenameFrom(m3)
102-
testRenameFrom(m4)
103-
testRenameFrom(m5)
104-
testRenameFrom(m6)
112+
@Test def renameRenamingImport: Unit = {
113+
def sources =
114+
withSources(
115+
code"""object A { class ${m1}C${m2}; object ${m3}C${m4} }""",
116+
code"""object O1 {
117+
import A.{${m5}C${m6} => ${m7}Renamed${m8}}
118+
class C2 extends ${m9}Renamed${m10} { val x = ${m11}Renamed${m12} }
119+
}
120+
object O2 {
121+
import A.{${m13}C${m14} => ${m15}Renamed${m16}}
122+
class C3 extends ${m17}Renamed${m18} { val x = ${m19}Renamed${m20} }
123+
}"""
124+
)
125+
def testRename(m: CodeMarker, expectations: Set[CodeRange]) =
126+
sources.rename(m, "NewName", expectations)
127+
128+
testRename(m1, Set(m1 to m2, m3 to m4, m5 to m6, m13 to m14))
129+
testRename(m2, Set(m1 to m2, m3 to m4, m5 to m6, m13 to m14))
130+
testRename(m3, Set(m1 to m2, m3 to m4, m5 to m6, m13 to m14))
131+
testRename(m4, Set(m1 to m2, m3 to m4, m5 to m6, m13 to m14))
132+
testRename(m5, Set(m1 to m2, m3 to m4, m5 to m6, m13 to m14))
133+
testRename(m6, Set(m1 to m2, m3 to m4, m5 to m6, m13 to m14))
134+
135+
testRename(m7, Set(m7 to m8, m9 to m10, m11 to m12))
136+
testRename(m8, Set(m7 to m8, m9 to m10, m11 to m12))
137+
testRename(m9, Set(m7 to m8, m9 to m10, m11 to m12))
138+
testRename(m10, Set(m7 to m8, m9 to m10, m11 to m12))
139+
testRename(m11, Set(m7 to m8, m9 to m10, m11 to m12))
140+
testRename(m12, Set(m7 to m8, m9 to m10, m11 to m12))
141+
142+
testRename(m13, Set(m1 to m2, m3 to m4, m5 to m6, m13 to m14))
143+
testRename(m14, Set(m1 to m2, m3 to m4, m5 to m6, m13 to m14))
144+
145+
testRename(m15, Set(m15 to m16, m17 to m18, m19 to m20))
146+
testRename(m16, Set(m15 to m16, m17 to m18, m19 to m20))
147+
testRename(m17, Set(m15 to m16, m17 to m18, m19 to m20))
148+
testRename(m18, Set(m15 to m16, m17 to m18, m19 to m20))
149+
testRename(m19, Set(m15 to m16, m17 to m18, m19 to m20))
150+
testRename(m20, Set(m15 to m16, m17 to m18, m19 to m20))
151+
152+
}
153+
154+
@Test def renameRenamingImportNested: Unit = {
155+
def sources =
156+
withSources(
157+
code"""object A { class C }""",
158+
code"""import A.{C => ${m1}Renamed${m2}}
159+
object O {
160+
import A.{C => ${m3}Renamed${m4}}
161+
class C2 extends ${m5}Renamed${m6} {
162+
import A.{C => ${m7}Renamed${m8}}
163+
}
164+
123 match {
165+
case x if new ${m9}Renamed${m10} == null => ???
166+
case foo if {
167+
import A.{C => ${m11}Renamed${m12}}
168+
new ${m13}Renamed${m14} != null
169+
} => ???
170+
}
171+
new A.C
172+
}"""
173+
)
174+
def testRename(m: CodeMarker, expectations: Set[CodeRange]) =
175+
sources.rename(m, "NewName", expectations)
176+
177+
testRename(m1, Set(m1 to m2))
178+
testRename(m2, Set(m1 to m2))
179+
testRename(m3, Set(m3 to m4, m5 to m6, m9 to m10))
180+
testRename(m4, Set(m3 to m4, m5 to m6, m9 to m10))
181+
testRename(m5, Set(m3 to m4, m5 to m6, m9 to m10))
182+
testRename(m6, Set(m3 to m4, m5 to m6, m9 to m10))
183+
testRename(m7, Set(m7 to m8))
184+
testRename(m8, Set(m7 to m8))
185+
testRename(m9, Set(m3 to m4, m5 to m6, m9 to m10))
186+
testRename(m10, Set(m3 to m4, m5 to m6, m9 to m10))
187+
testRename(m11, Set(m11 to m12, m13 to m14))
188+
testRename(m12, Set(m11 to m12, m13 to m14))
189+
testRename(m13, Set(m11 to m12, m13 to m14))
190+
testRename(m14, Set(m11 to m12, m13 to m14))
105191
}
106192

107193
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ object Code {
3030
val m16 = new CodeMarker("m16")
3131
val m17 = new CodeMarker("m17")
3232
val m18 = new CodeMarker("m18")
33+
val m19 = new CodeMarker("m19")
34+
val m20 = new CodeMarker("m20")
3335

3436
implicit class CodeHelper(val sc: StringContext) extends AnyVal {
3537

0 commit comments

Comments
 (0)