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

Add utility to extract macro classes from a classpath. #8201

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions 3rdparty/jvm/io/get-coursier/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
jar_library(
name='coursier',
jars=[
scala_jar(org='io.get-coursier', name='coursier', rev='2.0.0-RC3-3'),
],
)
6 changes: 6 additions & 0 deletions 3rdparty/jvm/org/scala-lang/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
jar_library(
name='scala-compiler',
jars=[
jar(org='org.scala-lang', name='scala-compiler', rev='2.12.8'),
],
)
13 changes: 13 additions & 0 deletions 3rdparty/jvm/org/scalameta/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
jar_library(
name='scalameta',
jars=[
scala_jar(org='org.scalameta', name='scalameta', rev='4.2.2'),
],
)

jar_library(
name='testkit',
jars=[
scala_jar(org='org.scalameta', name='testkit', rev='4.2.2'),
],
)
6 changes: 6 additions & 0 deletions src/scala/org/pantsbuild/native_image/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
scala_library(
dependencies=[
'3rdparty/jvm/org/scalameta:scalameta',
'3rdparty/jvm/org/scala-lang:scala-compiler',
],
)
63 changes: 63 additions & 0 deletions src/scala/org/pantsbuild/native_image/FindMacros.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.pantsbuild.native_image

import scala.tools.asm._
import scala.meta.internal.io._
import scala.meta.internal.metacp._
import scala.meta.io.Classpath
import scala.tools.scalap.scalax.rules.scalasig._
import scala.collection.mutable

object FindMacros {
def main(args: Array[String]): Unit = {
if (args.isEmpty) {
println("Missing argument <classpath>, see --help.")
System.exit(1)
} else if (args.sameElements(Array("--help"))) {
println(
"""|usage: find-macros <classpath>
|
|Given a JVM classpath, prints out the fully qualified JVM names
|of classes |that define Scala macros. The classpath is formatted
|as a path separated |list of paths to jar files, example
|"a.jar:b.jar:c.jar".
|""".stripMargin
)
} else {
val classNames = run(args(0))
classNames.foreach { className =>
println(className)
}
}
}

def run(classpath: String): Iterable[String] = {
val compiler = new FindMacrosCompiler(classpath)
val classNames = mutable.Set.empty[String]
Classpath(classpath).entries.par.foreach { jar =>
Classpath(jar).foreach { jarRoot =>
for {
classfile <- FileIO.listAllFilesRecursively(jarRoot.path).iterator
if classfile.toNIO.getFileName().toString().endsWith(".class")
scalapSignature <- classfile.toClassNode.scalaSig.iterator
scalapSymbol <- scalapSignature.scalaSig.symbols.iterator.collect {
case sig: SymbolInfoSymbol => sig
}
if scalapSymbol.isMethod
scalapAnnotation <- scalapSymbol.attributes
className <- scalapAnnotation.typeRef match {
// The Scala compiler generates a `@macroImpl(...)` annotation
// for every macro definition such as `def foo: Int = macro impl`.
case tpe: TypeRefType if tpe.symbol.name == "macroImpl" =>
compiler.macroClassName(scalapSymbol)
case _ =>
Nil
}
} {
classNames += className
}
}
}
compiler.shutdown()
classNames
}
}
76 changes: 76 additions & 0 deletions src/scala/org/pantsbuild/native_image/FindMacrosCompiler.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.pantsbuild.native_image

import scala.tools.asm._
import scala.tools.scalap.scalax.rules.scalasig._
import scala.reflect.NameTransformer
import scala.tools.nsc.interactive.Global
import scala.tools.nsc.Settings
import scala.tools.nsc.reporters.ConsoleReporter
import scala.reflect.io.VirtualDirectory

