scala.meta parent of parent of Defn.Object

223 views Asked by At

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.

1

There are 1 answers

13
Dmytro Mitin On

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)