Skip to content

Commit 65645e5

Browse files
rochalaWojciechMazur
authored andcommitted
Instantiate Type Vars in completion labels of extension methods (#18914)
This PR fixes the completion labels for extension methods and old style completion methods. It required an API change of `Completion.scala` as it didn't contain enough information to properly create labels on presentation compiler side. Along with this following parts were rewritten to avoid recomputation: - computation of adjustedPath which is necessary for extension construct completions, - computation of prefix is now unified across the metals and presentation compilers, - completionKind was changed, and kind Scope, Member are now part of completion mode. The biggest change is basically added support for old style extension methods. I found out that the type var was not instantiated after `ImplicitSearch` computation, and was only calculated at later stages. I don't have enough knowledge about Inference and Typer to know whether this is a bug or rather an intended behaviour but I managed to solve it without touching the typer: ```scala def tryToInstantiateTypeVars(conversionTarget: SearchSuccess): Type = try val typingCtx = ctx.fresh inContext(typingCtx): val methodRefTree = ref(conversionTarget.ref, needLoad = false) val convertedTree = ctx.typer.typedAheadExpr(untpd.Apply(untpd.TypedSplice(methodRefTree), untpd.TypedSplice(qual) :: Nil)) Inferencing.fullyDefinedType(convertedTree.tpe, "", pos) catch case error => conversionTarget.tree.tpe // fallback to not fully defined type ``` [Cherry-picked 95266f2]
1 parent f14c9dd commit 65645e5

20 files changed

+525
-375
lines changed

compiler/src/dotty/tools/dotc/core/Flags.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ object Flags {
242242
val (AccessorOrSealed @ _, Accessor @ _, Sealed @ _) = newFlags(11, "<accessor>", "sealed")
243243

244244
/** A mutable var, an open class */
245-
val (MutableOrOpen @ __, Mutable @ _, Open @ _) = newFlags(12, "mutable", "open")
245+
val (MutableOrOpen @ _, Mutable @ _, Open @ _) = newFlags(12, "mutable", "open")
246246

247247
/** Symbol is local to current class (i.e. private[this] or protected[this]
248248
* pre: Private or Protected are also set

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

+159-113
Large diffs are not rendered by default.

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

+29
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,21 @@ object Interactive {
420420
false
421421
}
422422

423+
424+
/** Some information about the trees is lost after Typer such as Extension method construct
425+
* is expanded into methods. In order to support completions in those cases
426+
* we have to rely on untyped trees and only when types are necessary use typed trees.
427+
*/
428+
def resolveTypedOrUntypedPath(tpdPath: List[Tree], pos: SourcePosition)(using Context): List[untpd.Tree] =
429+
lazy val untpdPath: List[untpd.Tree] = NavigateAST
430+
.pathTo(pos.span, List(ctx.compilationUnit.untpdTree), true).collect:
431+
case untpdTree: untpd.Tree => untpdTree
432+
433+
tpdPath match
434+
case (_: Bind) :: _ => tpdPath
435+
case (_: untpd.TypTree) :: _ => tpdPath
436+
case _ => untpdPath
437+
423438
/**
424439
* Is this tree using a renaming introduced by an import statement or an alias for `this`?
425440
*
@@ -436,6 +451,20 @@ object Interactive {
436451
def sameName(n0: Name, n1: Name): Boolean =
437452
n0.stripModuleClassSuffix.toTermName eq n1.stripModuleClassSuffix.toTermName
438453

454+
/** https://scala-lang.org/files/archive/spec/3.4/02-identifiers-names-and-scopes.html
455+
* import java.lang.*
456+
* {
457+
* import scala.*
458+
* {
459+
* import Predef.*
460+
* { /* source */ }
461+
* }
462+
* }
463+
*/
464+
def isImportedByDefault(sym: Symbol)(using Context): Boolean =
465+
val owner = sym.effectiveOwner
466+
owner == defn.ScalaPredefModuleClass || owner == defn.ScalaPackageClass || owner == defn.JavaLangPackageClass
467+
439468
private[interactive] def safely[T](op: => List[T]): List[T] =
440469
try op catch { case ex: TypeError => Nil }
441470
}

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

