Reputation: 547
Let it be the following hierarchy:
object X extends Y{
...
}
trait Y extends Z {
...
}
trait Z {
def run(): Unit
}
I parse the scala file containing the X
and
I want to know if its parent or grandparent is Z
.
I can check for parent as follows:
Given that x: Defn.Object
is the X
class I parsed,
x
.children.collect { case c: Template => c }
.flatMap(p => p.children.collectFirst { case c: Init => c }
will give Y
.
Question: Any idea how I can get the parent of the parent of X
(which is Z
in the above example) ?
Loading Y
(the same way I loaded X
) and finding it's parent doesn't seem like a good idea, since the above is part of a scan procedure where among all files under src/main/scala
I'm trying to find all classes which extend Z
and implement run
, so I don't see an easy and performant way to create a graph with all intermediate classes so as to load them in the right order and check for their parents.
Upvotes: 1
Views: 237
Reputation: 51703
It seems you want Scalameta to process your sources not syntactically but semantically. Then you need SemanticDB. Probably the most convenient way to work with SemanticDB is Scalafix
rules/src/main/scala/MyRule.scala
import scalafix.v1._
import scala.meta._
class MyRule extends SemanticRule("MyRule") {
override def isRewrite: Boolean = true
override def description: String = "My Rule"
override def fix(implicit doc: SemanticDocument): Patch = {
doc.tree.traverse {
case q"""..$mods object $ename extends ${template"""
{ ..$stats } with ..$inits { $self => ..$stats1 }"""}""" =>
val initsParents = inits.collect(_.symbol.info.map(_.signature) match {
case Some(ClassSignature(_, parents, _, _)) => parents
}).flatten
println(s"object: $ename, parents: $inits, grand-parents: $initsParents")
}
Patch.empty
}
}
in/src/main/scala/App.scala
object X extends Y{
override def run(): Unit = ???
}
trait Y extends Z {
}
trait Z {
def run(): Unit
}
Output of sbt out/compile
object: X, parents: List(Y), grand-parents: List(AnyRef, Z)
build.sbt
name := "scalafix-codegen"
inThisBuild(
List(
//scalaVersion := "2.13.2",
scalaVersion := "2.11.12",
addCompilerPlugin(scalafixSemanticdb),
scalacOptions ++= List(
"-Yrangepos"
)
)
)
lazy val rules = project
.settings(
libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % "0.9.16",
organization := "com.example",
version := "0.1",
)
lazy val in = project
lazy val out = project
.settings(
sourceGenerators.in(Compile) += Def.taskDyn {
val root = baseDirectory.in(ThisBuild).value.toURI.toString
val from = sourceDirectory.in(in, Compile).value
val to = sourceManaged.in(Compile).value
val outFrom = from.toURI.toString.stripSuffix("/").stripPrefix(root)
val outTo = to.toURI.toString.stripSuffix("/").stripPrefix(root)
Def.task {
scalafix
.in(in, Compile)
.toTask(s" --rules=file:rules/src/main/scala/MyRule.scala --out-from=$outFrom --out-to=$outTo")
.value
(to ** "*.scala").get
}
}.taskValue
)
project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.16")
Other examples:
https://github.com/olafurpg/scalafix-codegen (semantic)
https://github.com/DmytroMitin/scalafix-codegen (semantic)
https://github.com/DmytroMitin/scalameta-demo (syntactic)
Is it possible to using macro to modify the generated code of structural-typing instance invocation? (semantic)
Scala conditional compilation (syntactic)
Macro annotation to override toString of Scala function (syntactic)
How to merge multiple imports in scala? (syntactic)
You can avoid Scalafix but then you'll have to work with internals of SemanticDB manually
import scala.meta._
import scala.meta.interactive.InteractiveSemanticdb
import scala.meta.internal.semanticdb.{ClassSignature, Range, SymbolInformation, SymbolOccurrence, TypeRef}
val source: String =
"""object X extends Y{
| override def run(): Unit = ???
|}
|
|trait Y extends Z
|
|trait Z {
| def run(): Unit
|}""".stripMargin
val textDocument = InteractiveSemanticdb.toTextDocument(
InteractiveSemanticdb.newCompiler(List(
"-Yrangepos"
)),
source
)
implicit class TreeOps(tree: Tree) {
val occurence: Option[SymbolOccurrence] = {
val treeRange = Range(tree.pos.startLine, tree.pos.startColumn, tree.pos.endLine, tree.pos.endColumn)
textDocument.occurrences
.find(_.range.exists(occurrenceRange => treeRange == occurrenceRange))
}
val info: Option[SymbolInformation] = occurence.flatMap(_.symbol.info)
}
implicit class StringOps(symbol: String) {
val info: Option[SymbolInformation] = textDocument.symbols.find(_.symbol == symbol)
}
source.parse[Source].get.traverse {
case tree@q"""..$mods object $ename extends ${template"""
{ ..$stats } with ..$inits { $self => ..$stats1 }"""}""" =>
val initsParents = inits.collect(_.info.map(_.signature) match {
case Some(ClassSignature(_, parents, _, _)) =>
parents.collect {
case TypeRef(_, symbol, _) => symbol
}
}).flatten
println(s"object = $ename = ${ename.info.map(_.symbol)}, parents = $inits = ${inits.map(_.info.map(_.symbol))}, grand-parents = $initsParents")
}
Output:
object = X = Some(_empty_/X.), parents = List(Y) = List(Some(_empty_/Y#)), grand-parents = List(scala/AnyRef#, _empty_/Z#)
build.sbt
//scalaVersion := "2.13.3"
scalaVersion := "2.11.12"
lazy val scalametaV = "4.3.18"
libraryDependencies ++= Seq(
"org.scalameta" %% "scalameta" % scalametaV,
"org.scalameta" % "semanticdb-scalac" % scalametaV cross CrossVersion.full
)
Semanticdb code seems to be working in Scala 3
https://scastie.scala-lang.org/DmytroMitin/3QQwsDG2Rqm71qa6mMMkTw/36 [copy] (at Scastie -Dscala.usejavacp=true
didn't help with object scala.runtime in compiler mirror not found
, so I used Coursier to guarantee that scala-library
is on path, locally it works without Coursier)
Upvotes: 1