Skip to content
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

Suggestion: Surface - get list of derived classes #3428

Open
OndrejSpanel opened this issue Feb 28, 2024 · 3 comments
Open

Suggestion: Surface - get list of derived classes #3428

OndrejSpanel opened this issue Feb 28, 2024 · 3 comments
Labels

Comments

@OndrejSpanel
Copy link
Contributor

OndrejSpanel commented Feb 28, 2024

I use Surface to inspect ASTs in my application. There is one feature which I currently have to implement on my own, and that is getting all derived classes of a sealed trait / class. I have an implementation for Scala 2 using TypeTags and for Scala 3 using a macro.

Given Surface seems to be used to implement codecs and codec libraries often offer a similar functionality, would such a function be in a scope of the surface library?

The declaration could be def Surface.derivedClasses[T]: Seq[Surface].

If this is welcome, I can try preparing a PR.


I am not sure if providing methods is also desired. In my current use I do not need it.

I am afraid a separate function would be necessary for this. I would be nice to forward necessary information in Surface implementations, so that it a method def methodsOf: Seq[MethodSurface] could be implement directly in the Surface trait, but while this would be straightforward in Scala 2 using TypeTag, I do not see how this could be done in Scala 3 (something like inline type alias would be necessary to pass [A]).

A simple alternative would be to provide a separate function as:

def Surface.derivedClassesWithMethods[T]: Seq[(Surface, Seq[MethodSurface])].

... or perhaps somebody can suggest something better.

@xerial
Copy link
Member

xerial commented Feb 29, 2024

Listing derived classes of a sealed trait is interesting. I also have some use cases, but mostly for testing purpose. I'd like to see how complex your implementation is before deciding to support it in airframe-surface.

In JVM world, I would use just Java reflection as in

def findClasses[A](
packageName: String,
toSearch: Class[A],
classLoader: ClassLoader = Thread.currentThread.getContextClassLoader
): Seq[Class[A]] = {

which is simpler to use and does not need to generate a lot of code at compile time.

def Surface.derivedClassesWithMethods[T]: Seq[(Surface, Seq[MethodSurface])] will generate too large code, which may hit 64k-byte code size limitation in JVM. Enumerating Surface.methodsOf[X], methodsOf[Y], .. would be much easier now that GitHub copilot can generate such enumeration code at ease.

def Surface.derivedClassesWithMethods[T]: Seq[(Surface, Seq[MethodSurface])].

@xerial xerial added the idea label Feb 29, 2024
@OndrejSpanel
Copy link
Contributor Author

OndrejSpanel commented Feb 29, 2024

Checking my source code it seems my Scala 2 version is not producing Surface, only Type. I guess producing Surface from it should be possible, but Surface.of is a macro and therefore will not work with it:

  private def listDerivedClasses(t: Type): Set[Type] = {
    def recurse(tpe: Type): Set[Type] = {
      if (tpe.typeSymbol.isAbstract && tpe.typeSymbol.isClass && tpe.typeSymbol.asClass.isSealed) {
        val subclasses = tpe.typeSymbol.asClass.knownDirectSubclasses.map(sym => sym.asType.typeSignature)
        subclasses.flatMap(recurse)
      } else {
        Set(tpe)
      }
    }
    recurse(t)
  }

  def listDerivedClasses[T: TypeTag]: Set[TypeDesc] = {
    listDerivedClasses(implicitly[TypeTag[T]].tpe)
  }

Here is Scala 3 version, which produces list of Surfaces just fine:

  def getSurface(quotes: Quotes)(classSymbol: quotes.reflect.Symbol): Option[Expr[Surface]] = {
    given q: Quotes = quotes
    import quotes.reflect.*
    val subclassType = classSymbol.typeRef
    Option.when(!classSymbol.flags.is(Flags.Trait) || !classSymbol.flags.is(Flags.Sealed)) {
      subclassType.asType match {
        case '[t] =>
          '{ Surface.of[t] }.asExprOf[Surface]
      }
    }
  }

  def enumerateSubclassesImpl[T: Type](using Quotes): Expr[List[Surface]] = {
    import quotes.reflect.*

    // Get the symbol of the sealed trait T
    val traitSymbol = TypeRepr.of[T].typeSymbol
    if (traitSymbol.flags.is(Flags.Sealed)) { // Ensure T is a sealed trait and get its subclasses
      // Recursive function to get all subclasses, including indirect ones
      def getAllSubclasses(symbol: Symbol): List[Symbol] = {
        symbol.children.flatMap { child =>
          child :: getAllSubclasses(child)
        }
      }

      // documentation seems unclear, it seems children already include indirect children, but not always
      val process = getAllSubclasses(traitSymbol).distinct

      val childrenExpr = process.flatMap(getSurface(summon[Quotes]))
      Expr.ofList(childrenExpr)
    } else {
      report.error(s"${traitSymbol.fullName} is not a sealed trait")
      '{ List.empty[Surface] }
    }
  }

  inline def enumerateSubclasses[T]: List[Surface] = ${ enumerateSubclassesImpl[T] }

@OndrejSpanel
Copy link
Contributor Author

Enumerating Surface.methodsOf[X], methodsOf[Y], .. would be much easier now that GitHub copilot can generate such enumeration code at ease.

I think the trouble with such solutions is a maintenance burden - you need to update the list each time the list of subclasses changes. It may be easy to do, but it may be easy to forget to do so. However, as I said, I personally have no need for such function at this moment, therefore I will not press to implement it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants