Skip to content

Semi-Fix scaladoc of extensions methods #14321

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions scaladoc-testcases/src/tests/extensionParams.scala
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
package tests.extensionParams

extension [A](thiz: A)
def toTuple2[B](that: B): (A, B) = thiz -> that

def toTuple2[B](that: B): (A, B)
= thiz -> that
extension [A](a: A)(using Int)
def f[B](b: B): (A, B) = ???
def f1[B](b: B): (A, B)
= ???

extension [A](a: A)(using Int)
def ff(b: A): (A, A) = ???
def f2(b: A): (A, A)
= ???

extension [A](a: A)(using Int)
def fff(using String)(b: A): (A, A) = ???
def f3(using String)(b: A): (A, A)
= ???

extension (a: Char)(using Int)
def ffff(using String)(b: Int): Unit = ???
def f4(using String)(b: Int): Unit
= ???

extension (a: Char)(using Int)
def fffff[B](using String)(b: B): Unit = ???
def f5[B](using String)(b: B): Unit
= ???

extension [A <: List[Char]](a: A)(using Int)
def ffffff[B](b: B): (A, B) = ???
def f6[B](b: B): (A, B)
= ???

extension [A <: List[Char]](using String)(using Unit)(a: A)(using Int)(using Number)
def f7[B, C](b: B)(c: C): (A, B)
= ???

extension [A <: List[Char]](using String)(using Unit)(a: A)(using Int)(using Number)
def f8(b: Any)(c: Any): Any
= ???

extension [A <: List[Char]](using String)(using Unit)(a: A)(using Int)(using Number)
def f9[B, C](using Int)(b: B)(c: C): (A, B)
= ???

