Skip to content

Commit 94a3cb5

Browse files
committed
refactor Completions, remove unsafeNulls from Completion.scala
1 parent 4085574 commit 94a3cb5

File tree

4 files changed

+84
-73
lines changed

4 files changed

+84
-73
lines changed

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

Lines changed: 61 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package dotty.tools.dotc.interactive
22

3-
import scala.language.unsafeNulls
4-
53
import dotty.tools.dotc.ast.untpd
64
import dotty.tools.dotc.ast.NavigateAST
75
import dotty.tools.dotc.config.Printers.interactiv
@@ -38,18 +36,17 @@ import scala.util.control.NonFatal
3836
*/
3937
case class Completion(label: String, description: String, symbols: List[Symbol])
4038

41-
object Completion {
39+
object Completion:
4240

4341
import dotty.tools.dotc.ast.tpd._
4442

4543
/** Get possible completions from tree at `pos`
4644
*
4745
* @return offset and list of symbols for possible completions
4846
*/
49-
def completions(pos: SourcePosition)(using Context): (Int, List[Completion]) = {
50-
val path = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span)
47+
def completions(pos: SourcePosition)(using Context): (Int, List[Completion]) =
48+
val path: List[Tree] = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span)
5149
computeCompletions(pos, path)(using Interactive.contextOfPath(path).withPhase(Phases.typerPhase))
52-
}
5350