/**
* Wrapper around a scala-compiler Global instance with helper methods to
* extract information from macro definition annotations.
*/
class FindMacrosCompiler(classpath: String) {
private val g = newCompiler(classpath)

def shutdown(): Unit = g.askShutdown()

def macroClassName(scalapSymbol: Symbol): List[String] = synchronized {
for {
scalacSymbol <- compilerSymbol(scalapSymbol).alternatives
scalacAnnotation <- scalacSymbol.annotations
// See documentation for `scala.tools.nsc.typechecker.MacroImplBinding`
// for details about the structure of the `@macroImpl` annotation.
className <- scalacAnnotation.args match {
case List(apply) =>
treeArguments(apply).collect {
case g.Assign(
g.Literal(g.Constant("className")),
g.Literal(g.Constant(fqn: String))
) =>
fqn
}
case _ => Nil
}
} yield className
}

private def newCompiler(classpath: String): Global = {
val settings = new Settings()
val vd = new VirtualDirectory("(memory)", None)
settings.classpath.value = classpath
settings.outputDirs.setSingleOutput(vd)
settings.YpresentationAnyThread.value = true
val reporter = new ConsoleReporter(settings)
new Global(settings, reporter)
}

private def isTermSymbol(scalapSymbol: Symbol): Boolean =
scalapSymbol match {
case _: ObjectSymbol => true
case _: MethodSymbol => true
case s: ExternalSymbol => s.entry.entryType == 10
case c: ClassSymbol => c.isModule
case _ => false
}

// Returns a Scala compiler symbol given a scalap Symbol.
private def compilerSymbol(scalapSymbol: Symbol): g.Symbol = {
val encoded = NameTransformer.encode(scalapSymbol.name)
val name =
if (isTermSymbol(scalapSymbol)) g.TermName(encoded)
else g.TypeName(encoded)
val result = scalapSymbol.parent match {
case None => g.findMemberFromRoot(name)
case Some(value) => compilerSymbol(value).info.decl(name)
}
result
}

private def treeArguments(t: g.Tree): List[g.Tree] = t match {
case g.Apply(_, as) => as
case g.TypeApply(fun, _) => treeArguments(fun)
}
}
8 changes: 8 additions & 0 deletions tests/scala/org/pantsbuild/native_image/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
junit_tests(
sources=globs('*.scala'),
dependencies=[
'src/scala/org/pantsbuild/native_image',
'3rdparty/jvm/org/scalameta:testkit',
'3rdparty/jvm/io/get-coursier:coursier',
]
)
147 changes: 147 additions & 0 deletions tests/scala/org/pantsbuild/native_image/FindMacrosSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package org.pantsbuild.native_image

import scala.meta.testkit.DiffAssertions
import org.scalatest.FunSuite
import coursier._
import java.io.File