extension [A <: List[Char]](using String)(using Unit)(a: A)(using Int)(using Number)
def f10(using Int)(b: Any)(c: Any): Any
= ???
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@ trait ClassLikeSupport:
case dd: DefDef if isDocumentableExtension(dd.symbol) =>
dd.symbol.extendedSymbol.map { extSym =>
val memberInfo = unwrapMemberInfo(c, dd.symbol)
val typeParams = dd.symbol.extendedTypeParams.map(mkTypeArgument(_, memberInfo.genericTypes))
val typeParams = dd.symbol.extendedTypeParamList.map(mkTypeArgument(_, memberInfo.genericTypes))
//println(dd.name)
//println("MemberInfo:\n" + memberInfo.paramLists)
//println("extended...:\n" + dd.symbol.extendedTermParamLists)
val termParams = dd.symbol.extendedTermParamLists.zipWithIndex.flatMap { case (paramList, index) =>
memberInfo.paramLists(index) match
case EvidenceOnlyParameterList => Nil
Expand Down Expand Up @@ -328,7 +331,7 @@ trait ClassLikeSupport:
): Member =
val method = methodSymbol.tree.asInstanceOf[DefDef]
val paramLists: List[TermParamClause] = methodSymbol.nonExtensionTermParamLists
val genericTypes: List[TypeDef] = if (methodSymbol.isClassConstructor) Nil else methodSymbol.nonExtensionLeadingTypeParams
val genericTypes: List[TypeDef] = if (methodSymbol.isClassConstructor) Nil else methodSymbol.nonExtensionTypeParamList

val memberInfo = unwrapMemberInfo(c, methodSymbol)

Expand Down
108 changes: 66 additions & 42 deletions scaladoc/src/dotty/tools/scaladoc/tasty/SymOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,15 @@ object SymOps:
import reflect._
sym.flags.is(Flags.Artifact)

def isLeftAssoc: Boolean = !sym.name.endsWith(":")
/**
* note that this is not the right criterion:
* An extension method is treated as a right-associative operator (as in SLS §6.12.3)
* if it has a name ending in : and is immediately followed by a single parameter.
* https://docs.scala-lang.org/scala3/reference/contextual/right-associative-extension-methods.html
*/
def isRightAssoc: Boolean = sym.name.endsWith(":")

def isLeftAssoc: Boolean = !sym.isRightAssoc

def extendedSymbol: Option[reflect.ValDef] =
import reflect.*
Expand All @@ -152,52 +160,68 @@ object SymOps:
else termParamss(1).params(0)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extendedSymbol should probably also be updated, as it implicitly uses the old heuristic


def extendedTypeParams: List[reflect.TypeDef] =
import reflect.*
val method = sym.tree.asInstanceOf[DefDef]
method.leadingTypeParams
def splitExtensionParamLists: (List[reflect.ParamClause], List[reflect.ParamClause]) =
if sym.isRightAssoc && sym.isExtensionMethod then
val unswapped@(extPart, defPart) = sym.splitExtensionParamListsAssumingLeftAssoc
def nonUsingClauses(clauses: List[reflect.ParamClause]) = clauses.zipWithIndex.collect{case (terms: reflect.TermParamClause, i) if !terms.isGiven => (terms, i)}
val extNonUsingClause = nonUsingClauses(extPart)
val defNonUsingClauses = nonUsingClauses(defPart)
assert(extNonUsingClause.size == 1)
Copy link
Contributor Author

@Sporarum Sporarum Jan 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assert seems to be causing some of the tests to fail, but I don't understand why


def extendedTermParamLists: List[reflect.TermParamClause] =
import reflect.*
if sym.nonExtensionLeadingTypeParams.nonEmpty then
sym.nonExtensionParamLists.takeWhile {
case _: TypeParamClause => false
case _ => true
}.collect {
case tpc: TermParamClause => tpc
}
if defNonUsingClauses.lift(0).map(_._1.params.size != 1).getOrElse(true) // was not really right associative, see comment of isRightAssoc
then unswapped
else
val (first, i1) = extNonUsingClause(0)
val (second, i2) = defNonUsingClauses(0) // since cond is false, we know lift(0) returned Some(_)
(extPart.updated(i1, second), defPart.updated(i2, first))
else
List.empty
sym.splitExtensionParamListsAssumingLeftAssoc

/**
* This uses the assumption that there is the following "pos hierachy": extension paramss < DefDef < extMethod paramss
* /!\ where DefDef is the tree containing the paramss
* It wouldn't really make sense for the Def's position not to be either the "def" or the method name,
* but is not enforced
*/
def splitExtensionParamListsAssumingLeftAssoc: (List[reflect.ParamClause], List[reflect.ParamClause]) =
val method = sym.tree.asInstanceOf[reflect.DefDef]
val paramss = method.paramss //List[ParamClause[T]] //ParamClause[T] = List[ValDef[T]] | List[TypeDef[T]]
val defCoord = method.symbol.pos.get.start //.span.point

val res = paramss.span{
case reflect.TypeParamClause(params) => params.head.symbol.pos.get.start < defCoord //.span.start
case reflect.TermParamClause(params) =>
params.headOption
.map(_.symbol.pos.get.start < defCoord) //.span.start
.getOrElse(false) // () is only allowed on the RHS of extensions
}
//println(method.name)
//println(res._1.map(_.params.map(_.show)).mkString("ExtensionPart:\n","\n","\n"))
//println(res._2.map(_.params.map(_.show)).mkString("NonExtensionPart:\n","\n","\n"))
res


def extendedParamLists: List[reflect.ParamClause] =
sym.splitExtensionParamLists._1

def extendedTermParamLists: List[reflect.TermParamClause] =
sym.extendedParamLists.collect{case terms: reflect.TermParamClause => terms}

def extendedTypeParamList: List[reflect.TypeDef] =
val typeParamss: List[reflect.TypeParamClause] = sym.extendedParamLists.collect{case types: reflect.TypeParamClause => types}
typeParamss.headOption.map(_.params).getOrElse(List()) // only one type param clause on LHS

def nonExtensionTermParamLists: List[reflect.TermParamClause] =
import reflect.*
if sym.nonExtensionLeadingTypeParams.nonEmpty then
sym.nonExtensionParamLists.dropWhile {
case _: TypeParamClause => false
case _ => true
}.drop(1).collect {
case tpc: TermParamClause => tpc
}
else
sym.nonExtensionParamLists.collect {
case tpc: TermParamClause => tpc
}

def nonExtensionParamLists: List[reflect.ParamClause] =
import reflect.*
val method = sym.tree.asInstanceOf[DefDef]
if sym.isExtensionMethod then
val params = method.paramss
val toDrop = if method.leadingTypeParams.nonEmpty then 2 else 1
if sym.isLeftAssoc || params.size == 1 then params.drop(toDrop)
else params.head :: params.tail.drop(toDrop)
else method.paramss

def nonExtensionLeadingTypeParams: List[reflect.TypeDef] =
import reflect.*
sym.nonExtensionParamLists.collectFirst {
case TypeParamClause(params) => params
}.toList.flatten
sym.splitExtensionParamLists._2

def nonExtensionTermParamLists: List[reflect.TermParamClause] =
sym.nonExtensionParamLists.collect{case terms: reflect.TermParamClause => terms}

def nonExtensionTypeParamList: List[reflect.TypeDef] =
val typeParamss: List[reflect.TypeParamClause] = sym.nonExtensionParamLists.collect{case types: reflect.TypeParamClause => types}
typeParamss.headOption.map(_.params).getOrElse(List()) // only one type param clause on RHS


end extension

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dotty.tools.scaladoc.signatures

import java.security.Signature

class GenericSignaftures extends SignatureTest("genericSignatures", Seq("class"))

class ObjectSignatures extends SignatureTest("objectSignatures", Seq("object"))
Expand All @@ -22,6 +24,8 @@ class GenericMethodsTest extends SignatureTest("genericMethods", Seq("def"))

class MethodsAndConstructors extends SignatureTest("methodsAndConstructors", Seq("def"))

class ExtensionParams extends SignatureTest("extensionParams", SignatureTest.all)

class TypesSignatures extends SignatureTest("typesSignatures", SignatureTest.all)

class FieldsSignatures extends SignatureTest("fieldsSignatures", SignatureTest.all.filterNot(_ == "object"))
Expand Down