5451
/**
5552
* Inspect `path` to determine what kinds of symbols should be considered.
@@ -61,10 +58,11 @@ object Completion {
6158
*
6259
* Otherwise, provide no completion suggestion.
6360
*/
64-
def completionMode(path: List[Tree], pos: SourcePosition): Mode =
65-
path match {
66-
case Ident(_) :: Import(_, _) :: _ => Mode.ImportOrExport
67-
case (ref: RefTree) :: _ =>
61+
def completionMode(path: List[untpd.Tree], pos: SourcePosition): Mode =
62+
path match
63+
case untpd.Ident(_) :: untpd.Import(_, _) :: _ => Mode.ImportOrExport
64+
case untpd.Ident(_) :: (_: untpd.ImportSelector) :: _ => Mode.ImportOrExport
65+
case (ref: untpd.RefTree) :: _ =>
6866
if (ref.name.isTermName) Mode.Term
6967
else if (ref.name.isTypeName) Mode.Type
7068
else Mode.None
@@ -73,9 +71,8 @@ object Completion {
7371
if sel.imported.span.contains(pos.span) then Mode.ImportOrExport
7472
else Mode.None // Can't help completing the renaming
7573

76-
case (_: ImportOrExport) :: _ => Mode.ImportOrExport
74+
case (_: untpd.ImportOrExport) :: _ => Mode.ImportOrExport
7775
case _ => Mode.None
78-
}
7976

8077
/** When dealing with <errors> in varios palces we check to see if they are
8178
* due to incomplete backticks. If so, we ensure we get the full prefix
@@ -97,15 +94,18 @@ object Completion {
9794
* Inspect `path` to determine the completion prefix. Only symbols whose name start with the
9895
* returned prefix should be considered.
9996
*/
100-
def completionPrefix(path: List[untpd.Tree], pos: SourcePosition)(using Context): String =
97+
def completionPrefix(path: List[untpd.Tree], pos: SourcePosition)(using Context): String =
10198
path match
10299
case (sel: untpd.ImportSelector) :: _ =>
103100
completionPrefix(sel.imported :: Nil, pos)
104101

102+
case untpd.Ident(_) :: (sel: untpd.ImportSelector) :: _ if !sel.isGiven =>
103+
completionPrefix(sel.imported :: Nil, pos)
104+
105105
case (tree: untpd.ImportOrExport) :: _ =>
106-
tree.selectors.find(_.span.contains(pos.span)).map { selector =>
106+
tree.selectors.find(_.span.contains(pos.span)).map: selector =>
107107
completionPrefix(selector :: Nil, pos)
108-
}.getOrElse("")
108+
.getOrElse("")
109109

110110
// Foo.`se<TAB> will result in Select(Ident(Foo), <error>)
111111
case (select: untpd.Select) :: _ if select.name == nme.ERROR =>
@@ -119,29 +119,34 @@ object Completion {
119119
if (ref.name == nme.ERROR) ""
120120
else ref.name.toString.take(pos.span.point - ref.span.point)
121121

122-
case _ =>
123-
""
122+
case _ => ""
123+
124124
end completionPrefix
125125

126126
/** Inspect `path` to determine the offset where the completion result should be inserted. */
127-
def completionOffset(path: List[Tree]): Int =
128-
path match {
129-
case (ref: RefTree) :: _ => ref.span.point
127+
def completionOffset(untpdPath: List[untpd.Tree]): Int =
128+
untpdPath match {
129+
case (ref: untpd.RefTree) :: _ => ref.span.point
130130
case _ => 0
131131
}
132132

133-
/**
134-
* Inspect `path` to deterimine whether enclosing tree is a result of tree extension.
135-
* If so, completion should use untyped path containing tree before extension to get proper results.
133+
/** Some information about the trees is lost after Typer such as Extension method definitions
134+
* are expanded into methods. In order to support completions in those cases
135+
* we have to rely on untyped trees and only when types are necessary use typed trees.
136136
*/
137-
def pathBeforeDesugaring(path: List[Tree], pos: SourcePosition)(using Context): List[Tree] =
138-
val hasUntypedTree = path.headOption.forall(NavigateAST.untypedPath(_, exactMatch = true).nonEmpty)
139-
if hasUntypedTree then path
140-
else NavigateAST.untypedPath(pos.span).collect:
141-
case tree: untpd.Tree => tree
142-
143-
private def computeCompletions(pos: SourcePosition, path: List[Tree])(using Context): (Int, List[Completion]) = {
144-
val path0 = pathBeforeDesugaring(path, pos)
137+
def resolveTypedOrUntypedPath(tpdPath: List[Tree], pos: SourcePosition)(using Context): List[untpd.Tree] =
138+
lazy val untpdPath: List[untpd.Tree] = NavigateAST
139+
.pathTo(pos.span, List(ctx.compilationUnit.untpdTree), true).collect:
140+
case untpdTree: untpd.Tree => untpdTree
141+
142+
143+
tpdPath match
144+
case (_: Bind) :: _ => tpdPath
145+
case (_: untpd.TypTree) :: _ => tpdPath
146+
case _ => untpdPath
147+
148+
private def computeCompletions(pos: SourcePosition, tpdPath: List[Tree])(using Context): (Int, List[Completion]) =
149+
val path0 = resolveTypedOrUntypedPath(tpdPath, pos)
145150
val mode = completionMode(path0, pos)
146151
val rawPrefix = completionPrefix(path0, pos)
147152

@@ -150,16 +155,15 @@ object Completion {
150155

151156
val completer = new Completer(mode, prefix, pos)
152157

153-
val completions = path0 match {
154-
// Ignore synthetic select from `This` because in code it was `Ident`
155-
// See example in dotty.tools.languageserver.CompletionTest.syntheticThis
156-
case Select(qual @ This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions
157-
case Select(qual, _) :: _ if qual.tpe.hasSimpleKind => completer.selectionCompletions(qual)
158-
case Select(qual, _) :: _ => Map.empty
159-
case (tree: ImportOrExport) :: _ => completer.directMemberCompletions(tree.expr)
160-
case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => completer.directMemberCompletions(expr)
161-
case _ => completer.scopeCompletions
162-
}
158+
val completions = tpdPath match
159+
// Ignore synthetic select from `This` because in code it was `Ident`
160+
// See example in dotty.tools.languageserver.CompletionTest.syntheticThis
161+
case Select(qual @ This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions
162+
case Select(qual, _) :: _ if qual.tpe.hasSimpleKind => completer.selectionCompletions(qual)
163+
case Select(qual, _) :: _ => Map.empty
164+
case (tree: ImportOrExport) :: _ => completer.directMemberCompletions(tree.expr)
165+
case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => completer.directMemberCompletions(expr)
166+
case _ => completer.scopeCompletions
163167

164168
val describedCompletions = describeCompletions(completions)
165169
val backtickedCompletions =
@@ -173,7 +177,6 @@ object Completion {
173177
| type = ${completer.mode.is(Mode.Type)}
174178
| results = $backtickedCompletions%, %""")
175179
(offset, backtickedCompletions)
176-
}
177180

178181
def backtickCompletions(completion: Completion, hasBackTick: Boolean) =
179182
if hasBackTick || needsBacktick(completion.label) then
@@ -186,17 +189,17 @@ object Completion {
186189
// https://github.com/scalameta/metals/blob/main/mtags/src/main/scala/scala/meta/internal/mtags/KeywordWrapper.scala
187190
// https://github.com/com-lihaoyi/Ammonite/blob/73a874173cd337f953a3edc9fb8cb96556638fdd/amm/util/src/main/scala/ammonite/util/Model.scala
188191
private def needsBacktick(s: String) =
189-
val chunks = s.split("_", -1)
192+
val chunks = s.split("_", -1).nn
190193

191194
val validChunks = chunks.zipWithIndex.forall { case (chunk, index) =>
192-
chunk.forall(Chars.isIdentifierPart) ||
193-
(chunk.forall(Chars.isOperatorPart) &&
195+
chunk.nn.forall(Chars.isIdentifierPart) ||
196+
(chunk.nn.forall(Chars.isOperatorPart) &&
194197
index == chunks.length - 1 &&
195198
!(chunks.lift(index - 1).contains("") && index - 1 == 0))
196199
}
197200

198201
val validStart =
199-
Chars.isIdentifierStart(s(0)) || chunks(0).forall(Chars.isOperatorPart)
202+
Chars.isIdentifierStart(s(0)) || chunks(0).nn.forall(Chars.isOperatorPart)
200203

201204
val valid = validChunks && validStart && !keywords.contains(s)
202205

@@ -228,7 +231,7 @@ object Completion {
228231
* For the results of all `xyzCompletions` methods term names and type names are always treated as different keys in the same map
229232
* and they never conflict with each other.
230233
*/
231-
class Completer(val mode: Mode, val prefix: String, pos: SourcePosition) {
234+
class Completer(val mode: Mode, val prefix: String, pos: SourcePosition):
232235
/** Completions for terms and types that are currently in scope:
233236
* the members of the current class, local definitions and the symbols that have been imported,
234237
* recursively adding completions from outer scopes.
@@ -242,7 +245,7 @@ object Completion {
242245
* (even if the import follows it syntactically)
243246
* - a more deeply nested import shadowing a member or a local definition causes an ambiguity
244247
*/
245-
def scopeCompletions(using context: Context): CompletionMap = {
248+
def scopeCompletions(using context: Context): CompletionMap =
246249
val mappings = collection.mutable.Map.empty[Name, List[ScopedDenotations]].withDefaultValue(List.empty)
247250
def addMapping(name: Name, denots: ScopedDenotations) =
248251
mappings(name) = mappings(name) :+ denots
@@ -314,7 +317,7 @@ object Completion {
314317
}
315318

316319
resultMappings
317-
}
320+
end scopeCompletions
318321

319322
/** Widen only those types which are applied or are exactly nothing
320323
*/
@@ -347,16 +350,16 @@ object Completion {
347350
/** Completions introduced by imports directly in this context.
348351
* Completions from outer contexts are not included.
349352
*/
350-
private def importedCompletions(using Context): CompletionMap = {
353+
private def importedCompletions(using Context): CompletionMap =
351354
val imp = ctx.importInfo
352355

353-
def fromImport(name: Name, nameInScope: Name): Seq[(Name, SingleDenotation)] =
354-
imp.site.member(name).alternatives
355-
.collect { case denot if include(denot, nameInScope) => nameInScope -> denot }
356-
357356
if imp == null then
358357
Map.empty
359358
else
359+
def fromImport(name: Name, nameInScope: Name): Seq[(Name, SingleDenotation)] =
360+
imp.site.member(name).alternatives
361+
.collect { case denot if include(denot, nameInScope) => nameInScope -> denot }
362+
360363
val givenImports = imp.importedImplicits
361364
.map { ref => (ref.implicitName: Name, ref.underlyingRef.denot.asSingleDenotation) }
362365
.filter((name, denot) => include(denot, name))
@@ -382,7 +385,7 @@ object Completion {
382385
}.toSeq.groupByName
383386

384387
givenImports ++ wildcardMembers ++ explicitMembers
385-
}
388+
end importedCompletions
386389

387390
/** Completions from implicit conversions including old style extensions using implicit classes */
388391
private def implicitConversionMemberCompletions(qual: Tree)(using Context): CompletionMap =
@@ -544,7 +547,6 @@ object Completion {
544547
extension [N <: Name](namedDenotations: Seq[(N, SingleDenotation)])
545548
@annotation.targetName("groupByNameTupled")
546549
def groupByName: CompletionMap = namedDenotations.groupMap((name, denot) => name)((name, denot) => denot)
547-
}
548550

549551
private type CompletionMap = Map[Name, Seq[SingleDenotation]]
550552

@@ -557,11 +559,11 @@ object Completion {
557559
* The completion mode: defines what kinds of symbols should be included in the completion
558560
* results.
559561
*/
560-
class Mode(val bits: Int) extends AnyVal {
562+
class Mode(val bits: Int) extends AnyVal:
561563
def is(other: Mode): Boolean = (bits & other.bits) == other.bits
562564
def |(other: Mode): Mode = new Mode(bits | other.bits)
563-
}
564-
object Mode {
565+
566+
object Mode:
565567
/** No symbol should be included */
566568
val None: Mode = new Mode(0)
567569

@@ -573,6 +575,4 @@ object Completion {
573575

574576
/** Both term and type symbols are allowed */
575577
val ImportOrExport: Mode = new Mode(4) | Term | Type
576-
}
577-
}
578578

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,18 +1523,29 @@ class CompletionTest {
15231523
|object Test:
15241524
| def foo: ArrayBuffer[Fo${m1}] = ???
15251525
"""
1526-
.completion(m1, Set(
1527-
("Foo",Class,"Foo")
1528-
)
1529-
)
1526+
.completion(m1, Set(("Foo",Class,"Foo")))
15301527
}
15311528

15321529
@Test def extensionDefinitionCompletions: Unit =
15331530
code"""|trait Foo
15341531
|object T:
15351532
| extension (x: Fo$m1)
15361533
|"""
1537-
.completion(m1, Set(
1538-
("Foo",Class,"Foo")
1539-
))
1534+
.completion(m1, Set(("Foo",Class,"Foo")))
1535+
1536+
@Test def selectDynamic: Unit =
1537+
code"""|import scala.language.dynamics
1538+
|class Foo extends Dynamic {
1539+
| def banana: Int = 42
1540+
| def selectDynamic(field: String): Foo = this
1541+
| def applyDynamicNamed(name: String)(arg: (String, Int)): Foo = this
1542+
| def updateDynamic(name: String)(value: Int): Foo = this
1543+
|}
1544+
|object Test:
1545+
| val x = new Foo()
1546+
| x.sele$m1
1547+
| x.bana$m2
1548+
|"""
1549+
.completion(m1, Set(("selectDynamic", Method, "(field: String): Foo")))
1550+
.completion(m2, Set(("banana", Method, "=> Int")))
15401551
}

presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class Completions(
5757
val coursierComplete = new CoursierComplete(BuildInfo.scalaVersion)
5858

5959
private lazy val completionMode =
60-
val adjustedPath = Completion.pathBeforeDesugaring(path, pos)
60+
val adjustedPath = Completion.resolveTypedOrUntypedPath(path, pos)
6161
val mode = Completion.completionMode(adjustedPath, pos)
6262
path match
6363
case Literal(Constant(_: String)) :: _ => Mode.Term // literal completions

presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ class CompletionSuite extends BaseCompletionSuite:
539539
| new Foo().bana@@
540540
|}
541541
|""".stripMargin,
542-
"selectDynamic(field: String): Foo"
542+
"banana: Int"
543543
)
544544

545545
@Test def dynamic2 =
@@ -549,7 +549,7 @@ class CompletionSuite extends BaseCompletionSuite:
549549
| val x = new Foo().foo.bana@@
550550
|}
551551
|""".stripMargin,
552-
"selectDynamic(field: String): Foo"
552+
"banana: Int"
553553
)
554554

555555
@Test def dynamic3 =
@@ -560,7 +560,7 @@ class CompletionSuite extends BaseCompletionSuite:
560560
| (foo.bar = 42).bana@@
561561
|}
562562
|""".stripMargin,
563-
"selectDynamic(field: String): Foo"
563+
"banana: Int"
564564
)
565565

566566
@Test def dynamic4 =
@@ -570,7 +570,7 @@ class CompletionSuite extends BaseCompletionSuite:
570570
| val foo = new Foo().foo(x = 42).bana@@
571571
|}
572572
|""".stripMargin,
573-
"selectDynamic(field: String): Foo"
573+
"banana: Int"
574574
)
575575

576576
@Test def dynamic5 =

0 commit comments

Comments
 (0)