2

I'm using SBT to build my project. I want to analyze some classes from my source code using either Scala or Java reflection during the build process.

How do I define an SBT task that loads a single known class or all classes from my source code?

import sbt._

val loadedClasses = taskKey[Seq[Class[_]]]("All classes from the source")

val classToLoad = settingKey[String]("Scala class name to load")
val loadedClass = taskKey[Seq[Class[_]]]("Loaded classToLoad")
Kolmar
  • 14,086
  • 1
  • 22
  • 25
  • Also looking for solutions that don't use internal APIs or provide access to `scala.reflect` `Mirror` inside an SBT task. – Kolmar May 26 '20 at 19:05

2 Answers2

3

You can use the output of the fullClasspathAsJars SBT task to get access to the JARs produced from you source code. This task doesn't include JARs of the dependencies. Then you can create a ClassLoader to load classes from those JARs:

import java.net.URLClassLoader

val classLoader = taskKey[ClassLoader]("Class loader for source classes")
classLoader := {
  val jarUrls = (Compile / fullClasspathAsJars).value.map(_.data.toURI.toURL).toArray
  new URLClassLoader(jarUrls, ClassLoader.getSystemClassLoader)
}

Then if you know the name of your class in the JAR, you can use this ClassLoader to load it.

Note the difference between Scala class names and class names in the JAR. Scala class names may be mangled, and one Scala class can produce several classes in the JAR. For example my.company.Box.MyClass class from the following snippet produces two JAR classes: my.company.Box$MyClass and my.company.Box$MyClass$, the latter being the class of the companion object.

package my.company
object Box {
  case class MyClass()
}

So if you want to specify a class by its Scala name or to list all classes defined in the source, you have to use the output of the compile SBT task. This task produces a CompileAnalysis object which is part of internal SBT API and is prone to change in the future. The following code works as of SBT 1.3.10.

To load a class by its Scala name:

import sbt.internal.inc.Analysis
import xsbti.compile.CompileAnalysis

def loadClass(
  scalaClassName: String,
  classLoader: ClassLoader,
  compilation: CompileAnalysis
): List[Class[_]] = {
  compilation match {
    case analysis: Analysis =>
      analysis.relations.productClassName
        .forward(scalaClassName)
        .map(classLoader.loadClass)
        .toList
  }
}

classToLoad := "my.company.Box.MyClass"
loadedClass := loadClass(
  classToLoad.value,
  classLoader.value,
  (Compile / compile).value)

To list all classes from the source code:

def loadAllClasses(
  classLoader: ClassLoader,
  compilation: CompileAnalysis,
): List[Class[_]] = {
  val fullClassNames = compilation match {
    case analysis: Analysis =>
      analysis.relations.allSources.flatMap { source =>
        // Scala class names
        val classNames = analysis.relations.classNames(source)
        val getProductName = analysis.relations.productClassName
        classNames.flatMap { className =>
          // Class names in the JAR
          val productNames = getProductName.forward(className)
          if (productNames.isEmpty) Set(className) else productNames
        }
      }.toList
  }

  fullClassNames.map(className => classLoader.loadClass(className))
}

loadedClasses := loadAllClasses(
  classLoader.value,
  (Compile / compile).value)
Kolmar
  • 14,086
  • 1
  • 22
  • 25
  • Was this supposed to be as a reference for future users? You seem to have answered your question almost immediately. – user May 26 '20 at 19:10
  • 3
    @user https://stackoverflow.com/help/self-answer *"To encourage people to do this, there is a checkbox at the bottom of the page every time you ask a question. If you have more than 15 reputation and already know the answer, click the checkbox that says "Answer your own question" at the bottom of the Ask Question page. Type in your answer, then submit both question and answer together."* – Dmytro Mitin May 26 '20 at 19:30
  • @DmytroMitin I didn't know about that, I thought people only did that for meta – user May 26 '20 at 19:32
  • 2
    @user Yes, it's encouraged to answer own questions, but I'd also like to know if there is a better way as I've mentioned in the comment to the question, so other answers are welcome. – Kolmar May 26 '20 at 19:47
  • Will this example work before the code is compiled? It looks like you're directly accessing the compiled binaries. – Thomas Jun 06 '20 at 03:54
1

Based on Reference scala file from build.sbt add the following to project/build.sbt

Compile / unmanagedSourceDirectories += baseDirectory.value / ".." / "src" / "main" / "scala"

and then scala-reflect on project's sources from within build.sbt like so

val reflectScalaClasses = taskKey[Unit]("Reflect on project sources from within sbt")
reflectScalaClasses := {
  import scala.reflect.runtime.universe._
  println(typeOf[example.Hello])
}

where

src
├── main
│   └── scala
│       └── example
│           ├── Hello.scala
Mario Galic
  • 47,285
  • 6
  • 56
  • 98