diff --git a/compiler/src/dotty/tools/dotc/util/Signatures.scala b/compiler/src/dotty/tools/dotc/util/Signatures.scala index 8003eb5e365b..30285644721c 100644 --- a/compiler/src/dotty/tools/dotc/util/Signatures.scala +++ b/compiler/src/dotty/tools/dotc/util/Signatures.scala @@ -5,11 +5,13 @@ 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._ import core.Types._ +import core.Symbols.NoSymbol +import interactive.Interactive import util.Spans.Span import reporting._ @@ -24,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. @@ -40,6 +49,17 @@ 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 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(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. * @@ -48,102 +68,274 @@ 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). */ + @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 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 + val (paramN, funN, signatures) = computeSignatureHelp(path, span) + (paramN, funN, signatures.flatMap(_.denot)) - enclosingApply.map { + /** + * 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 + * @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 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, Signatures.countParams(fun)) - }.getOrElse((0, 0, Nil)) - def callInfo( - span: Span, - params: List[tpd.Tree], - fun: tpd.Tree, - alreadyAppliedCount : Int - )(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 + /** + * Finds enclosing application from given `path` for `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 } - val (alternativeIndex, alternatives) = fun.tpe match { + /** + * 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]) = + 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) + val (alternativeIndex, alternatives) = alternativesFromError(err, params) match + // 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 _ => val funSymbol = fun.symbol val alternatives = funSymbol.owner.info.member(funSymbol.name).alternatives val alternativeIndex = alternatives.map(_.symbol).indexOf(funSymbol) max 0 (alternativeIndex, alternatives) - } - (paramIndex, alternativeIndex, alternatives) + 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 pre = treeQualifier(fun) + val alternativesWithTypes = alternatives.map(_.asSeenFrom(pre.tpe.widenTermRefExpr)) + val alternativeSignatures = alternativesWithTypes.flatMap(toApplySignature) + + (paramIndex, alternativeIndex, alternativeSignatures) + else + (0, 0, Nil) + /** + * 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[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) + )(using Context): (Int, Int, List[Signature]) = + val resultType = unapplyMethodResult(fun) + val isUnapplySeq = fun.denot.name == core.Names.termName("unapplySeq") + val denot = fun.denot.mapInfo(_ => resultType) - val appliedDenot = fun.symbol.asSingleDenotation.mapInfo(_ => fun.tpe) :: Nil - (activeParameter, 0, appliedDenot) + val paramTypes = extractParamTypess(resultType, denot, patterns.size).flatten.map(stripAllAnnots) + val paramNames = extractParamNamess(resultType, denot).flatten - 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. + val activeParameter = unapplyParameterIndex(patterns, span, paramTypes.length) + 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 - 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 - } + // 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 + * + * @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 + + /** + * 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) - // 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]] = + /** + * 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 + + /** + * 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 = 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("_") + val getMethod = resultType.member(core.Names.termName("get")) + val dropMethod = resultType.member(core.Names.termName("drop")) + + 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) + + /** + * 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)(using Context): Option[Signature] = { + /** + * Creates signature for apply method. + * + * @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] = { val symbol = denot.symbol val docComment = ParsedComment.docOf(symbol) @@ -158,8 +350,7 @@ object Signatures { Nil else toParamss(res) - case _ => - Nil + case _ => Nil } val params = tp.paramNames.zip(tp.paramInfos).map { case (name, info) => Signatures.Param( @@ -168,78 +359,81 @@ 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).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 - + 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)) + 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), Some(denot))) + case other => None + } - val signature = - Signatures.Signature(name, - typeParams, - paramss, - returnType, - docComment.map(_.mainDoc)) + @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 - Some(signature) + // 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 - case other => - None - } + toUnapplySignature(denot.asSingleDenotation, paramNames, paramTypes) + else + toApplySignature(denot) } /** - * The number of parameters that are applied in `tree`. + * 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(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, None, Some(denot))) + else + None + + /** + * 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/src/dotty/tools/languageserver/DottyLanguageServer.scala b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala index 179b8002dfb5..5bc1e9f7dfa5 100644 --- a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala +++ b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala @@ -549,7 +549,6 @@ 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 @@ -557,10 +556,9 @@ class DottyLanguageServer extends LanguageServer 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) + val (paramN, callableN, signatures) = Signatures.signatureHelp(path, pos.span) + new SignatureHelp(signatures.map(signatureToSignatureInformation).asJava, callableN, paramN) - 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 a6c6eeb67e06..84379da3fbe3 100644 --- a/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala +++ b/language-server/test/dotty/tools/languageserver/SignatureHelpTest.scala @@ -17,12 +17,53 @@ class SignatureHelpTest { .signatureHelp(m1, List(signature), Some(0), 0) } + @Test def properFunctionReturnWithoutParenthesis: Unit = { + 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 + |} + |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)$m2(3$m3) + |}""" + .signatureHelp(m1, List(listSignature), Some(0), 0) + .signatureHelp(m2, List(listSignature), Some(0), 0) + .signatureHelp(m3, List(listSignature), Some(0), 2) + } + + @Test def optionProperSignature: Unit = { + 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(signature), 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]` - S("apply[A]", Nil, 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) @@ -31,6 +72,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 @@ -40,7 +92,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 = { @@ -115,7 +167,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 = { @@ -154,6 +206,73 @@ 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) + 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($m1) => + | case h$m2 :: t$m3 => + | } + |} + """ + .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 = { val signature = S("", Nil, List(List(P("", "String"), P("", "String"))), None) @@ -173,7 +292,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 = ??? @@ -185,10 +305,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 = { @@ -207,7 +368,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: