diff --git a/compiler/src/dotty/tools/dotc/util/Signatures.scala b/compiler/src/dotty/tools/dotc/util/Signatures.scala index d24b4d8c3bf9..8003eb5e365b 100644 --- a/compiler/src/dotty/tools/dotc/util/Signatures.scala +++ b/compiler/src/dotty/tools/dotc/util/Signatures.scala @@ -6,11 +6,13 @@ import ast.tpd import core.Constants.Constant import core.Contexts._ import core.Denotations.SingleDenotation +import core.Flags +import core.NameOps.isUnapplyName +import core.Names._ +import core.Types._ import util.Spans.Span -import core.Types.{ErrorType, MethodType, PolyType} import reporting._ -import dotty.tools.dotc.core.Types.Type object Signatures { @@ -35,8 +37,7 @@ object Signatures { * @param isImplicit Is this parameter implicit? */ case class Param(name: String, tpe: String, doc: Option[String] = None, isImplicit: Boolean = false) { - def show: String = - s"$name: $tpe" + def show: String = if name.nonEmpty then s"$name: $tpe" else tpe } /** @@ -48,21 +49,21 @@ object Signatures { * being called, the list of overloads of this function). */ def callInfo(path: List[tpd.Tree], span: Span)(using Context): (Int, Int, List[SingleDenotation]) = - val enclosingApply = path.find { - case Apply(fun, _) => !fun.span.contains(span) - case UnApply(fun, _, _) => !fun.span.contains(span) - case _ => false - } + val enclosingApply = path.dropWhile { + case apply @ Apply(fun, _) => fun.span.contains(span) || apply.span.end == span.end + case unapply @ UnApply(fun, _, _) => fun.span.contains(span) || unapply.span.end == span.end || isTuple(unapply) + case _ => true + }.headOption enclosingApply.map { - case UnApply(fun, _, patterns) => callInfo(span, patterns, fun, Signatures.countParams(fun)) + case UnApply(fun, _, patterns) => unapplyCallInfo(span, fun, patterns) case Apply(fun, params) => callInfo(span, params, fun, Signatures.countParams(fun)) }.getOrElse((0, 0, Nil)) def callInfo( span: Span, - params: List[Tree[Type]], - fun: Tree[Type], + params: List[tpd.Tree], + fun: tpd.Tree, alreadyAppliedCount : Int )(using Context): (Int, Int, List[SingleDenotation]) = val paramIndex = params.indexWhere(_.span.contains(span)) match { @@ -84,10 +85,67 @@ object Signatures { (paramIndex, alternativeIndex, alternatives) + private def unapplyCallInfo( + span: Span, + fun: tpd.Tree, + patterns: List[tpd.Tree] + )(using Context): (Int, Int, List[SingleDenotation]) = + val patternPosition = patterns.indexWhere(_.span.contains(span)) + val activeParameter = extractParamTypess(fun.tpe.finalResultType.widen).headOption.map { params => + (patternPosition, patterns.length) match + case (-1, 0) => 0 // there are no patterns yet so it must be first one + case (-1, pos) => -1 // there are patterns, we must be outside range so we set no active parameter + case _ => (params.size - 1) min patternPosition max 0 // handle unapplySeq to always highlight Seq[A] on elements + }.getOrElse(-1) + + val appliedDenot = fun.symbol.asSingleDenotation.mapInfo(_ => fun.tpe) :: Nil + (activeParameter, 0, appliedDenot) + + private def isTuple(tree: tpd.Tree)(using Context): Boolean = + ctx.definitions.isTupleClass(tree.symbol.owner.companionClass) + + private def extractParamTypess(resultType: Type)(using Context): List[List[Type]] = + resultType match { + // Reference to a type which is not a type class + case ref: TypeRef if !ref.symbol.isPrimitiveValueClass => + getExtractorMembers(ref) + // Option or Some applied type. There is special syntax for multiple returned arguments: + // Option[TupleN] and Option[Seq], + // We are not intrested in them, instead we extract proper type parameters from the Option type parameter. + case AppliedType(TypeRef(_, cls), (appliedType @ AppliedType(tycon, args)) :: Nil) + if (cls == ctx.definitions.OptionClass || cls == ctx.definitions.SomeClass) => + tycon match + case TypeRef(_, cls) if cls == ctx.definitions.SeqClass => List(List(appliedType)) + case _ => List(args) + // Applied type extractor. We must extract from applied type to retain type parameters + case appliedType: AppliedType => getExtractorMembers(appliedType) + // This is necessary to extract proper result type as unapply can return other methods eg. apply + case MethodTpe(_, _, resultType) => + extractParamTypess(resultType.widenDealias) + case _ => + Nil + } + + // Returns extractors from given type. In case if there are no extractor methods it fallbacks to get method + private def getExtractorMembers(resultType: Type)(using Context): List[List[Type]] = + val productAccessors = resultType.memberDenots( + underscoreMembersFilter, + (name, buf) => buf += resultType.member(name).asSingleDenotation + ) + val availableExtractors = if productAccessors.isEmpty then + List(resultType.member(core.Names.termName("get"))) + else + productAccessors + List(availableExtractors.map(_.info.finalResultType.stripAnnots).toList) + + object underscoreMembersFilter extends NameFilter { + def apply(pre: Type, name: Name)(using Context): Boolean = name.startsWith("_") + def isStable = true + } + def toSignature(denot: SingleDenotation)(using Context): Option[Signature] = { val symbol = denot.symbol val docComment = ParsedComment.docOf(symbol) - val classTree = symbol.topLevelClass.asClass.rootTree def toParamss(tp: MethodType)(using Context): List[List[Param]] = { val rest = tp.resType match { @@ -104,7 +162,8 @@ object Signatures { Nil } val params = tp.paramNames.zip(tp.paramInfos).map { case (name, info) => - Signatures.Param(name.show, + Signatures.Param( + name.show, info.widenTermRefExpr.show, docComment.flatMap(_.paramDoc(name)), isImplicit = tp.isImplicitMethod) @@ -113,7 +172,35 @@ object Signatures { params :: rest } + def extractParamNamess(resultType: Type): List[List[Name]] = + if resultType.typeSymbol.flags.is(Flags.CaseClass) && symbol.flags.is(Flags.Synthetic) then + resultType.typeSymbol.primaryConstructor.paramInfo.paramNamess + else + Nil + + def toUnapplyParamss(method: Type)(using Context): List[Param] = { + val resultType = method.finalResultType.widenDealias match + case methodType: MethodType => methodType.resultType.widen + case other => other + + val paramNames = extractParamNamess(resultType).flatten + val paramTypes = extractParamTypess(resultType).flatten + + if paramNames.length == paramTypes.length then + (paramNames zip paramTypes).map((name, info) => Param(name.show, info.show)) + else + paramTypes.map(info => Param("", info.show)) + + } + denot.info.stripPoly match { + case tpe if denot.name.isUnapplyName => + val params = toUnapplyParamss(tpe) + if params.nonEmpty then + Some(Signature("", Nil, List(params), None)) + else + None + case tpe: MethodType => val paramss = toParamss(tpe) val typeParams = denot.info match { @@ -174,7 +261,7 @@ object Signatures { err.msg match case msg: AmbiguousOverload => msg.alternatives case msg: NoMatchingOverload => msg.alternatives - case _ => Nil + case _ => Nil // If the user writes `foo(bar, )`, the typer will insert a synthetic // `null` parameter: `foo(bar, null)`. This may influence what's the "best" @@ -191,8 +278,7 @@ object Signatures { alt.info.stripPoly match { case tpe: MethodType => userParamsTypes.zip(tpe.paramInfos).takeWhile{ case (t0, t1) => t0 <:< t1 }.size - case _ => - 0 + case _ => 0 } } val bestAlternative = diff --git a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala index 171a3ac5615d..a6c6eeb67e06 100644 --- a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala +++ b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala @@ -31,6 +31,337 @@ class SignatureHelpTest { .signatureHelp(m2, List(mapSig), Some(0), 0) } + @Test def unapplyBooleanReturn: Unit = { + code"""object Even: + | def unapply(s: String): Boolean = s.size % 2 == 0 + | + |object O: + | "even" match + | case s @ Even(${m1}) => println(s"s has an even number of characters") + | case s => println(s"s has an odd number of characters") + """ + .signatureHelp(m1, Nil, Some(0), -1) + } + + @Test def unapplyCustomClass: Unit = { + val signature = S("", Nil, List(List(P("", "Int"))), None) + + code"""class Nat(val x: Int): + | def get: Int = x + | + |object Nat: + | def unapply(x: Int): Nat = new Nat(x) + | + |object O: + | 5 match + | case Nat(${m1}) => println(s"n is a natural number") + | case _ => () + """ + .signatureHelp(m1, List(signature), Some(0), 0) + } + + @Test def unapplyTypeClass: Unit = { + val signature = S("", Nil, List(List(P("", "Int"), P("", "String"))), None) + + code"""class Two[A, B](a: A, b: B) + |object Two { + | def unapply[A, B](t: Two[A, B]): Option[(A, B)] = None + |} + | + |object Main { + | val tp = new Two(1, "") + | tp match { + | case Two(x$m1, $m2) => + | } + |}""" + .signatureHelp(m1, List(signature), Some(0), 0) + .signatureHelp(m2, List(signature), Some(0), 1) + } + + @Test def nestedUnapplySignature: Unit = { + val signatureOneTwo = S("", Nil, List(List(P("a", "One"), P("b", "Two"))), None) + val signatureOne = S("", Nil, List(List(P("c", "Int"))), None) + val signatureTwo = S("", Nil, List(List(P("d", "Int"))), None) + + code"""case class One(c: Int) + |case class Two(d: Int) + |case class OneTwo[A, B](a: A, b: B) + | + |object Main { + | val tp = new OneTwo(One(1), Two(5)) + | tp match { + | case OneTwo(x${m1}, ${m2}) => + | case OneTwo(One(x${m4})${m3}, T${m5}wo(${m6})${m8})${m7} => + | } + |}""" + .signatureHelp(m1, List(signatureOneTwo), Some(0), 0) + .signatureHelp(m2, List(signatureOneTwo), Some(0), 1) + .signatureHelp(m3, List(signatureOneTwo), Some(0), 0) + .signatureHelp(m4, List(signatureOne), Some(0), 0) + .signatureHelp(m5, List(signatureOneTwo), Some(0), 1) + .signatureHelp(m6, List(signatureTwo), Some(0), 0) + } + + @Test def properParameterIndexTest: Unit = { + val signature = S("", Nil, List(List(P("a", "Int"), P("b", "String"))), None) + code"""case class Two(a: Int, b: String) + | + |object Main { + | val tp = new Two(1, "") + | tp match { + | case Two(x${m1}, y ${m2}, d${m3})${m4} => + | } + |}""" + .signatureHelp(m1, List(signature), Some(0), 0) + .signatureHelp(m2, List(signature), Some(0), -1) + .signatureHelp(m3, List(signature), Some(0), -1) + .signatureHelp(m4, Nil, Some(0), 0) + } + + @Test def unapplyClass: Unit = { + val signature = S("", Nil, List(List(P("", "Int"), P("", "String"))), None) + + code"""class Two(a: Int, b: String) + |object Two { + | def unapply(t: Two): Option[(Int, String)] = None + |} + | + |object Main { + | val tp = new Two(1, "") + | tp match { + | case Two(x$m1, $m2) => + | } + |}""" + .signatureHelp(m1, List(signature), Some(0), 0) + .signatureHelp(m2, List(signature), Some(0), 1) + } + + @Test def productMatch: Unit = { + val signature = S("", Nil, List(List(P("", "Char"), P("", "Char"))), None) + + code"""class FirstChars(s: String) extends Product: + | def _1 = s.charAt(0) + | def _2 = s.charAt(1) + | + |object FirstChars: + | def unapply(s: String): FirstChars = new FirstChars(s) + | + |object Test: + | "Hi!" match + | case FirstChars(ch${m1}, ch${m2}) => ??? + """ + .signatureHelp(m1, List(signature), Some(0), 0) + .signatureHelp(m2, List(signature), Some(0), 1) + } + + @Test def productTypeClassMatch: Unit = { + val signature = S("", Nil, List(List(P("", "String"), P("", "String"))), None) + + code"""class FirstChars[A](s: A) extends Product: + | def _1 = s + | def _2 = s + | + |object FirstChars: + | def unapply(s: String): FirstChars[String] = new FirstChars(s) + | + |object Test: + | "Hi!" match + | case FirstChars(ch${m1}, ch${m2}) => ??? + """ + .signatureHelp(m1, List(signature), Some(0), 0) + .signatureHelp(m2, List(signature), Some(0), 1) + } + + @Test def nameBasedMatch: Unit = { + val signature = S("", Nil, List(List(P("", "Int"), P("", "String"))), None) + + code"""object ProdEmpty: + | def _1: Int = ??? + | def _2: String = ??? + | def isEmpty = true + | def unapply(s: String): this.type = this + | def get = this + | + |object Test: + | "" match + | case ProdEmpty(${m1}, ${m2}) => ??? + | case _ => () + """ + .signatureHelp(m1, List(signature), Some(0), 0) + .signatureHelp(m2, List(signature), Some(0), 1) + } + + @Test def getObjectMatch: Unit = { + val signature = S("", Nil, List(List(P("", "String"))), None) + + code"""object ProdEmpty: + | def isEmpty = true + | def unapply(s: String): this.type = this + | def get: String = "" + | + |object Test: + | "" match + | case ProdEmpty(${m1}) => ??? + | case _ => () + """ + .signatureHelp(m1, List(signature), Some(0), 0) + } + + @Test def sequenceMatch: Unit = { + val signature = S("", Nil, List(List(P("", "Seq[Char]"))), None) + + code"""object CharList: + | def unapplySeq(s: String): Option[Seq[Char]] = Some(s.toList) + | + |object Test: + | "example" match + | case CharList(c${m1}1, c${m2}2, c${m3}3, c4, _, _, ${m4}) => ??? + | case _ => + | println("Expected *exactly* 7 characters!") + """ + .signatureHelp(m1, List(signature), Some(0), 0) + .signatureHelp(m2, List(signature), Some(0), 0) + .signatureHelp(m3, List(signature), Some(0), 0) + .signatureHelp(m4, List(signature), Some(0), 0) + } + + @Test def productSequenceMatch: Unit = { + val signature = S("", Nil, List(List(P("", "String"), P("", "Seq[Int]"))), None) + + code"""class Foo(val name: String, val children: Int*) + |object Foo: + | def unapplySeq(f: Foo): Option[(String, Seq[Int])] = + | Some((f.name, f.children)) + | + |def foo(f: Foo) = f match + | case Foo(na${m1}e, n${m2} : _*) => + | case Foo(nam${m3}e, ${m4}x, ${m5}y, n${m6}s : _*) => + """ + .signatureHelp(m1, List(signature), Some(0), 0) + .signatureHelp(m2, List(signature), Some(0), 1) + .signatureHelp(m3, List(signature), Some(0), 0) + .signatureHelp(m4, List(signature), Some(0), 1) + .signatureHelp(m5, List(signature), Some(0), 1) + .signatureHelp(m6, List(signature), Some(0), 1) + } + + @Test def productSequenceMatchForCaseClass: Unit = { + val signature = S("", Nil, List(List(P("name", "String"), P("children", "Seq[Int]"))), None) + + code"""case class Foo(val name: String, val children: Int*) + | + |def foo(f: Foo) = f match + | case Foo(na${m1}e, n${m2} : _*) => + | case Foo(nam${m3}e, ${m4}x, ${m5}y, n${m6}s : _*) => + """ + .signatureHelp(m1, List(signature), Some(0), 0) + .signatureHelp(m2, List(signature), Some(0), 1) + .signatureHelp(m3, List(signature), Some(0), 0) + .signatureHelp(m4, List(signature), Some(0), 1) + .signatureHelp(m5, List(signature), Some(0), 1) + .signatureHelp(m6, List(signature), Some(0), 1) + } + + @Test def unapplyManyType: Unit = { + val signature = S("", Nil, List(List(P("", "Int"), P("", "String"))), None) + + code""" + |object Opt { + | def unapply[A, B](t: Option[(A, B)]): Option[(A, B)] = None + |} + | + |object Main { + | Option((1, "foo")) match { + | case Opt(x$m1, $m2) => + | } + |}""" + .signatureHelp(m1, List(signature), Some(0), 0) + .signatureHelp(m2, List(signature), Some(0), 1) + } + + @Test def unapplyTypeCaseClass: Unit = { + val signature = S("", Nil, List(List(P("a", "Int"), P("b", "String"))), None) + + code"""case class Two[A, B](a: A, b: B) + | + |object Main { + | val tp = new Two(1, "") + | tp match { + | case Two(x$m1, $m2) => + | } + |}""" + .signatureHelp(m1, List(signature), Some(0), 0) + .signatureHelp(m2, List(signature), Some(0), 1) + } + + @Test def noUnapplyForTuple: Unit = { + code"""object Main { + | (1, 2) match + | case (x${m1}, ${m2}) => + |}""" + .signatureHelp(m1, Nil, Some(0), 0) + .signatureHelp(m2, Nil, Some(0), 0) + } + + @Test def unapplyCaseClass: Unit = { + val signature = S("", Nil, List(List(P("a", "Int"), P("b", "String"))), None) + + code"""case class Two(a: Int, b: String) + | + |object Main { + | val tp = new Two(1, "") + | tp match { + | case Two(x$m1, $m2) => + | } + |}""" + .signatureHelp(m1, List(signature), Some(0), 0) + .signatureHelp(m2, List(signature), Some(0), 1) + } + + @Test def unapplyOption: Unit = { + val signature = S("", Nil, List(List(P("", "Int"))), None) + + code"""|object Main { + | Option(1) match { + | case Some(${m1}) => + | } + |}""" + .signatureHelp(m1, List(signature), Some(0), 0) + } + + @Test def unapplyWithImplicits: Unit = { + val signature = S("", Nil, List(List(P("", "Int"))), None) + code"""| + |object Opt: + | def unapply[A](using String)(a: Option[A])(using Int) = a + | + |object Main { + | given String = "" + | given Int = 0 + | Option(1) match { + | case Opt(${m1}) => + | } + |}""" + + .signatureHelp(m1, List(signature), Some(0), 0) + } + + @Test def unapplyWithMultipleImplicits: Unit = { + val signature = S("", Nil, List(List(P("", "Int"))), None) + code"""| + |object Opt: + | def unapply[A](using String)(using Int)(a: Option[A]) = a + | + |object Main { + | given String = "" + | given Int = 0 + | Option(1) match { + | case Opt(${m1}) => + | } + |}""" + .signatureHelp(m1, List(signature), Some(0), 0) + } + /** Implicit parameter lists consisting solely of DummyImplicits are hidden. */ @Test def hiddenDummyParams: Unit = { val foo1Sig = @@ -362,78 +693,6 @@ class SignatureHelpTest { ), None, 0) } - @Test def unapplyMethod: Unit = { - code"""|object Main { - | Option(1) match { - | case Some(${m1}) => - | } - |}""" - - .signatureHelp(m1, List( - S("unapply[A]", Nil, List(List( - P("x$0", "Some[A]", None), - )), Some("Option[A]"), None) - ), None, 0) - } - - @Test def unapplyMethodImplicits: Unit = { - code"""| - |object Opt: - | def unapply[A](using String)(a: Option[A])(using Int) = a - | - |object Main { - | given String = "" - | given Int = 0 - | Option(1) match { - | case Opt(${m1}) => - | } - |}""" - - .signatureHelp(m1, List( - S("unapply[A]", Nil, List( - List( - P("x$1", "String", None, isImplicit = true) - ), - List( - P("a", "Option[A]", None), - ), - List( - P("x$3", "Int", None, isImplicit = true) - ) - ), - Some("Option[A]"), None) - ), None, 1) - } - - @Test def unapplyMethodImplicitsMultiple: Unit = { - code"""| - |object Opt: - | def unapply[A](using String)(using Int)(a: Option[A]) = a - | - |object Main { - | given String = "" - | given Int = 0 - | Option(1) match { - | case Opt(${m1}) => - | } - |}""" - - .signatureHelp(m1, List( - S("unapply[A]", Nil, List( - List( - P("x$1", "String", None, isImplicit = true) - ), - List( - P("x$2", "Int", None, isImplicit = true) - ), - List( - P("a", "Option[A]", None), - ) - ), - Some("Option[A]"), None) - ), None, 2) - } - @Test def nestedApplySignatures: Unit = { val signatures = (1 to 5).map { i => S(s"foo$i", Nil, List(List(P("x", "Int"))), Some("Int"))