class FindMacrosSuite extends FunSuite with DiffAssertions {
test("expect") {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it might be worth using ignore for this test since it downloads quite a lot of jars from Maven Central and I estimate the FindMacros class won't change so frequently.

val fetch = Fetch()
.addDependencies(
dep"org.scalatest:scalatest_2.12:3.0.8",
dep"org.scalameta:metals_2.12:0.7.0",
dep"org.apache.spark:spark-sql_2.12:2.4.3",
dep"com.typesafe.akka:akka-http_2.11:10.1.9",
dep"org.typelevel:cats-effect_2.12:2.0.0-RC1",
dep"io.getquill:quill-core_2.12:3.4.2",
dep"com.chuusai:shapeless_2.12:2.3.3"
)
.run()
val obtained = FindMacros
.run(fetch.mkString(File.pathSeparator))
.toSeq
.sorted
.mkString("\n")
val expected =
"""|
|akka.http.ccompat.pre213macro$
|akka.http.ccompat.since213macro$
|akka.macros.LogHelper$
|akka.parboiled2.DynamicRuleDispatch$
|akka.parboiled2.ParserMacros$
|cats.arrow.FunctionKMacros$
|com.typesafe.scalalogging.LoggerMacro$
|com.typesafe.scalalogging.LoggerTakingImplicitMacro$
|fastparse.internal.MacroImpls$
|fastparse.internal.MacroRepImpls$
|io.getquill.ImplicitQueryMacro
|io.getquill.context.ActionMacro
|io.getquill.context.QueryMacro
|io.getquill.dsl.DynamicQueryDslMacro
|io.getquill.dsl.EncodingDslMacro
|io.getquill.dsl.MetaDslMacro
|io.getquill.dsl.QueryDslMacro
|io.getquill.dsl.QuotationMacro
|io.getquill.monad.IOMonadMacro
|macrocompat.BundleMacro
|org.scalactic.RequirementsMacro$
|org.scalactic.SnapshotsMacro$
|org.scalactic.anyvals.DigitCharMacro$
|org.scalactic.anyvals.DigitMacro$
|org.scalactic.anyvals.DigitStringMacro$
|org.scalactic.anyvals.GuessANumberMacro$
|org.scalactic.anyvals.PercentMacro$
|org.scalactic.anyvals.PosDoubleMacro$
|org.scalactic.anyvals.PosFloatMacro$
|org.scalactic.anyvals.PosIntMacro$
|org.scalactic.anyvals.PosLongMacro$
|org.scalactic.anyvals.PosZDoubleMacro$
|org.scalactic.anyvals.PosZFloatMacro$
|org.scalactic.anyvals.PosZIntMacro$
|org.scalactic.anyvals.PosZLongMacro$
|org.scalactic.anyvals.TLAMacro$
|org.scalactic.source.PositionMacro$
|org.scalamacros.resetallattrs.Macros$
|org.scalameta.UnreachableMacros
|org.scalameta.adt.AdtNamerMacros
|org.scalameta.adt.AdtTyperMacrosBundle
|org.scalameta.adt.LiftableMacros
|org.scalameta.data.DataMacros
|org.scalameta.data.DataTyperMacrosBundle
|org.scalameta.explore.ExploreMacros
|org.scalameta.invariants.Macros
|org.scalameta.tests.typecheckError$
|org.scalatest.AssertionsMacro$
|org.scalatest.CompileMacro$
|org.scalatest.DiagrammedAssertionsMacro$
|org.scalatest.ExpectationsMacro$
|org.scalatest.matchers.MatchPatternMacro$
|org.scalatest.matchers.MatcherFactory1$
|org.scalatest.matchers.MatcherFactory2$
|org.scalatest.matchers.MatcherFactory3$
|org.scalatest.matchers.MatcherFactory4$
|org.scalatest.matchers.MatcherFactory5$
|org.scalatest.matchers.MatcherFactory6$
|org.scalatest.matchers.MatcherFactory7$
|org.scalatest.matchers.MatcherFactory8$
|org.scalatest.matchers.TypeMatcherMacro$
|perfolation.Macros$
|pprint.TPrintLowPri$
|scala.meta.internal.classifiers.ClassifierMacros
|scala.meta.internal.fastparse.utils.MacroUtils$
|scala.meta.internal.prettyprinters.ShowMacros
|scala.meta.internal.quasiquotes.ConversionMacros
|scala.meta.internal.quasiquotes.ReificationMacros
|scala.meta.internal.tokens.RootNamerMacros
|scala.meta.internal.tokens.TokenInfoMacros
|scala.meta.internal.tokens.TokenNamerMacros
|scala.meta.internal.transversers.TransformerMacros
|scala.meta.internal.transversers.TraverserMacros
|scala.meta.internal.trees.AstInfoMacros
|scala.meta.internal.trees.AstNamerMacros
|scala.meta.internal.trees.BranchNamerMacros
|scala.meta.internal.trees.CommonTyperMacrosBundle
|scala.meta.internal.trees.LiftableMacros
|scala.meta.internal.trees.QuasiquoteMacros
|scala.meta.internal.trees.RegistryMacros
|scala.meta.internal.trees.RootNamerMacros
|scribe.Macros$
|scribe.ScribeMacros$
|shapeless.AnnotationMacros
|shapeless.CachedImplicitMacros
|shapeless.CachedMacros
|shapeless.DefaultMacros
|shapeless.Generic1Macros
|shapeless.GenericMacros
|shapeless.IsCCons1Macros
|shapeless.IsHCons1Macros
|shapeless.LabelledMacros
|shapeless.LazyMacrosRef
|shapeless.LowPriorityMacros
|shapeless.NatMacros
|shapeless.OrphanMacros
|shapeless.PolyMacros
|shapeless.ProductMacros
|shapeless.RecordMacros
|shapeless.SingletonTypeMacros
|shapeless.Split1Macros
|shapeless.TestMacros
|shapeless.TheMacros
|shapeless.TypeOf$Macros
|shapeless.TypeableMacros
|shapeless.UnionMacros
|shapeless.ops.nat$ToIntMacros
|shapeless.ops.record.LacksKeyMacros
|shapeless.ops.record.ModifierMacros
|shapeless.ops.record.RemoverMacros
|shapeless.ops.record.SelectorMacros
|shapeless.ops.record.UpdaterMacros
|shapeless.test.CompileTimeMacros
|shapeless.test.IllTypedMacros
|shapeless.test.TypeTraceMacros
|sourcecode.Macros$
|""".stripMargin
assertNoDiffOrPrintExpected(obtained, expected)
}
}