From b1243e517209e178aa91858903ef94d4e2d1780a Mon Sep 17 00:00:00 2001 From: rochala Date: Sat, 21 May 2022 15:41:56 +0200 Subject: [PATCH 1/9] first part of fixes into apply signatures --- .../dotty/tools/dotc/util/Signatures.scala | 68 +++++++++++++------ .../languageserver/SignatureHelpTest.scala | 49 ++++++++++++- 2 files changed, 94 insertions(+), 23 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/util/Signatures.scala b/compiler/src/dotty/tools/dotc/util/Signatures.scala index 8003eb5e365b..1994273b1c5d 100644 --- a/compiler/src/dotty/tools/dotc/util/Signatures.scala +++ b/compiler/src/dotty/tools/dotc/util/Signatures.scala @@ -11,6 +11,7 @@ import core.NameOps.isUnapplyName import core.Names._ import core.Types._ import util.Spans.Span +import core.Symbols.NoSymbol import reporting._ @@ -49,31 +50,43 @@ 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.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 { + findEnclosingApply(path, span) match + case tpd.EmptyTree => (0, 0, Nil) case UnApply(fun, _, patterns) => unapplyCallInfo(span, fun, patterns) - case Apply(fun, params) => callInfo(span, params, fun, Signatures.countParams(fun)) - }.getOrElse((0, 0, Nil)) + case Apply(fun, params) => callInfo(span, params, fun) + + /** + * Finds enclosing application from given `path` to `span`. + * + * @param path The path to the function application + * @param span The position of the cursor + * @return Tree which encloses closest application containing span. + * In case if cursor is pointing on closing parenthesis and + * next subsequent application exists, it returns the latter + */ + private def findEnclosingApply(path: List[tpd.Tree], span: Span)(using Context): tpd.Tree = + path.filterNot { + case apply @ Apply(fun, _) => fun.span.contains(span) || isTuple(apply) + case unapply @ UnApply(fun, _, _) => fun.span.contains(span) || isTuple(unapply) + case _ => true + } match { + case Nil => tpd.EmptyTree + case direct :: enclosing :: _ if direct.source(span.end -1) == ')' => enclosing + case direct :: _ => direct + } def callInfo( span: Span, params: List[tpd.Tree], - fun: tpd.Tree, - alreadyAppliedCount : Int + fun: tpd.Tree )(using Context): (Int, Int, List[SingleDenotation]) = - val paramIndex = params.indexWhere(_.span.contains(span)) match { - case -1 => (params.length - 1 max 0) + alreadyAppliedCount - case n => n + alreadyAppliedCount - } - val (alternativeIndex, alternatives) = fun.tpe match { case err: ErrorType => - val (alternativeIndex, alternatives) = alternativesFromError(err, params) + val (alternativeIndex, alternatives) = alternativesFromError(err, params) match { + case (_, Nil) => (0, fun.denot.alternatives) + case other => other + } + (alternativeIndex, alternatives) case _ => @@ -83,6 +96,12 @@ object Signatures { (alternativeIndex, alternatives) } + val curriedArguments = countParams(fun, alternatives(alternativeIndex)) + val paramIndex = params.indexWhere(_.span.contains(span)) match { + case -1 => (params.length - 1 max 0) + curriedArguments + case n => n + curriedArguments + } + (paramIndex, alternativeIndex, alternatives) private def unapplyCallInfo( @@ -102,7 +121,7 @@ object Signatures { (activeParameter, 0, appliedDenot) private def isTuple(tree: tpd.Tree)(using Context): Boolean = - ctx.definitions.isTupleClass(tree.symbol.owner.companionClass) + tree.symbol != NoSymbol && ctx.definitions.isTupleClass(tree.symbol.owner.companionClass) private def extractParamTypess(resultType: Type)(using Context): List[List[Type]] = resultType match { @@ -229,17 +248,22 @@ object Signatures { } /** - * The number of parameters that are applied in `tree`. + * The number of parameters before `tree` application. It is necessary to properly show + * parameter number for erroneous applications before current one. * * This handles currying, so for an application such as `foo(1, 2)(3)`, the result of - * `countParams` should be 3. + * `countParams` should be 3. It also takes into considerations unapplied arguments so for `foo(1)(3)` + * we will still get 3, as first application `foo(1)` takes 2 parameters with currently only 1 applied. * * @param tree The tree to inspect. + * @param denot Denotation of function we are trying to apply + * @param alreadyCurried Number of subsequent Apply trees before current tree * @return The number of parameters that are passed. */ - private def countParams(tree: tpd.Tree): Int = + private def countParams(tree: tpd.Tree, denot: SingleDenotation, alreadyCurried: Int = 0)(using Context): Int = tree match { - case Apply(fun, params) => countParams(fun) + params.length + case Apply(fun, params) => + countParams(fun, denot, alreadyCurried + 1) + denot.symbol.paramSymss(alreadyCurried).length case _ => 0 } diff --git a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala index a6c6eeb67e06..a332ac60228b 100644 --- a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala +++ b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala @@ -17,6 +17,53 @@ class SignatureHelpTest { .signatureHelp(m1, List(signature), Some(0), 0) } + @Test def properFunctionReturnWithoutParenthesis: Unit = { + val listSignature = + S("apply[A]", Nil, List(List(P("elems", "A*"))), Some("CC[A]")) + val optionSignature = + S("apply[A]", Nil, List(List(P("x", "A"))), Some("Option[A]")) + code"""object O { + | List(1, 2$m1 + |} + |object T { + | List(Option(1$m2 + |} + |object Z { + | List(Option(1)$m3 + |}""" + .signatureHelp(m1, List(listSignature), Some(0), 0) + .signatureHelp(m2, List(optionSignature), Some(0), 0) + .signatureHelp(m3, List(listSignature), Some(0), 0) + } + + @Test def partialyFailedCurriedFunctions: Unit = { + val listSignature = + S("curry", Nil, List(List(P("a", "Int"), P("b", "Int")), List(P("c", "Int"))), Some("Int")) + code"""object O { + |def curry(a: Int, b: Int)(c: Int) = a + | curry(1$m1)(3$m2) + |}""" + .signatureHelp(m1, List(listSignature), Some(0), 0) + .signatureHelp(m2, List(listSignature), Some(0), 2) + } + + @Test def optionProperSignature: Unit = { + val listSignature = + S("apply[A]", Nil, List(List(P("x", "A"))), Some("Option[A]")) + code"""object O { + | Option(1, 2, 3, $m1) + |}""" + .signatureHelp(m1, List(listSignature), Some(0), 0) + } + + @Test def noSignaturesForTuple: Unit = { + code"""object O { + | (1, $m1, $m2) + |}""" + .signatureHelp(m1, Nil, Some(0), 0) + .signatureHelp(m2, Nil, Some(0), 0) + } + @Test def fromScala2: Unit = { val applySig = // TODO: Ideally this should say `List[A]`, not `CC[A]` @@ -115,7 +162,7 @@ class SignatureHelpTest { .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) + .signatureHelp(m4, List(signature), Some(0), -1) } @Test def unapplyClass: Unit = { From c9d2331fdac7a62d8b7b6902e9ba4bcf89b717e7 Mon Sep 17 00:00:00 2001 From: rochala Date: Mon, 23 May 2022 16:03:05 +0200 Subject: [PATCH 2/9] signature change required for showing apply when unapply is called --- .../dotty/tools/dotc/util/Signatures.scala | 69 +++++++++------- .../languageserver/DottyLanguageServer.scala | 13 +-- .../languageserver/SignatureHelpTest.scala | 81 +++++++++++++++++-- 3 files changed, 121 insertions(+), 42 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/util/Signatures.scala b/compiler/src/dotty/tools/dotc/util/Signatures.scala index 1994273b1c5d..fa2080823ff5 100644 --- a/compiler/src/dotty/tools/dotc/util/Signatures.scala +++ b/compiler/src/dotty/tools/dotc/util/Signatures.scala @@ -10,8 +10,9 @@ import core.Flags import core.NameOps.isUnapplyName import core.Names._ import core.Types._ -import util.Spans.Span import core.Symbols.NoSymbol +import interactive.Interactive +import util.Spans.Span import reporting._ @@ -41,6 +42,11 @@ object Signatures { def show: String = if name.nonEmpty then s"$name: $tpe" else tpe } + def signatureHelp(pos: SourcePosition)(using Context): (Int, Int, List[Signature]) = { + val path = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span) + computeSignatureHelp(path, pos.span)(using Interactive.contextOfPath(path)) + } + /** * Extract (current parameter index, function index, functions) out of a method call. * @@ -49,11 +55,11 @@ object Signatures { * @return A triple containing the index of the parameter being edited, the index of the function * being called, the list of overloads of this function). */ - def callInfo(path: List[tpd.Tree], span: Span)(using Context): (Int, Int, List[SingleDenotation]) = + def computeSignatureHelp(path: List[tpd.Tree], span: Span)(using Context): (Int, Int, List[Signature]) = findEnclosingApply(path, span) match case tpd.EmptyTree => (0, 0, Nil) + case Apply(fun, params) => applyCallInfo(span, params, fun) case UnApply(fun, _, patterns) => unapplyCallInfo(span, fun, patterns) - case Apply(fun, params) => callInfo(span, params, fun) /** * Finds enclosing application from given `path` to `span`. @@ -75,11 +81,11 @@ object Signatures { case direct :: _ => direct } - def callInfo( + private def applyCallInfo( span: Span, params: List[tpd.Tree], fun: tpd.Tree - )(using Context): (Int, Int, List[SingleDenotation]) = + )(using Context): (Int, Int, List[Signature]) = val (alternativeIndex, alternatives) = fun.tpe match { case err: ErrorType => val (alternativeIndex, alternatives) = alternativesFromError(err, params) match { @@ -101,14 +107,14 @@ object Signatures { case -1 => (params.length - 1 max 0) + curriedArguments case n => n + curriedArguments } - - (paramIndex, alternativeIndex, alternatives) + val alternativeSignatures = alternatives.flatMap(toSignature(_, false)) + (paramIndex, alternativeIndex, alternativeSignatures) private def unapplyCallInfo( span: Span, fun: tpd.Tree, patterns: List[tpd.Tree] - )(using Context): (Int, Int, List[SingleDenotation]) = + )(using Context): (Int, Int, List[Signature]) = val patternPosition = patterns.indexWhere(_.span.contains(span)) val activeParameter = extractParamTypess(fun.tpe.finalResultType.widen).headOption.map { params => (patternPosition, patterns.length) match @@ -117,44 +123,45 @@ object Signatures { 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) + // Handle undefined type parameters + val appliedDenot = fun match + case TypeApply(_, Bind(_, _) :: _) => fun.symbol.asSingleDenotation + case _ => fun.symbol.asSingleDenotation.mapInfo(_ => fun.tpe) + + val signature = toSignature(appliedDenot, true).toList + (activeParameter, 0, signature) private def isTuple(tree: tpd.Tree)(using Context): Boolean = tree.symbol != NoSymbol && ctx.definitions.isTupleClass(tree.symbol.owner.companionClass) - private def extractParamTypess(resultType: Type)(using Context): List[List[Type]] = + private def extractParamTypess(resultType: Type, isUnapplySeq: Boolean = false)(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 ref: TypeRef if !ref.symbol.isPrimitiveValueClass => getExtractorMembers(ref) + // Option or Some applied type. There is special syntax for multiple returned arguments: Option[TupleN] + // We are not intrested in it, 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) + case typeRef: TypeRef if ctx.definitions.isTupleClass(typeRef.symbol) => List(args.map(_.stripAnnots)) + case _ => List(List(appliedType.stripAnnots)) // Applied type extractor. We must extract from applied type to retain type parameters - case appliedType: AppliedType => getExtractorMembers(appliedType) + case appliedType: AppliedType => getExtractorMembers(appliedType, isUnapplySeq) // This is necessary to extract proper result type as unapply can return other methods eg. apply - case MethodTpe(_, _, resultType) => - extractParamTypess(resultType.widenDealias) - case _ => - Nil + case MethodTpe(_, _, resultType) => extractParamTypess(resultType.widenDealias.stripAnnots, isUnapplySeq) + 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]] = + private def getExtractorMembers(resultType: Type, isUnapplySeq: Boolean = false)(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 + val availableExtractors = (productAccessors.isEmpty, isUnapplySeq) match + case (_, true) => List(resultType.member(core.Names.termName("drop"))) + case (true, false) => List(resultType.member(core.Names.termName("get"))) + case _ => productAccessors List(availableExtractors.map(_.info.finalResultType.stripAnnots).toList) object underscoreMembersFilter extends NameFilter { @@ -162,7 +169,7 @@ object Signatures { def isStable = true } - def toSignature(denot: SingleDenotation)(using Context): Option[Signature] = { + def toSignature(denot: SingleDenotation, isUnapply: Boolean = false)(using Context): Option[Signature] = { val symbol = denot.symbol val docComment = ParsedComment.docOf(symbol) @@ -203,7 +210,7 @@ object Signatures { case other => other val paramNames = extractParamNamess(resultType).flatten - val paramTypes = extractParamTypess(resultType).flatten + val paramTypes = extractParamTypess(resultType, denot.name == core.Names.termName("unapplySeq")).flatten if paramNames.length == paramTypes.length then (paramNames zip paramTypes).map((name, info) => Param(name.show, info.show)) @@ -213,7 +220,7 @@ object Signatures { } denot.info.stripPoly match { - case tpe if denot.name.isUnapplyName => + case tpe if isUnapply => val params = toUnapplyParamss(tpe) if params.nonEmpty then Some(Signature("", Nil, List(params), None)) diff --git a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala index 179b8002dfb5..001defaa2319 100644 --- a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala +++ b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala @@ -549,18 +549,19 @@ class DottyLanguageServer extends LanguageServer } override def signatureHelp(params: TextDocumentPositionParams) = computeAsync { canceltoken => - val uri = new URI(params.getTextDocument.getUri) val driver = driverFor(uri) implicit def ctx: Context = driver.currentCtx val pos = sourcePosition(driver, uri, params.getPosition) - val trees = driver.openedTrees(uri) - val path = Interactive.pathTo(trees, pos) - val (paramN, callableN, alternatives) = Signatures.callInfo(path, pos.span) - val signatureInfos = alternatives.flatMap(Signatures.toSignature) + driver.compilationUnits.get(uri) match { + case Some(unit) => + val freshCtx = ctx.fresh.setCompilationUnit(unit) + val (paramN, callableN, signatures) = Signatures.signatureHelp(pos)(using freshCtx) + new SignatureHelp(signatures.map(signatureToSignatureInformation).asJava, callableN, paramN) + case None => new SignatureHelp(Nil.asJava, 0, 0) + } - new SignatureHelp(signatureInfos.map(signatureToSignatureInformation).asJava, callableN, paramN) } override def getTextDocumentService: TextDocumentService = this diff --git a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala index a332ac60228b..e42400c8ea4c 100644 --- a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala +++ b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala @@ -19,9 +19,9 @@ class SignatureHelpTest { @Test def properFunctionReturnWithoutParenthesis: Unit = { val listSignature = - S("apply[A]", Nil, List(List(P("elems", "A*"))), Some("CC[A]")) + S("apply", List("A"), List(List(P("elems", "A*"))), Some("CC[A]")) val optionSignature = - S("apply[A]", Nil, List(List(P("x", "A"))), Some("Option[A]")) + S("apply", List("A"), List(List(P("x", "A"))), Some("Option[A]")) code"""object O { | List(1, 2$m1 |} @@ -49,7 +49,7 @@ class SignatureHelpTest { @Test def optionProperSignature: Unit = { val listSignature = - S("apply[A]", Nil, List(List(P("x", "A"))), Some("Option[A]")) + S("apply", List("A"), List(List(P("x", "A"))), Some("Option[A]")) code"""object O { | Option(1, 2, 3, $m1) |}""" @@ -67,7 +67,7 @@ class SignatureHelpTest { @Test def fromScala2: Unit = { val applySig = // TODO: Ideally this should say `List[A]`, not `CC[A]` - S("apply[A]", Nil, List(List(P("elems", "A*"))), Some("CC[A]")) + S("apply", List("A"), List(List(P("elems", "A*"))), Some("CC[A]")) val mapSig = S("map[B]", Nil, List(List(P("f", "A => B"))), Some("List[B]")) code"""object O { @@ -201,6 +201,77 @@ class SignatureHelpTest { .signatureHelp(m2, List(signature), Some(0), 1) } + @Test def noUnapplySignatureWhenApplyingUnapply: Unit = { + val signature = S("unapply", List("A"), List(List(P("a", "A"))), Some("Some[(A, A)]")) + + code""" + |object And { + | def unapply[A](a: A): Some[(A, A)] = Some((a, a)) + |} + |object a { + | And.unapply($m1) + |} + """ + .signatureHelp(m1, List(signature), Some(0), 0) + } + + @Test def nestedOptionReturnedInUnapply: Unit = { + val signature = S("", Nil, List(List(P("", "Option[Int]"))), None) + + code"""object OpenBrowserCommand { + | def unapply(command: String): Option[Option[Int]] = { + | Some(Some(1)) + | } + | + | "" match { + | case OpenBrowserCommand($m1) => + | } + |} + """ + .signatureHelp(m1, List(signature), Some(0), 0) + } + + @Test def unknownTypeUnapply: Unit = { + val signature = S("", Nil, List(List(P("a", "A"), P("b", "B"))), None) + val signature2 = S("", Nil, List(List(P("a", "Int"), P("b", "Any"))), None) + + code"""case class Two[A, B](a: A, b: B) + |object Main { + | (null: Any) match { + | case Two(1, $m1) => + | case Option(5) => + | } + | Two(1, null: Any) match { + | case Two(x, $m2) + | } + | + |} + """ + .signatureHelp(m1, List(signature), Some(0), 1) + .signatureHelp(m2, List(signature2), Some(0), 1) + } + + @Test def sequenceMatchUnapply: Unit = { + val signatureSeq = S("", Nil, List(List(P("", "Seq[Int]"))), None) + // FIXME `Any` should be `Int` + val signatureList = S("", Nil, List(List(P("", "Seq[Any]"))), None) + val signatureVariadicExtractor = S("", Nil, List(List(P("", "Int"), P("","List[Int @uncheckedVariance]"))), None) + + code"""case class Two[A, B](a: A, b: B) + |object Main { + | Seq(1,2,3) match { + | case Seq($m2) => + | case List($m3) => + | case h$m4 :: t$m5 => + | } + |} + """ + .signatureHelp(m2, List(signatureSeq), Some(0), 0) + .signatureHelp(m3, List(signatureList), Some(0), 0) + .signatureHelp(m4, List(signatureVariadicExtractor), Some(0), 0) + .signatureHelp(m5, List(signatureVariadicExtractor), Some(0), 1) + } + @Test def productTypeClassMatch: Unit = { val signature = S("", Nil, List(List(P("", "String"), P("", "String"))), None) @@ -254,7 +325,7 @@ class SignatureHelpTest { .signatureHelp(m1, List(signature), Some(0), 0) } - @Test def sequenceMatch: Unit = { + @Test def customSequenceMatch: Unit = { val signature = S("", Nil, List(List(P("", "Seq[Char]"))), None) code"""object CharList: From 8d49bf538559a34ee201aba1c1b59d5226c2bf8a Mon Sep 17 00:00:00 2001 From: rochala Date: Tue, 24 May 2022 10:11:40 +0200 Subject: [PATCH 3/9] refactor after signature change to remove previous hacks --- .../dotty/tools/dotc/util/Signatures.scala | 281 +++++++++++------- .../languageserver/SignatureHelpTest.scala | 50 +++- 2 files changed, 225 insertions(+), 106 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/util/Signatures.scala b/compiler/src/dotty/tools/dotc/util/Signatures.scala index fa2080823ff5..e0be7f88eaa2 100644 --- a/compiler/src/dotty/tools/dotc/util/Signatures.scala +++ b/compiler/src/dotty/tools/dotc/util/Signatures.scala @@ -42,13 +42,20 @@ object Signatures { def show: String = if name.nonEmpty then s"$name: $tpe" else tpe } + /** + * Extract (current parameter index, function index, functions) method call for given position. + * + * @param pos Position for which call should be returned + * @return A triple containing the index of the parameter being edited, the index of functeon + * being called, the list of overloads of this function). + */ def signatureHelp(pos: SourcePosition)(using Context): (Int, Int, List[Signature]) = { val path = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span) computeSignatureHelp(path, pos.span)(using Interactive.contextOfPath(path)) } /** - * Extract (current parameter index, function index, functions) out of a method call. + * Computes call info (current parameter index, function index, functions) for a method call. * * @param path The path to the function application * @param span The position of the cursor @@ -62,7 +69,7 @@ object Signatures { case UnApply(fun, _, patterns) => unapplyCallInfo(span, fun, patterns) /** - * Finds enclosing application from given `path` to `span`. + * Finds enclosing application from given `path` for `span`. * * @param path The path to the function application * @param span The position of the cursor @@ -81,95 +88,186 @@ object Signatures { case direct :: _ => direct } + /** + * Extracts call information for a function application. + * + * @param span The position of the cursor + * @param params Current function parameters + * @param fun Function tree which is being applied + * + * @return A triple containing the index of the parameter being edited, the index of the function + * being called, the list of overloads of this function). + */ private def applyCallInfo( span: Span, params: List[tpd.Tree], fun: tpd.Tree )(using Context): (Int, Int, List[Signature]) = - val (alternativeIndex, alternatives) = fun.tpe match { + val (alternativeIndex, alternatives) = fun.tpe match case err: ErrorType => - val (alternativeIndex, alternatives) = alternativesFromError(err, params) match { + val (alternativeIndex, alternatives) = alternativesFromError(err, params) match case (_, Nil) => (0, fun.denot.alternatives) case other => other - } - (alternativeIndex, alternatives) - case _ => val funSymbol = fun.symbol val alternatives = funSymbol.owner.info.member(funSymbol.name).alternatives val alternativeIndex = alternatives.map(_.symbol).indexOf(funSymbol) max 0 (alternativeIndex, alternatives) - } val curriedArguments = countParams(fun, alternatives(alternativeIndex)) val paramIndex = params.indexWhere(_.span.contains(span)) match { case -1 => (params.length - 1 max 0) + curriedArguments case n => n + curriedArguments } - val alternativeSignatures = alternatives.flatMap(toSignature(_, false)) + val alternativeSignatures = alternatives.flatMap(toApplySignature) (paramIndex, alternativeIndex, alternativeSignatures) + /** + * Extracts call informatioin for function in unapply context. + * + * @param span The position of the cursor + * @param params Current function parameters + * @param fun Unapply function tree + * + * @return A triple containing the index of the parameter being edited, the index of the function + * being called, the list of overloads of this function). + */ private def unapplyCallInfo( span: Span, fun: tpd.Tree, patterns: List[tpd.Tree] )(using Context): (Int, Int, List[Signature]) = - 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 resultType = unapplyMethodResult(fun) + val isUnapplySeq = fun.denot.name == core.Names.termName("unapplySeq") + val denot = fun.denot + + def extractParamTypess(resultType: Type): List[List[Type]] = + resultType match + // Reference to a type which is not a type class + case ref: TypeRef if !ref.symbol.isPrimitiveValueClass => mapOptionLessUnapply(ref, patterns.size, isUnapplySeq) + // Option or Some applied type. There is special syntax for multiple returned arguments: Option[TupleN] + // We are not intrested in it, 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: TypeRef if ctx.definitions.isTupleClass(typeRef.symbol) => List(args) + case _ => List(List(appliedType)) + // Applied type extractor. We must extract from applied type to retain type parameters + case appliedType: AppliedType => mapOptionLessUnapply(appliedType, patterns.size, isUnapplySeq) + // This is necessary to extract proper result type as unapply can return other methods eg. apply + case MethodTpe(_, _, resultType) => extractParamTypess(resultType.widenDealias) + case _ => Nil + + def extractParamNamess(resultType: Type): List[List[Name]] = + if resultType.typeSymbol.flags.is(Flags.CaseClass) && denot.symbol.flags.is(Flags.Synthetic) then + resultType.typeSymbol.primaryConstructor.paramInfo.paramNamess + else + Nil + + val paramTypes = extractParamTypess(resultType).flatten.map(stripAllAnnots) + val paramNames = extractParamNamess(resultType).flatten + + val activeParameter = unapplyParameterIndex(patterns, span, paramTypes.length) + val unapplySignature = toUnapplySignature(paramNames, paramTypes).toList + + (activeParameter, 0, unapplySignature) - // Handle undefined type parameters - val appliedDenot = fun match - case TypeApply(_, Bind(_, _) :: _) => fun.symbol.asSingleDenotation - case _ => fun.symbol.asSingleDenotation.mapInfo(_ => fun.tpe) + /** + * Recursively strips annotations from given type + * + * @param tpe Type to strip annotations from + * @return Type with stripped annotations + */ + private def stripAllAnnots(tpe: Type)(using Context): Type = tpe match + case AppliedType(t, args) => AppliedType(stripAllAnnots(t), args.map(stripAllAnnots)) + case other => other.stripAnnots - val signature = toSignature(appliedDenot, true).toList - (activeParameter, 0, signature) + /** + * Get index of currently edited parameter in unapply context. + * + * @param patterns Currently applied patterns for unapply method + * @param span The position of the cursor + * @param maximumParams Number of parameters taken by unapply method + * @return Index of currently edited parameter + */ + private def unapplyParameterIndex(patterns: List[tpd.Tree], span: Span, maximumParams: Int)(using Context): Int = + val patternPosition = patterns.indexWhere(_.span.contains(span)) + (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 _ => (maximumParams - 1) min patternPosition max 0 // handle unapplySeq to always highlight Seq[A] on elements private def isTuple(tree: tpd.Tree)(using Context): Boolean = tree.symbol != NoSymbol && ctx.definitions.isTupleClass(tree.symbol.owner.companionClass) - private def extractParamTypess(resultType: Type, isUnapplySeq: Boolean = false)(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] - // We are not intrested in it, 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: TypeRef if ctx.definitions.isTupleClass(typeRef.symbol) => List(args.map(_.stripAnnots)) - case _ => List(List(appliedType.stripAnnots)) - // Applied type extractor. We must extract from applied type to retain type parameters - case appliedType: AppliedType => getExtractorMembers(appliedType, isUnapplySeq) - // This is necessary to extract proper result type as unapply can return other methods eg. apply - case MethodTpe(_, _, resultType) => extractParamTypess(resultType.widenDealias.stripAnnots, isUnapplySeq) - case _ => Nil - } + /** + * Get unapply method result type omiting unknown types and another method calls. + * + * @param fun Unapply tree + * @return Proper unapply method type after extracting result from method types and omiting unknown types. + */ + private def unapplyMethodResult(fun: tpd.Tree)(using Context): Type = + val typeWithoutBinds = fun match + case TypeApply(_, Bind(_, _) :: _) => fun.symbol.asSingleDenotation.info + case other => other.tpe + + typeWithoutBinds.finalResultType.widenDealias match + case methodType: MethodType => methodType.resultType.widen + case other => other - // Returns extractors from given type. In case if there are no extractor methods it fallbacks to get method - private def getExtractorMembers(resultType: Type, isUnapplySeq: Boolean = false)(using Context): List[List[Type]] = + /** + * Maps type by checking if given match is single match, name-based match, sequence match or product sequence match. + * The precedence in higher priority - higher index order for fixed-arity extractors is: + * 1. Single match + * 2. Name based match + * For variadic extractors: + * 1. Sequence match + * 2. Product sequence match + * + * @see [[https://docs.scala-lang.org/scala3/reference/changed-features/pattern-matching.html]] + * + * @param resultType Final result type for unapply + * @param patternCount Currently applied patterns to unapply function + * @param isUnapplySeq true if unapply name equals "unapplySeq", false otherwise + * @return List of List of types dependent on option less extractor type. + */ + private def mapOptionLessUnapply( + resultType: Type, + patternCount: Int, + isUnapplySeq: Boolean + )(using Context): List[List[Type]] = val productAccessors = resultType.memberDenots( underscoreMembersFilter, (name, buf) => buf += resultType.member(name).asSingleDenotation ) - val availableExtractors = (productAccessors.isEmpty, isUnapplySeq) match - case (_, true) => List(resultType.member(core.Names.termName("drop"))) - case (true, false) => List(resultType.member(core.Names.termName("get"))) + + val getMethod = resultType.member(core.Names.termName("get")) + val dropMethod = resultType.member(core.Names.termName("drop")) + + val availableExtractors = (getMethod.exists && patternCount <= 1, isUnapplySeq && dropMethod.exists) match + case (_, true) => List(dropMethod) + case (true, _) => List(getMethod) case _ => productAccessors - List(availableExtractors.map(_.info.finalResultType.stripAnnots).toList) - object underscoreMembersFilter extends NameFilter { - def apply(pre: Type, name: Name)(using Context): Boolean = name.startsWith("_") + List(availableExtractors.map(_.info.finalResultType).toList) + + /** + * Filter returning only members starting with underscore followed with number + */ + private object underscoreMembersFilter extends NameFilter { + def apply(pre: Type, name: Name)(using Context): Boolean = + name.startsWith("_") && name.toString.drop(1).toIntOption.isDefined def isStable = true } - def toSignature(denot: SingleDenotation, isUnapply: Boolean = false)(using Context): Option[Signature] = { + /** + * Creates signature for apply method. + * + * @param denot Functino denotation for which apply signature is returned. + * @return Signature if denot is a function, None otherwise + */ + private def toApplySignature(denot: SingleDenotation)(using Context): Option[Signature] = { val symbol = denot.symbol val docComment = ParsedComment.docOf(symbol) @@ -184,8 +282,7 @@ object Signatures { Nil else toParamss(res) - case _ => - Nil + case _ => Nil } val params = tp.paramNames.zip(tp.paramInfos).map { case (name, info) => Signatures.Param( @@ -194,66 +291,46 @@ object Signatures { docComment.flatMap(_.paramDoc(name)), isImplicit = tp.isImplicitMethod) } - 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, denot.name == core.Names.termName("unapplySeq")).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 isUnapply => - val params = toUnapplyParamss(tpe) - if params.nonEmpty then - Some(Signature("", Nil, List(params), None)) - else - None - + denot.info.stripPoly match case tpe: MethodType => val paramss = toParamss(tpe) - val typeParams = denot.info match { - case poly: PolyType => - poly.paramNames.zip(poly.paramInfos).map { case (x, y) => x.show + y.show } - case _ => - Nil - } - + val typeParams = denot.info match + case poly: PolyType => poly.paramNames.zip(poly.paramInfos).map { case (x, y) => x.show + y.show } + case _ => Nil val (name, returnType) = - if (symbol.isConstructor) (symbol.owner.name.show, None) - else (denot.name.show, Some(tpe.finalResultType.widenTermRefExpr.show)) - - val signature = - Signatures.Signature(name, - typeParams, - paramss, - returnType, - docComment.map(_.mainDoc)) - - Some(signature) - - case other => - None - } + if (symbol.isConstructor) then + (symbol.owner.name.show, None) + else + (denot.name.show, Some(tpe.finalResultType.widenTermRefExpr.show)) + Some(Signatures.Signature(name, typeParams, paramss, returnType, docComment.map(_.mainDoc))) + case other => None } + /** + * Creates signature for unapply method. It is different from apply one as it should not show function name, + * return type and type parameters. Instead we show function in the following pattern (_$1: T1, _$2: T2, _$n: Tn), + * where _$n is only present for synthetic product extractors such as case classes. + * In rest of cases signature skips them resulting in pattern (T1, T2, T3, Tn) + * + * @param paramNames Parameter names for unapply final result type. + * It non empty only when unapply returns synthetic product as for case classes. + * @param paramTypes Parameter types for unapply final result type. + * @return Signature if paramTypes is non empty, None otherwise + */ + private def toUnapplySignature(paramNames: List[Name], paramTypes: List[Type])(using Context): Option[Signature] = + val params = if paramNames.length == paramTypes.length then + (paramNames zip paramTypes).map((name, info) => Param(name.show, info.show)) + else + paramTypes.map(info => Param("", info.show)) + + if params.nonEmpty then + Some(Signature("", Nil, List(params), None)) + else + None + /** * The number of parameters before `tree` application. It is necessary to properly show * parameter number for erroneous applications before current one. diff --git a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala index e42400c8ea4c..6db6513605b3 100644 --- a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala +++ b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala @@ -87,7 +87,7 @@ class SignatureHelpTest { | 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) + .signatureHelp(m1, Nil, Some(0), 0) } @Test def unapplyCustomClass: Unit = { @@ -255,7 +255,7 @@ class SignatureHelpTest { val signatureSeq = S("", Nil, List(List(P("", "Seq[Int]"))), None) // FIXME `Any` should be `Int` val signatureList = S("", Nil, List(List(P("", "Seq[Any]"))), None) - val signatureVariadicExtractor = S("", Nil, List(List(P("", "Int"), P("","List[Int @uncheckedVariance]"))), None) + val signatureVariadicExtractor = S("", Nil, List(List(P("", "Int"), P("","List[Int]"))), None) code"""case class Two[A, B](a: A, b: B) |object Main { @@ -291,7 +291,8 @@ class SignatureHelpTest { } @Test def nameBasedMatch: Unit = { - val signature = S("", Nil, List(List(P("", "Int"), P("", "String"))), None) + val nameBasedMatch = S("", Nil, List(List(P("", "Int"), P("", "String"))), None) + val singleMatch = S("", Nil, List(List(P("", "ProdEmpty.type"))), None) code"""object ProdEmpty: | def _1: Int = ??? @@ -303,10 +304,51 @@ class SignatureHelpTest { |object Test: | "" match | case ProdEmpty(${m1}, ${m2}) => ??? + | case ProdEmpty(${m3}) => ??? + | case _ => () + """ + .signatureHelp(m1, List(nameBasedMatch), Some(0), 0) + .signatureHelp(m2, List(nameBasedMatch), Some(0), 1) + .signatureHelp(m3, List(singleMatch), Some(0), 0) + } + + @Test def nameBasedMatchWithWrongGet: Unit = { + val nameBasedMatch = S("", Nil, List(List(P("", "Int"))), None) + val singleMatch = S("", Nil, List(List(P("", "Int"))), None) + + code"""object ProdEmpty: + | def _1: Int = ??? + | def _2: String = ??? + | def isEmpty = true + | def unapply(s: String): this.type = this + | def get = 5 + | + |object Test: + | "" match + | case ProdEmpty(${m1}, ${m2}) => ??? + | case ProdEmpty(${m3}) => ??? + | case _ => () + """ + .signatureHelp(m1, List(nameBasedMatch), Some(0), 0) + .signatureHelp(m2, List(nameBasedMatch), Some(0), -1) + .signatureHelp(m3, List(singleMatch), Some(0), 0) + } + + @Test def nameBasedSingleMatchOrder: Unit = { + val signature = S("", Nil, List(List(P("", "String"))), None) + + code"""object ProdEmpty: + | def _1: Int = 1 + | 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) - .signatureHelp(m2, List(signature), Some(0), 1) } @Test def getObjectMatch: Unit = { From 06444bf3fe22110f3ed68248f68177e006b53ffd Mon Sep 17 00:00:00 2001 From: rochala Date: Tue, 24 May 2022 10:23:43 +0200 Subject: [PATCH 4/9] add comment to change in applySignature error recovery --- compiler/src/dotty/tools/dotc/util/Signatures.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/util/Signatures.scala b/compiler/src/dotty/tools/dotc/util/Signatures.scala index e0be7f88eaa2..1794fa019d94 100644 --- a/compiler/src/dotty/tools/dotc/util/Signatures.scala +++ b/compiler/src/dotty/tools/dotc/util/Signatures.scala @@ -106,7 +106,14 @@ object Signatures { val (alternativeIndex, alternatives) = fun.tpe match case err: ErrorType => val (alternativeIndex, alternatives) = alternativesFromError(err, params) match - case (_, Nil) => (0, fun.denot.alternatives) + // if we have no alternatives from error, we have to fallback to function denotation + // Check `partialyFailedCurriedFunctions` test for example + case (_, Nil) => + val denot = fun.denot + if denot.exists then + (0, List(denot.asSingleDenotation)) + else + (0, Nil) case other => other (alternativeIndex, alternatives) case _ => From 2ed8196466aa96beeab700e40af167be9ab02fa1 Mon Sep 17 00:00:00 2001 From: rochala Date: Tue, 24 May 2022 14:53:12 +0200 Subject: [PATCH 5/9] add deleted methods as depreceted ones, small fixes --- .../dotty/tools/dotc/util/Signatures.scala | 142 +++++++++++++----- 1 file changed, 104 insertions(+), 38 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/util/Signatures.scala b/compiler/src/dotty/tools/dotc/util/Signatures.scala index 1794fa019d94..92629df2ee1a 100644 --- a/compiler/src/dotty/tools/dotc/util/Signatures.scala +++ b/compiler/src/dotty/tools/dotc/util/Signatures.scala @@ -5,7 +5,7 @@ import ast.Trees._ import ast.tpd import core.Constants.Constant import core.Contexts._ -import core.Denotations.SingleDenotation +import core.Denotations.{SingleDenotation, Denotation} import core.Flags import core.NameOps.isUnapplyName import core.Names._ @@ -26,9 +26,16 @@ object Signatures { * @param paramss The parameter lists of this method * @param returnType The return type of this method, if this is not a constructor. * @param doc The documentation for this method. + * @param denot The function denotation */ - case class Signature(name: String, tparams: List[String], paramss: List[List[Param]], returnType: Option[String], doc: Option[String] = None) { - } + case class Signature( + name: String, + tparams: List[String], + paramss: List[List[Param]], + returnType: Option[String], + doc: Option[String] = None, + denot: Option[SingleDenotation] = None + ) /** * Represent a method's parameter. @@ -54,6 +61,23 @@ object Signatures { computeSignatureHelp(path, pos.span)(using Interactive.contextOfPath(path)) } + /** + * Extract (current parameter index, function index, functions) out of a method call. + * + * @param path The path to the function application + * @param span The position of the cursor + * @return A triple containing the index of the parameter being edited, the index of the function + * being called, the list of overloads of this function). + */ + @deprecated( + """This method is deprecated in favor of `signatureHelp`. + Returned denotation cannot be distinguished if its unapply or apply context""", + "3.1.3" + ) + def callInfo(path: List[tpd.Tree], span: Span)(using Context): (Int, Int, List[SingleDenotation]) = + val (paramN, funN, signatures) = computeSignatureHelp(path, span) + (paramN, funN, signatures.flatMap(_.denot)) + /** * Computes call info (current parameter index, function index, functions) for a method call. * @@ -147,39 +171,60 @@ object Signatures { )(using Context): (Int, Int, List[Signature]) = val resultType = unapplyMethodResult(fun) val isUnapplySeq = fun.denot.name == core.Names.termName("unapplySeq") - val denot = fun.denot - - def extractParamTypess(resultType: Type): List[List[Type]] = - resultType match - // Reference to a type which is not a type class - case ref: TypeRef if !ref.symbol.isPrimitiveValueClass => mapOptionLessUnapply(ref, patterns.size, isUnapplySeq) - // Option or Some applied type. There is special syntax for multiple returned arguments: Option[TupleN] - // We are not intrested in it, 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: TypeRef if ctx.definitions.isTupleClass(typeRef.symbol) => List(args) - case _ => List(List(appliedType)) - // Applied type extractor. We must extract from applied type to retain type parameters - case appliedType: AppliedType => mapOptionLessUnapply(appliedType, patterns.size, isUnapplySeq) - // This is necessary to extract proper result type as unapply can return other methods eg. apply - case MethodTpe(_, _, resultType) => extractParamTypess(resultType.widenDealias) - case _ => Nil - - def extractParamNamess(resultType: Type): List[List[Name]] = - if resultType.typeSymbol.flags.is(Flags.CaseClass) && denot.symbol.flags.is(Flags.Synthetic) then - resultType.typeSymbol.primaryConstructor.paramInfo.paramNamess - else - Nil + val denot = fun.denot.mapInfo(_ => resultType) - val paramTypes = extractParamTypess(resultType).flatten.map(stripAllAnnots) - val paramNames = extractParamNamess(resultType).flatten + val paramTypes = extractParamTypess(resultType, denot, patterns.size).flatten.map(stripAllAnnots) + val paramNames = extractParamNamess(resultType, denot).flatten val activeParameter = unapplyParameterIndex(patterns, span, paramTypes.length) - val unapplySignature = toUnapplySignature(paramNames, paramTypes).toList + val unapplySignature = toUnapplySignature(denot.asSingleDenotation, paramNames, paramTypes).toList (activeParameter, 0, unapplySignature) + + private def isUnapplySeq(denot: Denotation)(using Context): Boolean = + denot.name == core.Names.termName("unapplySeq") + + /** + * Extract parameter names from `resultType` only if `resultType` is case class and `denot` is synthetic. + * + * @param resultType Function result type + * @param denot Function denotation + * @return List of lists of names of parameters if `resultType` is case class without overriden unapply + */ + private def extractParamNamess(resultType: Type, denot: Denotation)(using Context): List[List[Name]] = + if resultType.typeSymbol.flags.is(Flags.CaseClass) && denot.symbol.flags.is(Flags.Synthetic) then + resultType.typeSymbol.primaryConstructor.paramInfo.paramNamess + else + Nil + + /** + * Extract parameter types from `resultType` in unapply context. + * + * @param resultType Function result type + * @param denot Function denotation + * @param patternsSize Number of pattern trees present in function tree + * @return List of lists of types present in unapply clause + */ + private def extractParamTypess( + resultType: Type, + denot: Denotation, + patternsSize: Int + )(using Context): List[List[Type]] = + resultType match + // unapply(_$1: Any): CustomClass + case ref: TypeRef if !ref.symbol.isPrimitiveValueClass => mapOptionLessUnapply(ref, patternsSize, isUnapplySeq(denot)) + // unapply(_$1: Any): Option[T[_]] + case AppliedType(TypeRef(_, cls), (appliedType @ AppliedType(tycon, args)) :: Nil) + if (cls == ctx.definitions.OptionClass || cls == ctx.definitions.SomeClass) => + tycon match + // unapply[T](_$1: Any): Option[(T1, T2 ... Tn)] + case typeRef: TypeRef if ctx.definitions.isTupleClass(typeRef.symbol) => List(args) + case _ => List(List(appliedType)) + // unapply[T](_$1: Any): CustomClass[T] + case appliedType: AppliedType => mapOptionLessUnapply(appliedType, patternsSize, isUnapplySeq(denot)) + case _ => Nil + /** * Recursively strips annotations from given type * @@ -252,10 +297,13 @@ object Signatures { val getMethod = resultType.member(core.Names.termName("get")) val dropMethod = resultType.member(core.Names.termName("drop")) - val availableExtractors = (getMethod.exists && patternCount <= 1, isUnapplySeq && dropMethod.exists) match - case (_, true) => List(dropMethod) - case (true, _) => List(getMethod) - case _ => productAccessors + val availableExtractors = + if isUnapplySeq && dropMethod.exists then + List(dropMethod) + else if getMethod.exists && patternCount <= 1 then + List(getMethod) + else + productAccessors List(availableExtractors.map(_.info.finalResultType).toList) @@ -271,7 +319,7 @@ object Signatures { /** * Creates signature for apply method. * - * @param denot Functino denotation for which apply signature is returned. + * @param denot Function denotation for which apply signature is returned. * @return Signature if denot is a function, None otherwise */ private def toApplySignature(denot: SingleDenotation)(using Context): Option[Signature] = { @@ -312,29 +360,47 @@ object Signatures { (symbol.owner.name.show, None) else (denot.name.show, Some(tpe.finalResultType.widenTermRefExpr.show)) - Some(Signatures.Signature(name, typeParams, paramss, returnType, docComment.map(_.mainDoc))) + Some(Signatures.Signature(name, typeParams, paramss, returnType, docComment.map(_.mainDoc), Some(denot))) case other => None } + @deprecated("Deprecated in favour of `signatureHelp` which now returns Signature along SingleDenotation", "3.1.3") + def toSignature(denot: SingleDenotation)(using Context): Option[Signature] = { + if denot.name.isUnapplyName then + val resultType = denot.info.stripPoly.finalResultType match + case methodType: MethodType => methodType.resultType.widen + case other => other + + // We can't get already applied patterns so we won't be able to get proper signature in case when + // it can be both name-based or single match extractor. See test `nameBasedTest` + val paramTypes = extractParamTypess(resultType, denot, 0).flatten.map(stripAllAnnots) + val paramNames = extractParamNamess(resultType, denot).flatten + + toUnapplySignature(denot.asSingleDenotation, paramNames, paramTypes) + else + toApplySignature(denot) + } + /** * Creates signature for unapply method. It is different from apply one as it should not show function name, * return type and type parameters. Instead we show function in the following pattern (_$1: T1, _$2: T2, _$n: Tn), * where _$n is only present for synthetic product extractors such as case classes. * In rest of cases signature skips them resulting in pattern (T1, T2, T3, Tn) * + * @param denot Unapply denotation * @param paramNames Parameter names for unapply final result type. * It non empty only when unapply returns synthetic product as for case classes. * @param paramTypes Parameter types for unapply final result type. * @return Signature if paramTypes is non empty, None otherwise */ - private def toUnapplySignature(paramNames: List[Name], paramTypes: List[Type])(using Context): Option[Signature] = + private def toUnapplySignature(denot: SingleDenotation, paramNames: List[Name], paramTypes: List[Type])(using Context): Option[Signature] = val params = if paramNames.length == paramTypes.length then (paramNames zip paramTypes).map((name, info) => Param(name.show, info.show)) else paramTypes.map(info => Param("", info.show)) if params.nonEmpty then - Some(Signature("", Nil, List(params), None)) + Some(Signature("", Nil, List(params), None, None, Some(denot))) else None From 3e6474a2a4317c1eb83fff68f53a4e9799f82159 Mon Sep 17 00:00:00 2001 From: rochala Date: Tue, 24 May 2022 15:23:40 +0200 Subject: [PATCH 6/9] fix unchecked List application --- .../src/dotty/tools/dotc/util/Signatures.scala | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/util/Signatures.scala b/compiler/src/dotty/tools/dotc/util/Signatures.scala index 92629df2ee1a..6bf7c500bf08 100644 --- a/compiler/src/dotty/tools/dotc/util/Signatures.scala +++ b/compiler/src/dotty/tools/dotc/util/Signatures.scala @@ -146,13 +146,16 @@ object Signatures { val alternativeIndex = alternatives.map(_.symbol).indexOf(funSymbol) max 0 (alternativeIndex, alternatives) - val curriedArguments = countParams(fun, alternatives(alternativeIndex)) - val paramIndex = params.indexWhere(_.span.contains(span)) match { - case -1 => (params.length - 1 max 0) + curriedArguments - case n => n + curriedArguments - } - val alternativeSignatures = alternatives.flatMap(toApplySignature) - (paramIndex, alternativeIndex, alternativeSignatures) + if alternativeIndex < alternatives.length then + val curriedArguments = countParams(fun, alternatives(alternativeIndex)) + val paramIndex = params.indexWhere(_.span.contains(span)) match { + case -1 => (params.length - 1 max 0) + curriedArguments + case n => n + curriedArguments + } + val alternativeSignatures = alternatives.flatMap(toApplySignature) + (paramIndex, alternativeIndex, alternativeSignatures) + else + (0, 0, Nil) /** * Extracts call informatioin for function in unapply context. From 32c52cf053b9fd85f8b2ce8c034ad60f345a554d Mon Sep 17 00:00:00 2001 From: rochala Date: Tue, 24 May 2022 16:53:18 +0200 Subject: [PATCH 7/9] remove fixme comment as this is not a bug --- .../tools/languageserver/SignatureHelpTest.scala | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala index 6db6513605b3..4adc5aca91bf 100644 --- a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala +++ b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala @@ -253,23 +253,19 @@ class SignatureHelpTest { @Test def sequenceMatchUnapply: Unit = { val signatureSeq = S("", Nil, List(List(P("", "Seq[Int]"))), None) - // FIXME `Any` should be `Int` - val signatureList = S("", Nil, List(List(P("", "Seq[Any]"))), None) val signatureVariadicExtractor = S("", Nil, List(List(P("", "Int"), P("","List[Int]"))), None) code"""case class Two[A, B](a: A, b: B) |object Main { | Seq(1,2,3) match { - | case Seq($m2) => - | case List($m3) => - | case h$m4 :: t$m5 => + | case Seq($m1) => + | case h$m2 :: t$m3 => | } |} """ - .signatureHelp(m2, List(signatureSeq), Some(0), 0) - .signatureHelp(m3, List(signatureList), Some(0), 0) - .signatureHelp(m4, List(signatureVariadicExtractor), Some(0), 0) - .signatureHelp(m5, List(signatureVariadicExtractor), Some(0), 1) + .signatureHelp(m1, List(signatureSeq), Some(0), 0) + .signatureHelp(m2, List(signatureVariadicExtractor), Some(0), 0) + .signatureHelp(m3, List(signatureVariadicExtractor), Some(0), 1) } @Test def productTypeClassMatch: Unit = { From 668614f3a52b2ef9013c55c898cd4fa08215166d Mon Sep 17 00:00:00 2001 From: rochala Date: Tue, 31 May 2022 13:00:11 +0200 Subject: [PATCH 8/9] add type parameter inference for apply methods --- .../dotty/tools/dotc/util/Signatures.scala | 13 +++++++- .../languageserver/SignatureHelpTest.scala | 30 +++++++++++-------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/util/Signatures.scala b/compiler/src/dotty/tools/dotc/util/Signatures.scala index 6bf7c500bf08..e713097e6241 100644 --- a/compiler/src/dotty/tools/dotc/util/Signatures.scala +++ b/compiler/src/dotty/tools/dotc/util/Signatures.scala @@ -127,6 +127,13 @@ object Signatures { params: List[tpd.Tree], fun: tpd.Tree )(using Context): (Int, Int, List[Signature]) = + def treeQualifier(tree: tpd.Tree): tpd.Tree = tree match + case Apply(qual, _) => treeQualifier(qual) + case TypeApply(qual, _) => treeQualifier(qual) + case AppliedTypeTree(qual, _) => treeQualifier(qual) + case Select(qual, _) => qual + case _ => tree + val (alternativeIndex, alternatives) = fun.tpe match case err: ErrorType => val (alternativeIndex, alternatives) = alternativesFromError(err, params) match @@ -152,7 +159,11 @@ object Signatures { case -1 => (params.length - 1 max 0) + curriedArguments case n => n + curriedArguments } - val alternativeSignatures = alternatives.flatMap(toApplySignature) + + val pre = treeQualifier(fun) + val alternativesWithTypes = alternatives.map(_.asSeenFrom(pre.tpe.widenTermRefExpr)) + val alternativeSignatures = alternativesWithTypes.flatMap(toApplySignature) + (paramIndex, alternativeIndex, alternativeSignatures) else (0, 0, Nil) diff --git a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala index 4adc5aca91bf..9dc1ea1eba57 100644 --- a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala +++ b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala @@ -18,10 +18,8 @@ class SignatureHelpTest { } @Test def properFunctionReturnWithoutParenthesis: Unit = { - val listSignature = - S("apply", List("A"), List(List(P("elems", "A*"))), Some("CC[A]")) - val optionSignature = - S("apply", List("A"), List(List(P("x", "A"))), Some("Option[A]")) + val listSignature = S("apply", List("A"), List(List(P("elems", "A*"))), Some("List[A]")) + val optionSignature = S("apply", List("A"), List(List(P("x", "A"))), Some("Option[A]")) code"""object O { | List(1, 2$m1 |} @@ -37,8 +35,7 @@ class SignatureHelpTest { } @Test def partialyFailedCurriedFunctions: Unit = { - val listSignature = - S("curry", Nil, List(List(P("a", "Int"), P("b", "Int")), List(P("c", "Int"))), Some("Int")) + val listSignature = S("curry", Nil, List(List(P("a", "Int"), P("b", "Int")), List(P("c", "Int"))), Some("Int")) code"""object O { |def curry(a: Int, b: Int)(c: Int) = a | curry(1$m1)(3$m2) @@ -48,8 +45,7 @@ class SignatureHelpTest { } @Test def optionProperSignature: Unit = { - val listSignature = - S("apply", List("A"), List(List(P("x", "A"))), Some("Option[A]")) + val listSignature = S("apply", List("A"), List(List(P("x", "A"))), Some("Option[A]")) code"""object O { | Option(1, 2, 3, $m1) |}""" @@ -65,11 +61,8 @@ class SignatureHelpTest { } @Test def fromScala2: Unit = { - val applySig = - // TODO: Ideally this should say `List[A]`, not `CC[A]` - S("apply", List("A"), List(List(P("elems", "A*"))), Some("CC[A]")) - val mapSig = - S("map[B]", Nil, List(List(P("f", "A => B"))), Some("List[B]")) + val applySig = S("apply", List("A"), List(List(P("elems", "A*"))), Some("List[A]")) + val mapSig = S("map[B]", Nil, List(List(P("f", "Int => B"))), Some("List[B]")) code"""object O { List($m1) List(1, 2, 3).map($m2) @@ -78,6 +71,17 @@ class SignatureHelpTest { .signatureHelp(m2, List(mapSig), Some(0), 0) } + @Test def typeParameterMethodApply: Unit = { + val testSig = S("method", Nil, List(List()), Some("Int")) + code"""case class Foo[A](test: A) { + | def method(): A = ??? + |} + |object O { + | Foo(5).method($m1) + |}""" + .signatureHelp(m1, List(testSig), Some(0), 0) + } + @Test def unapplyBooleanReturn: Unit = { code"""object Even: | def unapply(s: String): Boolean = s.size % 2 == 0 From fc26206e4853db2f0e0859097ddb433b59f24b31 Mon Sep 17 00:00:00 2001 From: rochala Date: Thu, 2 Jun 2022 15:56:40 +0200 Subject: [PATCH 9/9] change signatureHelp method signature --- compiler/src/dotty/tools/dotc/util/Signatures.scala | 9 ++++----- .../tools/languageserver/DottyLanguageServer.scala | 11 ++++------- .../tools/languageserver/SignatureHelpTest.scala | 9 +++++---- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/util/Signatures.scala b/compiler/src/dotty/tools/dotc/util/Signatures.scala index e713097e6241..30285644721c 100644 --- a/compiler/src/dotty/tools/dotc/util/Signatures.scala +++ b/compiler/src/dotty/tools/dotc/util/Signatures.scala @@ -52,14 +52,13 @@ object Signatures { /** * Extract (current parameter index, function index, functions) method call for given position. * - * @param pos Position for which call should be returned + * @param path The path to the function application + * @param span The position of the cursor * @return A triple containing the index of the parameter being edited, the index of functeon * being called, the list of overloads of this function). */ - def signatureHelp(pos: SourcePosition)(using Context): (Int, Int, List[Signature]) = { - val path = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span) - computeSignatureHelp(path, pos.span)(using Interactive.contextOfPath(path)) - } + def signatureHelp(path: List[tpd.Tree], pos: Span)(using Context): (Int, Int, List[Signature]) = + computeSignatureHelp(path, pos) /** * Extract (current parameter index, function index, functions) out of a method call. diff --git a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala index 001defaa2319..5bc1e9f7dfa5 100644 --- a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala +++ b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala @@ -554,13 +554,10 @@ class DottyLanguageServer extends LanguageServer implicit def ctx: Context = driver.currentCtx val pos = sourcePosition(driver, uri, params.getPosition) - driver.compilationUnits.get(uri) match { - case Some(unit) => - val freshCtx = ctx.fresh.setCompilationUnit(unit) - val (paramN, callableN, signatures) = Signatures.signatureHelp(pos)(using freshCtx) - new SignatureHelp(signatures.map(signatureToSignatureInformation).asJava, callableN, paramN) - case None => new SignatureHelp(Nil.asJava, 0, 0) - } + val trees = driver.openedTrees(uri) + val path = Interactive.pathTo(trees, pos) + val (paramN, callableN, signatures) = Signatures.signatureHelp(path, pos.span) + new SignatureHelp(signatures.map(signatureToSignatureInformation).asJava, callableN, paramN) } diff --git a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala index 9dc1ea1eba57..84379da3fbe3 100644 --- a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala +++ b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala @@ -38,18 +38,19 @@ class SignatureHelpTest { val listSignature = S("curry", Nil, List(List(P("a", "Int"), P("b", "Int")), List(P("c", "Int"))), Some("Int")) code"""object O { |def curry(a: Int, b: Int)(c: Int) = a - | curry(1$m1)(3$m2) + | curry(1$m1)$m2(3$m3) |}""" .signatureHelp(m1, List(listSignature), Some(0), 0) - .signatureHelp(m2, List(listSignature), Some(0), 2) + .signatureHelp(m2, List(listSignature), Some(0), 0) + .signatureHelp(m3, List(listSignature), Some(0), 2) } @Test def optionProperSignature: Unit = { - val listSignature = S("apply", List("A"), List(List(P("x", "A"))), Some("Option[A]")) + val signature = S("apply", List("A"), List(List(P("x", "A"))), Some("Option[A]")) code"""object O { | Option(1, 2, 3, $m1) |}""" - .signatureHelp(m1, List(listSignature), Some(0), 0) + .signatureHelp(m1, List(signature), Some(0), 0) } @Test def noSignaturesForTuple: Unit = {