1

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.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Chris
  • 549
  • 1
  • 5
  • 18

1 Answers1

1

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)

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Thanks for the quick and detailed Dmytro! I found something about scalafix, I was hopping I won't need it. I'll give it a try and will let you know. – Chris Jul 11 '20 at 02:29
  • @Chris Well, important is SemanticDB, not Scalafix. But Scalafix provides convenient API to work with symbols, types etc. (i.e. not only trees, tokens). – Dmytro Mitin Jul 11 '20 at 02:40
  • @Chris You can avoid Scalafix but then you'll have to work with SemanticDB manually. See update. – Dmytro Mitin Jul 11 '20 at 09:22
  • Thanks Dmytro, I get "cannot resolve symbol info and signature". Using scala 2.11.12. I need this for a devs' utility where all related dependencies can be available only in test scope, so I try to keep build.sbt clean – Chris Jul 15 '20 at 16:58
  • @Chris *cannot resolve symbol info and signature* Try Scalafix 0.9.16 and Scala 2.11.12 (or 2.13.2). Just now tried and it worked in both versions. (Just in case sbt 1.3.8.) – Dmytro Mitin Jul 15 '20 at 18:10
  • @Chris Anyway have you tried second approach (without Scalafix)? – Dmytro Mitin Jul 15 '20 at 18:11
  • @Chris In my first approach (with Scalafix) build.sbt sets up transformation of sources from `in` to `out`. You actually don't need `out`. I guess Scalafix can be run as an app as well (see `scalafix-cli`). – Dmytro Mitin Jul 15 '20 at 18:27
  • I tried the second approach (scala 2.11.12, sbt 1.2.8) . For `val initsParents = inits.collect(_.info.map(_.signature) match {` I'm getting 'symbols not found' for `info`&`signature`. for the match block I'm getting 'Required PartialFunction[Any, NotInferedB], Found Function1[Any, Seq[String]]. I was able to find a solution with reflection and meta finally: smth like `instance.info.baseClasses.exists(c => c.info.typeSymbol == baseType)` where `baseType == typeOf[Z].typeSymbol` – Chris Jul 15 '20 at 22:19
  • @Chris *I'm getting 'symbols not found' for `info`&`signature`* `scala.meta.Tree#info` is an extension method contributed by implicit class `TreeOps`, `SymbolInformation#signature` is https://www.javadoc.io/static/org.scalameta/trees_2.11/4.3.18/index.html#scala.meta.internal.semanticdb.SymbolInformation@signature:scala.meta.internal.semanticdb.Signature – Dmytro Mitin Jul 16 '20 at 00:39
  • @Chris Is it possible that it's just IDE shows `info`&`signature` in red? Did you try `sbt compile`? – Dmytro Mitin Jul 16 '20 at 00:40
  • @Chris Also in 2.11 we can't use trailing commas. `"-Yrangepos"` should be instead of `"-Yrangepos",` – Dmytro Mitin Jul 16 '20 at 01:22
  • 1
    Since Scalameta 4.4.16 `case tree@q"""..$mods object $ename extends ${template"""{ ..$stats } with ..$inits { $self => ..$stats1 }"""}""" =>` should be replaced with `case tree@q"""..$mods object $ename extends { ..$stats } with ..$inits { $self => ..$stats1 }""" =>` or `case tree@q"""..$mods object $ename $template""" => template match { case template"{ ..$stats } with ..$inits { $self => ..$stats1 }" =>` https://github.com/scalameta/scalameta/issues/2841 – Dmytro Mitin Sep 08 '22 at 21:37
  • Semanticdb code seems to be working in Scala 3 https://scastie.scala-lang.org/DmytroMitin/3QQwsDG2Rqm71qa6mMMkTw/30 – Dmytro Mitin Sep 25 '22 at 14:30