Skip to content

Commit d13b7a7

Browse files
authored
Merge pull request #15203 from rochala/signature-help-unapply
Add proper signatureHelp for unapply methods
2 parents 8bae490 + baeb5a0 commit d13b7a7

File tree

2 files changed

+434
-89
lines changed

2 files changed

+434
-89
lines changed

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

Lines changed: 103 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import ast.tpd
66
import core.Constants.Constant
77
import core.Contexts._
88
import core.Denotations.SingleDenotation
9+
import core.Flags
10+
import core.NameOps.isUnapplyName
11+
import core.Names._
12+
import core.Types._
913
import util.Spans.Span
10-
import core.Types.{ErrorType, MethodType, PolyType}
1114
import reporting._
1215

13-
import dotty.tools.dotc.core.Types.Type
1416

1517
object Signatures {
1618

@@ -35,8 +37,7 @@ object Signatures {
3537
* @param isImplicit Is this parameter implicit?
3638
*/
3739
case class Param(name: String, tpe: String, doc: Option[String] = None, isImplicit: Boolean = false) {
38-
def show: String =
39-
s"$name: $tpe"
40+
def show: String = if name.nonEmpty then s"$name: $tpe" else tpe
4041
}
4142

4243
/**
@@ -48,21 +49,21 @@ object Signatures {
4849
* being called, the list of overloads of this function).
4950
*/
5051
def callInfo(path: List[tpd.Tree], span: Span)(using Context): (Int, Int, List[SingleDenotation]) =
51-
val enclosingApply = path.find {
52-
case Apply(fun, _) => !fun.span.contains(span)
53-
case UnApply(fun, _, _) => !fun.span.contains(span)
54-
case _ => false
55-
}
52+
val enclosingApply = path.dropWhile {
53+
case apply @ Apply(fun, _) => fun.span.contains(span) || apply.span.end == span.end
54+
case unapply @ UnApply(fun, _, _) => fun.span.contains(span) || unapply.span.end == span.end || isTuple(unapply)
55+
case _ => true
56+
}.headOption
5657

5758
enclosingApply.map {
58-
case UnApply(fun, _, patterns) => callInfo(span, patterns, fun, Signatures.countParams(fun))
59+
case UnApply(fun, _, patterns) => unapplyCallInfo(span, fun, patterns)
5960
case Apply(fun, params) => callInfo(span, params, fun, Signatures.countParams(fun))
6061
}.getOrElse((0, 0, Nil))
6162

6263
def callInfo(
6364
span: Span,
64-
params: List[Tree[Type]],
65-
fun: Tree[Type],
65+
params: List[tpd.Tree],
66+
fun: tpd.Tree,
6667
alreadyAppliedCount : Int
6768
)(using Context): (Int, Int, List[SingleDenotation]) =
6869
val paramIndex = params.indexWhere(_.span.contains(span)) match {
@@ -84,10 +85,67 @@ object Signatures {
8485

8586
(paramIndex, alternativeIndex, alternatives)
8687

88+
private def unapplyCallInfo(
89+
span: Span,
90+
fun: tpd.Tree,
91+
patterns: List[tpd.Tree]
92+
)(using Context): (Int, Int, List[SingleDenotation]) =
93+
val patternPosition = patterns.indexWhere(_.span.contains(span))
94+
val activeParameter = extractParamTypess(fun.tpe.finalResultType.widen).headOption.map { params =>
95+
(patternPosition, patterns.length) match
96+
case (-1, 0) => 0 // there are no patterns yet so it must be first one
97+
case (-1, pos) => -1 // there are patterns, we must be outside range so we set no active parameter
98+
case _ => (params.size - 1) min patternPosition max 0 // handle unapplySeq to always highlight Seq[A] on elements
99+
}.getOrElse(-1)
100+
101+
val appliedDenot = fun.symbol.asSingleDenotation.mapInfo(_ => fun.tpe) :: Nil
102+
(activeParameter, 0, appliedDenot)
103+
104+
private def isTuple(tree: tpd.Tree)(using Context): Boolean =
105+
ctx.definitions.isTupleClass(tree.symbol.owner.companionClass)
106+
107+
private def extractParamTypess(resultType: Type)(using Context): List[List[Type]] =
108+
resultType match {
109+
// Reference to a type which is not a type class
110+
case ref: TypeRef if !ref.symbol.isPrimitiveValueClass =>
111+
getExtractorMembers(ref)
112+
// Option or Some applied type. There is special syntax for multiple returned arguments:
113+
// Option[TupleN] and Option[Seq],
114+
// We are not intrested in them, instead we extract proper type parameters from the Option type parameter.
115+
case AppliedType(TypeRef(_, cls), (appliedType @ AppliedType(tycon, args)) :: Nil)
116+
if (cls == ctx.definitions.OptionClass || cls == ctx.definitions.SomeClass) =>
117+
tycon match
118+
case TypeRef(_, cls) if cls == ctx.definitions.SeqClass => List(List(appliedType))
119+
case _ => List(args)
120+
// Applied type extractor. We must extract from applied type to retain type parameters
121+
case appliedType: AppliedType => getExtractorMembers(appliedType)
122+
// This is necessary to extract proper result type as unapply can return other methods eg. apply
123+
case MethodTpe(_, _, resultType) =>
124+
extractParamTypess(resultType.widenDealias)
125+
case _ =>
126+
Nil
127+
}
128+
129+
// Returns extractors from given type. In case if there are no extractor methods it fallbacks to get method
130+
private def getExtractorMembers(resultType: Type)(using Context): List[List[Type]] =
131+
val productAccessors = resultType.memberDenots(
132+
underscoreMembersFilter,
133+
(name, buf) => buf += resultType.member(name).asSingleDenotation
134+
)
135+
val availableExtractors = if productAccessors.isEmpty then
136+
List(resultType.member(core.Names.termName("get")))
137+
else
138+
productAccessors
139+
List(availableExtractors.map(_.info.finalResultType.stripAnnots).toList)
140+
141+
object underscoreMembersFilter extends NameFilter {
142+
def apply(pre: Type, name: Name)(using Context): Boolean = name.startsWith("_")
143+
def isStable = true
144+
}
145+
87146
def toSignature(denot: SingleDenotation)(using Context): Option[Signature] = {
88147
val symbol = denot.symbol
89148
val docComment = ParsedComment.docOf(symbol)
90-
val classTree = symbol.topLevelClass.asClass.rootTree
91149

92150
def toParamss(tp: MethodType)(using Context): List[List[Param]] = {
93151
val rest = tp.resType match {
@@ -104,7 +162,8 @@ object Signatures {
104162
Nil
105163
}
106164
val params = tp.paramNames.zip(tp.paramInfos).map { case (name, info) =>
107-
Signatures.Param(name.show,
165+
Signatures.Param(
166+
name.show,
108167
info.widenTermRefExpr.show,
109168
docComment.flatMap(_.paramDoc(name)),
110169
isImplicit = tp.isImplicitMethod)
@@ -113,7 +172,35 @@ object Signatures {
113172
params :: rest
114173
}
115174

175+
def extractParamNamess(resultType: Type): List[List[Name]] =
176+
if resultType.typeSymbol.flags.is(Flags.CaseClass) && symbol.flags.is(Flags.Synthetic) then
177+
resultType.typeSymbol.primaryConstructor.paramInfo.paramNamess
178+
else
179+
Nil
180+
181+
def toUnapplyParamss(method: Type)(using Context): List[Param] = {
182+
val resultType = method.finalResultType.widenDealias match
183+
case methodType: MethodType => methodType.resultType.widen
184+
case other => other
185+
186+
val paramNames = extractParamNamess(resultType).flatten
187+
val paramTypes = extractParamTypess(resultType).flatten
188+
189+
if paramNames.length == paramTypes.length then
190+
(paramNames zip paramTypes).map((name, info) => Param(name.show, info.show))
191+
else
192+
paramTypes.map(info => Param("", info.show))
193+
194+
}
195+
116196
denot.info.stripPoly match {
197+
case tpe if denot.name.isUnapplyName =>
198+
val params = toUnapplyParamss(tpe)
199+
if params.nonEmpty then
200+
Some(Signature("", Nil, List(params), None))
201+
else
202+
None
203+
117204
case tpe: MethodType =>
118205
val paramss = toParamss(tpe)
119206
val typeParams = denot.info match {
@@ -174,7 +261,7 @@ object Signatures {
174261
err.msg match
175262
case msg: AmbiguousOverload => msg.alternatives
176263
case msg: NoMatchingOverload => msg.alternatives
177-
case _ => Nil
264+
case _ => Nil
178265

179266
// If the user writes `foo(bar, <cursor>)`, the typer will insert a synthetic
180267
// `null` parameter: `foo(bar, null)`. This may influence what's the "best"
@@ -191,8 +278,7 @@ object Signatures {
191278
alt.info.stripPoly match {
192279
case tpe: MethodType =>
193280
userParamsTypes.zip(tpe.paramInfos).takeWhile{ case (t0, t1) => t0 <:< t1 }.size
194-
case _ =>
195-
0
281+
case _ => 0
196282
}
197283
}
198284
val bestAlternative =

0 commit comments

Comments
 (0)