Skip to content

Commit 4be7db7

Browse files
committed
properly instantiate type vars for completion labels
1 parent c81673c commit 4be7db7

File tree

10 files changed

+325
-200
lines changed

10 files changed

+325
-200
lines changed

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

Lines changed: 95 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import dotty.tools.dotc.core.TypeError
1919
import dotty.tools.dotc.core.Phases
2020
import dotty.tools.dotc.core.Types.{AppliedType, ExprType, MethodOrPoly, NameFilter, NoType, RefinedType, TermRef, Type, TypeProxy}
2121
import dotty.tools.dotc.parsing.Tokens
22+
import dotty.tools.dotc.typer.Implicits.SearchSuccess
23+
import dotty.tools.dotc.typer.Inferencing
2224
import dotty.tools.dotc.util.Chars
2325
import dotty.tools.dotc.util.SourcePosition
2426

@@ -28,6 +30,7 @@ import dotty.tools.dotc.core.ContextOps.localContext
2830
import dotty.tools.dotc.core.Names
2931
import dotty.tools.dotc.core.Types
3032
import dotty.tools.dotc.core.Symbols
33+
import dotty.tools.dotc.core.Constants
3134

3235
/**
3336
* One of the results of a completion query.
@@ -49,8 +52,31 @@ object Completion:
4952
* @return offset and list of symbols for possible completions
5053
*/
5154
def completions(pos: SourcePosition)(using Context): (Int, List[Completion]) =
52-
val path: List[Tree] = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span)
53-
computeCompletions(pos, path)(using Interactive.contextOfPath(path).withPhase(Phases.typerPhase))
55+
val tpdPath = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span)
56+
val completionContext = Interactive.contextOfPath(tpdPath).withPhase(Phases.typerPhase)
57+
inContext(completionContext):
58+
val untpdPath = Interactive.resolveTypedOrUntypedPath(tpdPath, pos)
59+
val mode = completionMode(untpdPath, pos)
60+
val rawPrefix = completionPrefix(untpdPath, pos)
61+
val completions = rawCompletions(pos, mode, rawPrefix, tpdPath, untpdPath)
62+
63+
postProcessCompletions(untpdPath, completions, rawPrefix)
64+
65+
66+
/** Get possible completions from tree at `pos`
67+
* This method requires manually computing the mode, prefix and paths.
68+
*
69+
* @return completion map of name to list of denotations
70+
*/
71+
def rawCompletions(
72+
pos: SourcePosition,
73+
mode: Mode,
74+
rawPrefix: String,
75+
tpdPath: List[Tree],
76+
untpdPath: List[untpd.Tree]
77+
)(using Context): CompletionMap =
78+
val adjustedPath = typeCheckExtensionConstructPath(untpdPath, tpdPath, pos)
79+
computeCompletions(pos, mode, rawPrefix, adjustedPath)
5480

5581
/**
5682
* Inspect `path` to determine what kinds of symbols should be considered.
@@ -63,90 +89,69 @@ object Completion:
6389
* Otherwise, provide no completion suggestion.
6490
*/
6591
def completionMode(path: List[untpd.Tree], pos: SourcePosition): Mode =
66-
path match
67-
case untpd.Ident(_) :: untpd.Import(_, _) :: _ => Mode.ImportOrExport
68-
case untpd.Ident(_) :: (_: untpd.ImportSelector) :: _ => Mode.ImportOrExport
69-
case (ref: untpd.RefTree) :: _ =>
70-
if (ref.name.isTermName) Mode.Term
71-
else if (ref.name.isTypeName) Mode.Type
72-
else Mode.None
7392

74-
case (sel: untpd.ImportSelector) :: _ =>
75-
if sel.imported.span.contains(pos.span) then Mode.ImportOrExport
76-
else Mode.None // Can't help completing the renaming
93+
val completionSymbolKind: Mode =
94+
path match
95+
case untpd.Ident(_) :: untpd.Import(_, _) :: _ => Mode.ImportOrExport
96+
case untpd.Ident(_) :: (_: untpd.ImportSelector) :: _ => Mode.ImportOrExport
97+
case Literal(Constants.Constant(_: String)) :: _ => Mode.Term // literal completions
98+
case (ref: untpd.RefTree) :: _ =>
99+
if (ref.name.isTermName) Mode.Term
100+
else if (ref.name.isTypeName) Mode.Type
101+
else Mode.None
77102