+94-6
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 {
@@ -1591,4 +1646,37 @@ class CompletionTest {
15911646
|"""
15921647
.completion(m1, Set(("selectDynamic", Method, "(field: String): Foo")))
15931648
.completion(m2, Set(("banana", Method, "=> Int")))
1649+
1650+
@Test def shadowedImport: Unit =
1651+
code"""|
1652+
|import Matches.*
1653+
|object Matches {
1654+
| val Number = "".r
1655+
|}
1656+
|object Main {
1657+
| Num$m1
1658+
|}
1659+
|""".completion(m1, Set(
1660+
("Number", Field, "scala.util.matching.Regex"),
1661+
("NumberFormatException", Module, "NumberFormatException"),
1662+
("Numeric", Field, "scala.math.Numeric")
1663+
))
1664+
1665+
@Test def shadowedImportType: Unit =
1666+
code"""|
1667+
|import Matches.*
1668+
|object Matches {
1669+
| val Number = "".r
1670+
|}
1671+
|object Main {
1672+
| val x: Num$m1
1673+
|}
1674+
|""".completion(m1, Set(
1675+
("Number", Class, "Number"),
1676+
("Number", Field, "scala.util.matching.Regex"),
1677+
("NumberFormatException", Module, "NumberFormatException"),
1678+
("NumberFormatException", Field, "NumberFormatException"),
1679+
("Numeric", Field, "Numeric"),
1680+
("Numeric", Field, "scala.math.Numeric")
1681+
))
15941682
}

presentation-compiler/src/main/dotty/tools/pc/IndexedContext.scala

+27-9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import dotty.tools.dotc.core.Names.*
1010
import dotty.tools.dotc.core.Scopes.EmptyScope
1111
import dotty.tools.dotc.core.Symbols.*
1212
import dotty.tools.dotc.core.Types.*
13+
import dotty.tools.dotc.interactive.Interactive
1314
import dotty.tools.dotc.typer.ImportInfo
1415
import dotty.tools.pc.IndexedContext.Result
1516
import dotty.tools.pc.utils.MtagsEnrichments.*
@@ -31,16 +32,33 @@ sealed trait IndexedContext:
3132
case Some(symbols) if symbols.exists(_ == sym) =>
3233
Result.InScope
3334
case Some(symbols)
34-
if symbols
35-
.exists(s => isTypeAliasOf(s, sym) || isTermAliasOf(s, sym)) =>
36-
Result.InScope
35+
if symbols.exists(s => isNotConflictingWithDefault(s, sym) || isTypeAliasOf(s, sym) || isTermAliasOf(s, sym)) =>
36+
Result.InScope
3737
// when all the conflicting symbols came from an old version of the file
38-
case Some(symbols) if symbols.nonEmpty && symbols.forall(_.isStale) =>
39-
Result.Missing
38+
case Some(symbols) if symbols.nonEmpty && symbols.forall(_.isStale) => Result.Missing
4039
case Some(_) => Result.Conflict
4140
case None => Result.Missing
4241
end lookupSym
4342

43+
/**
44+
* Scala by default imports following packages:
45+
* https://scala-lang.org/files/archive/spec/3.4/02-identifiers-names-and-scopes.html
46+
* import java.lang.*
47+
* {
48+
* import scala.*
49+
* {
50+
* import Predef.*
51+
* { /* source */ }
52+
* }
53+
* }
54+
*
55+
* This check is necessary for proper scope resolution, because when we compare symbols from
56+
* index including the underlying type like scala.collection.immutable.List it actually
57+
* is in current scope in form of type forwarder imported from Predef.
58+
*/
59+
private def isNotConflictingWithDefault(sym: Symbol, queriedSym: Symbol): Boolean =
60+
sym.info.widenDealias =:= queriedSym.info.widenDealias && (Interactive.isImportedByDefault(sym))
61+
4462
final def hasRename(sym: Symbol, as: String): Boolean =
4563
rename(sym) match
4664
case Some(v) => v == as
@@ -49,15 +67,15 @@ sealed trait IndexedContext:
4967
// detects import scope aliases like
5068
// object Predef:
5169
// val Nil = scala.collection.immutable.Nil
52-
private def isTermAliasOf(termAlias: Symbol, sym: Symbol): Boolean =
70+
private def isTermAliasOf(termAlias: Symbol, queriedSym: Symbol): Boolean =
5371
termAlias.isTerm && (
54-
sym.info match
72+
queriedSym.info match
5573
case clz: ClassInfo => clz.appliedRef =:= termAlias.info.resultType
5674
case _ => false
5775
)
5876

59-
private def isTypeAliasOf(alias: Symbol, sym: Symbol): Boolean =
60-
alias.isAliasType && alias.info.metalsDealias.typeSymbol == sym
77+
private def isTypeAliasOf(alias: Symbol, queriedSym: Symbol): Boolean =
78+
alias.isAliasType && alias.info.metalsDealias.typeSymbol == queriedSym
6179

6280
final def isEmpty: Boolean = this match
6381
case IndexedContext.Empty => true

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

+9-71
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,20 @@ import java.net.URI
55

66
import scala.meta.pc.OffsetParams
77

8-
import dotty.tools.dotc.ast.tpd.*
8+
import dotty.tools.dotc.ast.untpd.*
99
import dotty.tools.dotc.ast.untpd.ImportSelector
1010
import dotty.tools.dotc.core.Contexts.*
1111
import dotty.tools.dotc.core.StdNames.*
1212
import dotty.tools.dotc.util.Chars
1313
import dotty.tools.dotc.util.SourcePosition
1414
import dotty.tools.dotc.util.Spans
15+
import dotty.tools.dotc.interactive.Completion
1516
import dotty.tools.pc.utils.MtagsEnrichments.*
1617

1718
import org.eclipse.lsp4j as l
1819
import scala.annotation.tailrec
1920

20-
enum CompletionKind:
21-
case Empty, Scope, Members
22-
2321
case class CompletionPos(
24-
kind: CompletionKind,
2522
start: Int,
2623
end: Int,
2724
query: String,
@@ -40,29 +37,21 @@ object CompletionPos:
4037
def infer(
4138
cursorPos: SourcePosition,
4239
offsetParams: OffsetParams,
43-
treePath: List[Tree]
40+
adjustedPath: List[Tree]
4441
)(using Context): CompletionPos =
45-
infer(cursorPos, offsetParams.uri().nn, offsetParams.text().nn, treePath)
42+
infer(cursorPos, offsetParams.uri().nn, offsetParams.text().nn, adjustedPath)
4643

4744
def infer(
4845
cursorPos: SourcePosition,
4946
uri: URI,
5047
text: String,
51-
treePath: List[Tree]
48+
adjustedPath: List[Tree]
5249
)(using Context): CompletionPos =
53-
val start = inferIdentStart(cursorPos, text, treePath)
54-
val end = inferIdentEnd(cursorPos, text)
55-
val query = text.substring(start, end)
56-
val prevIsDot =
57-
if start - 1 >= 0 then text.charAt(start - 1) == '.' else false
58-
val kind =
59-
if prevIsDot then CompletionKind.Members
60-
else if isImportOrExportSelect(cursorPos, treePath) then
61-
CompletionKind.Members
62-
else if query.nn.isEmpty then CompletionKind.Empty
63-
else CompletionKind.Scope
50+
val identEnd = inferIdentEnd(cursorPos, text)
51+
val query = Completion.completionPrefix(adjustedPath, cursorPos)
52+
val start = cursorPos.point - query.length()
6453

65-
CompletionPos(kind, start, end, query.nn, cursorPos, uri)
54+
CompletionPos(start, identEnd, query.nn, cursorPos, uri)
6655
end infer
6756

6857
/**
@@ -87,57 +76,6 @@ object CompletionPos:
8776
(i, tabIndented)
8877
end inferIndent
8978

90-
private def isImportOrExportSelect(
91-
pos: SourcePosition,
92-
path: List[Tree],
93-
)(using Context): Boolean =
94-
@tailrec
95-
def loop(enclosing: List[Tree]): Boolean =
96-
enclosing match
97-
case head :: tl if !head.sourcePos.contains(pos) => loop(tl)
98-
case (tree: (Import | Export)) :: _ =>
99-
tree.selectors.exists(_.imported.sourcePos.contains(pos))
100-
case _ => false
101-
102-
loop(path)
103-
104-
105-
/**
106-
* Returns the start offset of the identifier starting as the given offset position.
107-
*/
108-
private def inferIdentStart(
109-
pos: SourcePosition,
110-
text: String,
111-
path: List[Tree]
112-
)(using Context): Int =
113-
def fallback: Int =
114-
var i = pos.point - 1
115-
while i >= 0 && Chars.isIdentifierPart(text.charAt(i)) do i -= 1
116-
i + 1
117-
def loop(enclosing: List[Tree]): Int =
118-
enclosing match
119-
case Nil => fallback
120-
case head :: tl =>
121-
if !head.sourcePos.contains(pos) then loop(tl)
122-
else
123-
head match
124-
case i: Ident => i.sourcePos.point
125-
case s: Select =>
126-
if s.name.toTermName == nme.ERROR || s.span.exists && pos.span.point < s.span.point
127-
then fallback
128-
else s.span.point
129-
case Import(_, sel) =>
130-
sel
131-
.collectFirst {
132-
case ImportSelector(imported, renamed, _)
133-
if imported.sourcePos.contains(pos) =>
134-
imported.sourcePos.point
135-
}
136-
.getOrElse(fallback)
137-
case _ => fallback
138-
loop(path)
139-
end inferIdentStart
140-
14179
/**
14280
* Returns the end offset of the identifier starting as the given offset position.
14381
*/

0 commit comments

Comments
 (0)