Skip to content

Commit 4eabca5

Browse files
authored
Add support for type parameters in signatureHelp (#15517)
* add support for type parameters in signatureHelp
1 parent 67ca67c commit 4eabca5

File tree

3 files changed

+209
-77
lines changed

3 files changed

+209
-77
lines changed

compiler/src/dotty/tools/dotc/util/Signatures.scala

Lines changed: 120 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import core.Denotations.{SingleDenotation, Denotation}
99
import core.Flags
1010
import core.NameOps.isUnapplyName
1111
import core.Names._
12+
import core.NameKinds
1213
import core.Types._
1314
import core.Symbols.NoSymbol
1415
import interactive.Interactive
@@ -54,7 +55,8 @@ object Signatures {
5455
*
5556
* @param path The path to the function application
5657
* @param span The position of the cursor
57-
* @return A triple containing the index of the parameter being edited, the index of functeon
58+
*
59+
* @return A triple containing the index of the parameter being edited, the index of functeon
5860
* being called, the list of overloads of this function).
5961
*/
6062
def signatureHelp(path: List[tpd.Tree], pos: Span)(using Context): (Int, Int, List[Signature]) =
@@ -65,7 +67,8 @@ object Signatures {
6567
*
6668
* @param path The path to the function application
6769
* @param span The position of the cursor
68-
* @return A triple containing the index of the parameter being edited, the index of the function
70+
*
71+
* @return A triple containing the index of the parameter being edited, the index of the function
6972
* being called, the list of overloads of this function).
7073
*/
7174
@deprecated(
@@ -82,49 +85,76 @@ object Signatures {
8285
*
8386
* @param path The path to the function application
8487
* @param span The position of the cursor
88+
*
8589
* @return A triple containing the index of the parameter being edited, the index of the function
8690
* being called, the list of overloads of this function).
8791
*/
8892
def computeSignatureHelp(path: List[tpd.Tree], span: Span)(using Context): (Int, Int, List[Signature]) =
8993
findEnclosingApply(path, span) match
90-
case tpd.EmptyTree => (0, 0, Nil)
9194
case Apply(fun, params) => applyCallInfo(span, params, fun)
9295
case UnApply(fun, _, patterns) => unapplyCallInfo(span, fun, patterns)
96+
case appliedTypeTree @ AppliedTypeTree(_, types) => appliedTypeTreeCallInfo(appliedTypeTree, types)
97+
case tp @ TypeApply(fun, types) => applyCallInfo(span, types, fun, true)
98+
case _ => (0, 0, Nil)
9399

94100
/**
95101
* Finds enclosing application from given `path` for `span`.
96102
*
97103
* @param path The path to the function application
98104
* @param span The position of the cursor
105+
*
99106
* @return Tree which encloses closest application containing span.
100107
* In case if cursor is pointing on closing parenthesis and
101108
* next subsequent application exists, it returns the latter
102109
*/
103110
private def findEnclosingApply(path: List[tpd.Tree], span: Span)(using Context): tpd.Tree =
104111
path.filterNot {
105-
case apply @ Apply(fun, _) => fun.span.contains(span) || isTuple(apply)
106-
case unapply @ UnApply(fun, _, _) => fun.span.contains(span) || isTuple(unapply)
112+
case apply @ Apply(fun, _) => fun.span.contains(span) || isValid(apply)
113+
case unapply @ UnApply(fun, _, _) => fun.span.contains(span) || isValid(unapply)
114+
case typeTree @ AppliedTypeTree(fun, _) => fun.span.contains(span) || isValid(typeTree)
115+
case typeApply @ TypeApply(fun, _) => fun.span.contains(span) || isValid(typeApply)
107116
case _ => true
108117
} match {
109118
case Nil => tpd.EmptyTree
110-
case direct :: enclosing :: _ if direct.source(span.end -1) == ')' => enclosing
119+
case direct :: enclosing :: _ if isClosingSymbol(direct.source(span.end -1)) => enclosing
111120
case direct :: _ => direct
112121
}
113122

123+
private def isClosingSymbol(ch: Char) = ch == ')' || ch == ']'
124+
114125
/**
115-
* Extracts call information for a function application.
126+
* Extracts call information for applied type tree:
116127
*
117-
* @param span The position of the cursor
118-
* @param params Current function parameters
119-
* @param fun Function tree which is being applied
128+
* @param types Currently applied function type parameters
129+
* @param fun Function tree which is being applied
130+
*/
131+
private def appliedTypeTreeCallInfo(
132+
fun: tpd.Tree,
133+
types: List[tpd.Tree]
134+
)(using Context): (Int, Int, List[Signature]) =
135+
val typeName = fun.symbol.name.show
136+
val typeParams = fun.symbol.typeRef.typeParams.map(_.paramName.show)
137+
val denot = fun.denot.asSingleDenotation
138+
val activeParameter = (types.length - 1) max 0
139+
140+
val signature = Signature(typeName, typeParams, Nil, Some(typeName) , None, Some(denot))
141+
(activeParameter, 0, List(signature))
142+
143+
/**
144+
* Extracts call information for a function application and type application.
120145
*
146+
* @param span The position of the cursor
147+
* @param params Current function parameters
148+
* @param fun Function tree which is being applied
149+
* @param isTypeApply Is a type application
121150
* @return A triple containing the index of the parameter being edited, the index of the function
122151
* being called, the list of overloads of this function).
123152
*/
124153
private def applyCallInfo(
125154
span: Span,
126155
params: List[tpd.Tree],
127-
fun: tpd.Tree
156+
fun: tpd.Tree,
157+
isTypeApply: Boolean = false
128158
)(using Context): (Int, Int, List[Signature]) =
129159
def treeQualifier(tree: tpd.Tree): tpd.Tree = tree match
130160
case Apply(qual, _) => treeQualifier(qual)
@@ -154,10 +184,11 @@ object Signatures {
154184

155185
if alternativeIndex < alternatives.length then
156186
val curriedArguments = countParams(fun, alternatives(alternativeIndex))
157-
val paramIndex = params.indexWhere(_.span.contains(span)) match {
158-
case -1 => (params.length - 1 max 0) + curriedArguments
159-
case n => n + curriedArguments
160-
}
187+
// We have to shift all arguments by number of type parameters to properly show activeParameter index
188+
val typeParamsShift = if !isTypeApply then fun.symbol.denot.paramSymss.flatten.filter(_.isType).length else 0
189+
val paramIndex = params.indexWhere(_.span.contains(span)) match
190+
case -1 => (params.length - 1 max 0) + curriedArguments + typeParamsShift
191+
case n => n + curriedArguments + typeParamsShift
161192

162193
val pre = treeQualifier(fun)
163194
val alternativesWithTypes = alternatives.map(_.asSeenFrom(pre.tpe.widenTermRefExpr))
@@ -170,9 +201,9 @@ object Signatures {
170201
/**
171202
* Extracts call informatioin for function in unapply context.
172203
*
173-
* @param span The position of the cursor
204+
* @param span The position of the cursor
174205
* @param params Current function parameters
175-
* @param fun Unapply function tree
206+
* @param fun Unapply function tree
176207
*
177208
* @return A triple containing the index of the parameter being edited, the index of the function
178209
* being called, the list of overloads of this function).
@@ -202,7 +233,8 @@ object Signatures {
202233
* Extract parameter names from `resultType` only if `resultType` is case class and `denot` is synthetic.
203234
*
204235
* @param resultType Function result type
205-
* @param denot Function denotation
236+
* @param denot Function denotation
237+
*
206238
* @return List of lists of names of parameters if `resultType` is case class without overriden unapply
207239
*/
208240
private def extractParamNamess(resultType: Type, denot: Denotation)(using Context): List[List[Name]] =
@@ -214,9 +246,10 @@ object Signatures {
214246
/**
215247
* Extract parameter types from `resultType` in unapply context.
216248
*
217-
* @param resultType Function result type
218-
* @param denot Function denotation
249+
* @param resultType Function result type
250+
* @param denot Function denotation
219251
* @param patternsSize Number of pattern trees present in function tree
252+
*
220253
* @return List of lists of types present in unapply clause
221254
*/
222255
private def extractParamTypess(
@@ -263,13 +296,19 @@ object Signatures {
263296
case (-1, pos) => -1 // there are patterns, we must be outside range so we set no active parameter
264297
case _ => (maximumParams - 1) min patternPosition max 0 // handle unapplySeq to always highlight Seq[A] on elements
265298

266-
private def isTuple(tree: tpd.Tree)(using Context): Boolean =
267-
tree.symbol != NoSymbol && ctx.definitions.isTupleClass(tree.symbol.owner.companionClass)
299+
/**
300+
* Checks if tree is valid for signatureHelp. Skipped trees are either tuple type or function type
301+
*
302+
* @param tree tree to validate
303+
*/
304+
private def isValid(tree: tpd.Tree)(using Context): Boolean =
305+
ctx.definitions.isTupleNType(tree.tpe) || ctx.definitions.isFunctionType(tree.tpe)
268306

269307
/**
270308
* Get unapply method result type omiting unknown types and another method calls.
271309
*
272310
* @param fun Unapply tree
311+
*
273312
* @return Proper unapply method type after extracting result from method types and omiting unknown types.
274313
*/
275314
private def unapplyMethodResult(fun: tpd.Tree)(using Context): Type =
@@ -292,9 +331,10 @@ object Signatures {
292331
*
293332
* @see [[https://docs.scala-lang.org/scala3/reference/changed-features/pattern-matching.html]]
294333
*
295-
* @param resultType Final result type for unapply
334+
* @param resultType Final result type for unapply
296335
* @param patternCount Currently applied patterns to unapply function
297336
* @param isUnapplySeq true if unapply name equals "unapplySeq", false otherwise
337+
*
298338
* @return List of List of types dependent on option less extractor type.
299339
*/
300340
private def mapOptionLessUnapply(
@@ -333,40 +373,63 @@ object Signatures {
333373
* Creates signature for apply method.
334374
*
335375
* @param denot Function denotation for which apply signature is returned.
376+
*
336377
* @return Signature if denot is a function, None otherwise
337378
*/
338379
private def toApplySignature(denot: SingleDenotation)(using Context): Option[Signature] = {
339380
val symbol = denot.symbol
340381
val docComment = ParsedComment.docOf(symbol)
341382

342-
def toParamss(tp: MethodType)(using Context): List[List[Param]] = {
343-
val rest = tp.resType match {
344-
case res: MethodType =>
345-
// Hide parameter lists consisting only of DummyImplicit,
346-
if (res.resultType.isParameterless &&
347-
res.isImplicitMethod &&
348-
res.paramInfos.forall(info =>
349-
info.classSymbol.derivesFrom(ctx.definitions.DummyImplicitClass)))
350-
Nil
351-
else
352-
toParamss(res)
383+
def isDummyImplicit(res: MethodType): Boolean =
384+
res.resultType.isParameterless &&
385+
res.isImplicitMethod &&
386+
res.paramInfos.forall(info =>
387+
info.classSymbol.derivesFrom(ctx.definitions.DummyImplicitClass))
388+
389+
def toParamss(tp: Type)(using Context): List[List[Param]] =
390+
val rest = tp.resultType match
391+
case res: MethodType => if isDummyImplicit(res) then Nil else toParamss(res)
392+
case _ => Nil
393+
394+
val currentParams = (tp.paramNamess, tp.paramInfoss) match
395+
case (params :: _, infos :: _) => params zip infos
353396
case _ => Nil
354-
}
355-
val params = tp.paramNames.zip(tp.paramInfos).map { case (name, info) =>
356-
Signatures.Param(
357-
name.show,
358-
info.widenTermRefExpr.show,
359-
docComment.flatMap(_.paramDoc(name)),
360-
isImplicit = tp.isImplicitMethod)
361-
}
362-
params :: rest
363-
}
397+
398+
val params = currentParams.map { (name, info) =>
399+
Signatures.Param(
400+
name.show,
401+
info.widenTermRefExpr.show,
402+
docComment.flatMap(_.paramDoc(name)),
403+
isImplicit = tp.isImplicitMethod
404+
)
405+
}
406+
407+
(params :: rest)
408+
409+
def isSyntheticEvidence(name: String) =
410+
if !name.startsWith(NameKinds.EvidenceParamName.separator) then false else
411+
symbol.paramSymss.flatten.find(_.name.show == name).exists(_.flags.is(Flags.Implicit))
364412

365413
denot.info.stripPoly match
366-
case tpe: MethodType =>
367-
val paramss = toParamss(tpe)
414+
case tpe: (MethodType | AppliedType | TypeRef | TypeParamRef) =>
415+
val paramss = toParamss(tpe).map(_.filterNot(param => isSyntheticEvidence(param.name)))
416+
val evidenceParams = (tpe.paramNamess.flatten zip tpe.paramInfoss.flatten).flatMap {
417+
case (name, AppliedType(tpe, (ref: TypeParamRef) :: _)) if isSyntheticEvidence(name.show) =>
418+
Some(ref.paramName -> tpe)
419+
case _ => None
420+
}
421+
368422
val typeParams = denot.info match
369-
case poly: PolyType => poly.paramNames.zip(poly.paramInfos).map { case (x, y) => x.show + y.show }
423+
case poly: PolyType =>
424+
val tparams = poly.paramNames.zip(poly.paramInfos)
425+
tparams.map { (name, info) =>
426+
evidenceParams.find((evidenceName: TypeName, _: Type) => name == evidenceName).flatMap {
427+
case (_, tparam) => tparam.show.split('.').lastOption
428+
} match {
429+
case Some(evidenceTypeName) => s"${name.show}: ${evidenceTypeName}"
430+
case None => name.show + info.show
431+
}
432+
}
370433
case _ => Nil
371434
val (name, returnType) =
372435
if (symbol.isConstructor) then
@@ -400,10 +463,11 @@ object Signatures {
400463
* where _$n is only present for synthetic product extractors such as case classes.
401464
* In rest of cases signature skips them resulting in pattern (T1, T2, T3, Tn)
402465
*
403-
* @param denot Unapply denotation
466+
* @param denot Unapply denotation
404467
* @param paramNames Parameter names for unapply final result type.
405-
* It non empty only when unapply returns synthetic product as for case classes.
468+
* It non empty only when unapply returns synthetic product as for case classes.
406469
* @param paramTypes Parameter types for unapply final result type.
470+
*
407471
* @return Signature if paramTypes is non empty, None otherwise
408472
*/
409473
private def toUnapplySignature(denot: SingleDenotation, paramNames: List[Name], paramTypes: List[Type])(using Context): Option[Signature] =
@@ -425,17 +489,17 @@ object Signatures {
425489
* `countParams` should be 3. It also takes into considerations unapplied arguments so for `foo(1)(3)`
426490
* we will still get 3, as first application `foo(1)` takes 2 parameters with currently only 1 applied.
427491
*
428-
* @param tree The tree to inspect.
429-
* @param denot Denotation of function we are trying to apply
492+
* @param tree The tree to inspect.
493+
* @param denot Denotation of function we are trying to apply
430494
* @param alreadyCurried Number of subsequent Apply trees before current tree
495+
*
431496
* @return The number of parameters that are passed.
432497
*/
433498
private def countParams(tree: tpd.Tree, denot: SingleDenotation, alreadyCurried: Int = 0)(using Context): Int =
434-
tree match {
499+
tree match
435500
case Apply(fun, params) =>
436501
countParams(fun, denot, alreadyCurried + 1) + denot.symbol.paramSymss(alreadyCurried).length
437502
case _ => 0
438-
}
439503

440504
/**
441505
* Inspect `err` to determine, if it is an error related to application of an overloaded
@@ -447,6 +511,7 @@ object Signatures {
447511
*
448512
* @param err The error message to inspect.
449513
* @param params The parameters that were given at the call site.
514+
*
450515
* @return A pair composed of the index of the best alternative (0 if no alternatives
451516
* were found), and the list of alternatives.
452517
*/
@@ -460,20 +525,18 @@ object Signatures {
460525
// If the user writes `foo(bar, <cursor>)`, the typer will insert a synthetic
461526
// `null` parameter: `foo(bar, null)`. This may influence what's the "best"
462527
// alternative, so we discard it.
463-
val userParams = params match {
528+
val userParams = params match
464529
case xs :+ (nul @ Literal(Constant(null))) if nul.span.isZeroExtent => xs
465530
case _ => params
466-
}
467531
val userParamsTypes = userParams.map(_.tpe)
468532

469533
// Assign a score to each alternative (how many parameters are correct so far), and
470534
// use that to determine what is the current active signature.
471535
val alternativesScores = alternatives.map { alt =>
472-
alt.info.stripPoly match {
536+
alt.info.stripPoly match
473537
case tpe: MethodType =>
474538
userParamsTypes.zip(tpe.paramInfos).takeWhile{ case (t0, t1) => t0 <:< t1 }.size
475539
case _ => 0
476-
}
477540
}
478541
val bestAlternative =
479542
if (alternativesScores.isEmpty) 0

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -929,18 +929,26 @@ object DottyLanguageServer {
929929

930930
/** Convert `signature` to a `SignatureInformation` */
931931
def signatureToSignatureInformation(signature: Signatures.Signature): lsp4j.SignatureInformation = {
932-
val paramInfoss = signature.paramss.map(_.map(paramToParameterInformation))
933-
val paramLists = signature.paramss.map { paramList =>
934-
val labels = paramList.map(_.show)
935-
val prefix = if (paramList.exists(_.isImplicit)) "implicit " else ""
936-
labels.mkString(prefix, ", ", "")
937-
}.mkString("(", ")(", ")")
932+
val tparams = signature.tparams.map(Signatures.Param("", _))
933+
val paramInfoss =
934+
(tparams ::: signature.paramss.flatten).map(paramToParameterInformation)
935+
val paramLists =
936+
if signature.paramss.forall(_.isEmpty) && tparams.nonEmpty then
937+
""
938+
else
939+
signature.paramss
940+
.map { paramList =>
941+
val labels = paramList.map(_.show)
942+
val prefix = if paramList.exists(_.isImplicit) then "using " else ""
943+
labels.mkString(prefix, ", ", "")
944+
}
945+
.mkString("(", ")(", ")")
938946
val tparamsLabel = if (signature.tparams.isEmpty) "" else signature.tparams.mkString("[", ", ", "]")
939947
val returnTypeLabel = signature.returnType.map(t => s": $t").getOrElse("")
940948
val label = s"${signature.name}$tparamsLabel$paramLists$returnTypeLabel"
941949
val documentation = signature.doc.map(DottyLanguageServer.markupContent)
942950
val sig = new lsp4j.SignatureInformation(label)
943-
sig.setParameters(paramInfoss.flatten.asJava)
951+
sig.setParameters(paramInfoss.asJava)
944952
documentation.foreach(sig.setDocumentation(_))
945953
sig
946954
}

0 commit comments

Comments
 (0)