78-
case (_: untpd.ImportOrExport) :: _ => Mode.ImportOrExport
79-
case _ => Mode.None
103+
case (sel: untpd.ImportSelector) :: _ =>
104+
if sel.imported.span.contains(pos.span) then Mode.ImportOrExport
105+
else Mode.None // Can't help completing the renaming
80106

81-
/** When dealing with <errors> in varios palces we check to see if they are
82-
* due to incomplete backticks. If so, we ensure we get the full prefix
83-
* including the backtick.
84-
*
85-
* @param content The source content that we'll check the positions for the prefix
86-
* @param start The start position we'll start to look for the prefix at
87-
* @param end The end position we'll look for the prefix at
88-
* @return Either the full prefix including the ` or an empty string
89-
*/
90-
private def checkBacktickPrefix(content: Array[Char], start: Int, end: Int): String =
91-
content.lift(start) match
92-
case Some(char) if char == '`' =>
93-
content.slice(start, end).mkString
94-
case _ =>
95-
""
107+
case (_: untpd.ImportOrExport) :: _ => Mode.ImportOrExport
108+
case _ => Mode.None
109+
110+
val completionKind: Mode =
111+
path match
112+
case Nil | (_: PackageDef) :: _ => Mode.None
113+
case untpd.Ident(_) :: (_: untpd.ImportSelector) :: _ => Mode.Member
114+
case (_: Select) :: _ => Mode.Member
115+
case _ => Mode.Scope
116+
117+
completionSymbolKind | completionKind
96118

97119
/**
98120
* Inspect `path` to determine the completion prefix. Only symbols whose name start with the
99121
* returned prefix should be considered.
100122
*/
101123
def completionPrefix(path: List[untpd.Tree], pos: SourcePosition)(using Context): String =
124+
def fallback: Int =
125+
var i = pos.point - 1
126+
while i >= 0 && Chars.isIdentifierPart(pos.source.content()(i)) do i -= 1
127+
i + 1
128+
102129
path match
103130
case (sel: untpd.ImportSelector) :: _ =>
104131
completionPrefix(sel.imported :: Nil, pos)
105132

106133
case untpd.Ident(_) :: (sel: untpd.ImportSelector) :: _ if !sel.isGiven =>
107-
completionPrefix(sel.imported :: Nil, pos)
134+
if sel.isWildcard then pos.source.content()(pos.point - 1).toString
135+
else completionPrefix(sel.imported :: Nil, pos)
108136

109137
case (tree: untpd.ImportOrExport) :: _ =>
110138
tree.selectors.find(_.span.contains(pos.span)).map: selector =>
111139
completionPrefix(selector :: Nil, pos)
112140
.getOrElse("")
113141

114-
// Foo.`se<TAB> will result in Select(Ident(Foo), <error>)
115-
case (select: untpd.Select) :: _ if select.name == nme.ERROR =>
116-
checkBacktickPrefix(select.source.content(), select.nameSpan.start, select.span.end)
117-
118-
// import scala.util.chaining.`s<TAB> will result in a Ident(<error>)
119-
case (ident: untpd.Ident) :: _ if ident.name == nme.ERROR =>
120-
checkBacktickPrefix(ident.source.content(), ident.span.start, ident.span.end)
142+
case (tree: untpd.RefTree) :: _ if tree.name != nme.ERROR =>
143+
tree.name.toString.take(pos.span.point - tree.span.point)
121144

122-
case (ref: untpd.RefTree) :: _ =>
123-
if (ref.name == nme.ERROR) ""
124-
else ref.name.toString.take(pos.span.point - ref.span.point)
145+
case _ => pos.source.content.slice(fallback, pos.point).mkString
125146

126-
case _ => ""
127147

128148
end completionPrefix
129149

130150
/** Inspect `path` to determine the offset where the completion result should be inserted. */
131151
def completionOffset(untpdPath: List[untpd.Tree]): Int =
132-
untpdPath match {
152+
untpdPath match
133153
case (ref: untpd.RefTree) :: _ => ref.span.point
134154
case _ => 0
135-
}
136-
137-
/** Some information about the trees is lost after Typer such as Extension method construct
138-
* is expanded into methods. In order to support completions in those cases
139-
* we have to rely on untyped trees and only when types are necessary use typed trees.
140-
*/
141-
def resolveTypedOrUntypedPath(tpdPath: List[Tree], pos: SourcePosition)(using Context): List[untpd.Tree] =
142-
lazy val untpdPath: List[untpd.Tree] = NavigateAST
143-
.pathTo(pos.span, List(ctx.compilationUnit.untpdTree), true).collect:
144-
case untpdTree: untpd.Tree => untpdTree
145-
146-
tpdPath match
147-
case (_: Bind) :: _ => tpdPath
148-
case (_: untpd.TypTree) :: _ => tpdPath
149-
case _ => untpdPath
150155

151156
/** Handle case when cursor position is inside extension method construct.
152157
* The extension method construct is then desugared into methods, and consturct parameters
@@ -170,18 +175,12 @@ object Completion:
170175
Interactive.pathTo(typedEnclosingParam, pos.span)
171176
.flatten.getOrElse(tpdPath)
172177

173-
private def computeCompletions(pos: SourcePosition, tpdPath: List[Tree])(using Context): (Int, List[Completion]) =
174-
val path0 = resolveTypedOrUntypedPath(tpdPath, pos)
175-
val mode = completionMode(path0, pos)
176-
val rawPrefix = completionPrefix(path0, pos)
177-
178+
private def computeCompletions(pos: SourcePosition, mode: Mode, rawPrefix: String, adjustedPath: List[Tree])(using Context): CompletionMap =
178179
val hasBackTick = rawPrefix.headOption.contains('`')
179180
val prefix = if hasBackTick then rawPrefix.drop(1) else rawPrefix
180-
181181
val completer = new Completer(mode, prefix, pos)
182182

183-
val adjustedPath = typeCheckExtensionConstructPath(path0, tpdPath, pos)
184-
val completions = adjustedPath match
183+
val result = adjustedPath match
185184
// Ignore synthetic select from `This` because in code it was `Ident`
186185
// See example in dotty.tools.languageserver.CompletionTest.syntheticThis
187186
case Select(qual @ This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions
@@ -191,17 +190,24 @@ object Completion:
191190
case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => completer.directMemberCompletions(expr)
192191
case _ => completer.scopeCompletions
193192

193+
interactiv.println(i"""completion info with pos = $pos,
194+
| prefix = ${completer.prefix},
195+
| term = ${completer.mode.is(Mode.Term)},
196+
| type = ${completer.mode.is(Mode.Type)},
197+
| scope = ${completer.mode.is(Mode.Scope)},
198+
| member = ${completer.mode.is(Mode.Member)}""")
199+
200+
result
201+
202+
def postProcessCompletions(path: List[untpd.Tree], completions: CompletionMap, rawPrefix: String)(using Context): (Int, List[Completion]) =
194203
val describedCompletions = describeCompletions(completions)
204+
val hasBackTick = rawPrefix.headOption.contains('`')
195205
val backtickedCompletions =
196206
describedCompletions.map(completion => backtickCompletions(completion, hasBackTick))
197207

198-
val offset = completionOffset(path0)
208+
interactiv.println(i"""completion resutls = $backtickedCompletions%, %""")
199209

200-
interactiv.println(i"""completion with pos = $pos,
201-
| prefix = ${completer.prefix},
202-
| term = ${completer.mode.is(Mode.Term)},
203-
| type = ${completer.mode.is(Mode.Type)}
204-
| results = $backtickedCompletions%, %""")
210+
val offset = completionOffset(path)
205211
(offset, backtickedCompletions)
206212

207213
def backtickCompletions(completion: Completion, hasBackTick: Boolean) =
@@ -415,11 +421,22 @@ object Completion:
415421

416422
/** Completions from implicit conversions including old style extensions using implicit classes */
417423
private def implicitConversionMemberCompletions(qual: Tree)(using Context): CompletionMap =
424+
425+
def tryToInstantiateTypeVars(conversionTarget: SearchSuccess): Type =
426+
try
427+
val typingCtx = ctx.fresh
428+
inContext(typingCtx):
429+
val methodRefTree = ref(conversionTarget.ref, needLoad = false)
430+
val convertedTree = ctx.typer.typedAheadExpr(untpd.Apply(untpd.TypedSplice(methodRefTree), untpd.TypedSplice(qual) :: Nil))
431+
Inferencing.fullyDefinedType(convertedTree.tpe, "", pos)
432+
catch
433+
case error => conversionTarget.tree.tpe // fallback to not fully defined type
434+
418435
if qual.tpe.isExactlyNothing || qual.tpe.isNullType then
419436
Map.empty
420437
else
421438
implicitConversionTargets(qual)(using ctx.fresh.setExploreTyperState())
422-
.flatMap(accessibleMembers)
439+
.flatMap { conversionTarget => accessibleMembers(tryToInstantiateTypeVars(conversionTarget)) }
423440
.toSeq
424441
.groupByName
425442

@@ -551,13 +568,13 @@ object Completion:
551568
* @param qual The argument to which the implicit conversion should be applied.
552569
* @return The set of types after `qual` implicit conversion.
553570
*/
554-
private def implicitConversionTargets(qual: Tree)(using Context): Set[Type] = {
571+
private def implicitConversionTargets(qual: Tree)(using Context): Set[SearchSuccess] = {
555572
val typer = ctx.typer
556573
val conversions = new typer.ImplicitSearch(defn.AnyType, qual, pos.span).allImplicits
557574
val targets = conversions.map(_.tree.tpe)
558575

559576
interactiv.println(i"implicit conversion targets considered: ${targets.toList}%, %")
560-
targets
577+
conversions
561578
}
562579

563580
/** Filter for names that should appear when looking for completions. */
@@ -602,3 +619,7 @@ object Completion:
602619
/** Both term and type symbols are allowed */
603620
val ImportOrExport: Mode = new Mode(4) | Term | Type
604621

622+
val Scope: Mode = new Mode(8)
623+
624+
val Member: Mode = new Mode(16)
625+

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,21 @@ object Interactive {
423423
false
424424
}
425425

426+
427+
/** Some information about the trees is lost after Typer such as Extension method construct
428+
* is expanded into methods. In order to support completions in those cases
429+
* we have to rely on untyped trees and only when types are necessary use typed trees.
430+
*/
431+
def resolveTypedOrUntypedPath(tpdPath: List[Tree], pos: SourcePosition)(using Context): List[untpd.Tree] =
432+
lazy val untpdPath: List[untpd.Tree] = NavigateAST
433+
.pathTo(pos.span, List(ctx.compilationUnit.untpdTree), true).collect:
434+
case untpdTree: untpd.Tree => untpdTree
435+
436+
tpdPath match
437+
case (_: Bind) :: _ => tpdPath
438+
case (_: untpd.TypTree) :: _ => tpdPath
439+
case _ => untpdPath
440+
426441
/**
427442
* Is this tree using a renaming introduced by an import statement or an alias for `this`?
428443
*

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

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,33 +1019,33 @@ class CompletionTest {
10191019
| val x = Bar.${m1}"""
10201020
.completion(
10211021
("getClass", Method, "[X0 >: Foo.Bar.type](): Class[? <: X0]"),
1022-
("ensuring", Method, "(cond: Boolean): A"),
1022+
("ensuring", Method, "(cond: Boolean): Foo.Bar.type"),
10231023
("##", Method, "=> Int"),
10241024
("nn", Method, "=> Foo.Bar.type"),
10251025
("==", Method, "(x$0: Any): Boolean"),
1026-
("ensuring", Method, "(cond: Boolean, msg: => Any): A"),
1026+
("ensuring", Method, "(cond: Boolean, msg: => Any): Foo.Bar.type"),
10271027
("ne", Method, "(x$0: Object): Boolean"),
10281028
("valueOf", Method, "($name: String): Foo.Bar"),
10291029
("equals", Method, "(x$0: Any): Boolean"),
10301030
("wait", Method, "(x$0: Long): Unit"),
10311031
("hashCode", Method, "(): Int"),
10321032
("notifyAll", Method, "(): Unit"),
10331033
("values", Method, "=> Array[Foo.Bar]"),
1034-
("", Method, "[B](y: B): (A, B)"),
1034+
("", Method, "[B](y: B): (Foo.Bar.type, B)"),
10351035
("!=", Method, "(x$0: Any): Boolean"),
10361036
("fromOrdinal", Method, "(ordinal: Int): Foo.Bar"),
10371037
("asInstanceOf", Method, "[X0]: X0"),
1038-
("->", Method, "[B](y: B): (A, B)"),
1038+
("->", Method, "[B](y: B): (Foo.Bar.type, B)"),
10391039
("wait", Method, "(x$0: Long, x$1: Int): Unit"),
10401040
("`back-tick`", Field, "Foo.Bar"),
10411041
("notify", Method, "(): Unit"),
10421042
("formatted", Method, "(fmtstr: String): String"),
1043-
("ensuring", Method, "(cond: A => Boolean, msg: => Any): A"),
1043+
("ensuring", Method, "(cond: Foo.Bar.type => Boolean, msg: => Any): Foo.Bar.type"),
10441044
("wait", Method, "(): Unit"),
10451045
("isInstanceOf", Method, "[X0]: Boolean"),
10461046
("`match`", Field, "Foo.Bar"),
10471047
("toString", Method, "(): String"),
1048-
("ensuring", Method, "(cond: A => Boolean): A"),
1048+
("ensuring", Method, "(cond: Foo.Bar.type => Boolean): Foo.Bar.type"),
10491049
("eq", Method, "(x$0: Object): Boolean"),
10501050
("synchronized", Method, "[X0](x$0: X0): X0")
10511051
)
@@ -1576,6 +1576,61 @@ class CompletionTest {
15761576
|"""
15771577
.completion(m1, Set(("TTT", Field, "T.TTT")))
15781578

1579+
@Test def properTypeVariable: Unit =
1580+
code"""|object M:
1581+
| List(1,2,3).filterNo$m1
1582+
|"""
1583+
.completion(m1, Set(("filterNot", Method, "(p: Int => Boolean): List[Int]")))
1584+
1585+
@Test def properTypeVariableForExtensionMethods: Unit =
1586+
code"""|object M:
1587+
| extension [T](x: List[T]) def test(aaa: T): T = ???
1588+
| List(1,2,3).tes$m1
1589+
|
1590+
|"""
1591+
.completion(m1, Set(("test", Method, "(aaa: Int): Int")))
1592+
1593+
@Test def properTypeVariableForExtensionMethodsByName: Unit =
1594+
code"""|object M:
1595+
| extension [T](xs: List[T]) def test(p: T => Boolean): List[T] = ???
1596+
| List(1,2,3).tes$m1
1597+
|"""
1598+
.completion(m1, Set(("test", Method, "(p: Int => Boolean): List[Int]")))
1599+
1600+
@Test def genericExtensionTypeParameterInference: Unit =
1601+
code"""|object M:
1602+
| extension [T](xs: T) def test(p: T): T = ???
1603+
| 3.tes$m1
1604+
|"""
1605+
.completion(m1, Set(("test", Method, "(p: Int): Int")))
1606+
1607+
@Test def genericExtensionTypeParameterInferenceByName: Unit =
1608+
code"""|object M:
1609+
| extension [T](xs: T) def test(p: T => Boolean): T = ???
1610+
| 3.tes$m1
1611+
|"""
1612+
.completion(m1, Set(("test", Method, "(p: Int => Boolean): Int")))
1613+
1614+
@Test def properTypeVariableForImplicitDefs: Unit =
1615+
code"""|object M:
1616+
| implicit class ListUtils[T](xs: List[T]) {
1617+
| def test(p: T => Boolean): List[T] = ???
1618+
| }
1619+
| List(1,2,3).tes$m1
1620+
|"""
1621+
.completion(m1, Set(("test", Method, "(p: Int => Boolean): List[Int]")))
1622+
1623+
@Test def properTypeParameterForImplicitDefs: Unit =
1624+
code"""|object M:
1625+
| implicit class ListUtils[T](xs: T) {
1626+
| def test(p: T => Boolean): T = ???
1627+
| }
1628+
| new ListUtils(1).tes$m1
1629+
| 1.tes$m2
1630+
|"""
1631+
.completion(m1, Set(("test", Method, "(p: Int => Boolean): Int")))
1632+
.completion(m2, Set(("test", Method, "(p: Int => Boolean): Int")))
1633+
15791634
@Test def selectDynamic: Unit =
15801635
code"""|import scala.language.dynamics
15811636
|class Foo extends Dynamic {

0 commit comments

Comments
 (0)