Search code examples
scalaabstract-syntax-treetype-inferencescalametasemanticdb

How to get the type of a variable with scalameta if the decltpe is empty?


If I have the following type

Defn.Var(mods, pats, decltpe, rhs) 

in scalameta it might happen that decltype is set to None for a variable like this:

var x = 10

I still want to know the exact type of the variable x which Scala has inferred without checking the type of the assignment expression myself. I know that I can just get the information that 10 is an Int literal but for more complex expressions it might be helpful to have some helper function for the type. Is there any function in scalameta which gives you the inferred type?


Solution

  • Scalameta

    libraryDependencies += "org.scalameta" %% "scalameta" % "4.2.0"
    

    mostly works with source code (parses it to trees, transforms trees) before its compilation i.e. when there can't be any information about symbols and types. To get information about symbols and types one should start compiler (one of).

    This is exactly what SemanticDB is for. If you switch on semanticdb-scalac compiler plugin

    addCompilerPlugin("org.scalameta" % "semanticdb-scalac" % "4.1.0" cross CrossVersion.full)
    scalacOptions ++= Seq(
      "-Yrangepos",
      "-P:semanticdb:synthetics:on",
    )
    

    then upon compilation it will generate .semanticdb files near to .class files. Information about symbols and types will be there. These files can be parsed with semanticdb

    libraryDependencies += "org.scalameta" %% "semanticdb" % "4.1.0"
    

    For example if you have App1.scala

    object App1 {
      var x = 10
    }
    

    then

    import java.nio.file.Paths
    import scala.meta.internal.semanticdb.Locator
    Locator(
      Paths.get("./target/scala-2.12/classes/META-INF/semanticdb/src/main/scala/App1.scala.semanticdb")
    )((path, textDocuments) =>
      println(textDocuments)
    )
    

    produces

    TextDocuments(Vector(TextDocument(SEMANTICDB4,src/main/scala/App1.scala,,29E9BFD566BEFD436FBE59679524E53D,SCALA,Vector(SymbolInformation(_empty_/App1.`x_=`().,SCALA,METHOD,2048,x_=,MethodSignature(Some(Scope(Vector(),Vector())),Vector(Scope(Vector(_empty_/App1.`x_=`().(x$1)),Vector())),TypeRef(Empty,scala/Unit#,Vector())),Vector(),PublicAccess()), SymbolInformation(_empty_/App1.,SCALA,OBJECT,8,App1,ClassSignature(Some(Scope(Vector(),Vector())),Vector(TypeRef(Empty,scala/AnyRef#,Vector())),Empty,Some(Scope(Vector(_empty_/App1.x()., _empty_/App1.`x_=`().),Vector()))),Vector(),PublicAccess()), SymbolInformation(_empty_/App1.`x_=`().(x$1),SCALA,PARAMETER,0,x$1,ValueSignature(TypeRef(Empty,scala/Int#,Vector())),Vector(),Empty), SymbolInformation(_empty_/App1.x().,SCALA,METHOD,2048,x,MethodSignature(Some(Scope(Vector(),Vector())),Vector(),TypeRef(Empty,scala/Int#,Vector())),Vector(),PublicAccess())),Vector(SymbolOccurrence(Some(Range(1,6,1,7)),_empty_/App1.x().,DEFINITION), SymbolOccurrence(Some(Range(0,7,0,11)),_empty_/App1.,DEFINITION)),Vector(),Vector())))
    

    You can pretty-print this file with Metap

    import scala.meta.cli.Metap
    Metap.main(Array("target/scala-2.12/classes/META-INF/semanticdb/src/main/scala/App1.scala.semanticdb"))
    

    Output

    src/main/scala/App1.scala
    -------------------------
    
    Summary:
    Schema => SemanticDB v4
    Uri => src/main/scala/App1.scala
    Text => empty
    Language => Scala
    Symbols => 4 entries
    Occurrences => 2 entries
    
    Symbols:
    _empty_/App1. => final object App1 extends AnyRef { +2 decls }
    _empty_/App1.`x_=`(). => var method x_=(x$1: Int): Unit
    _empty_/App1.`x_=`().(x$1) => param x$1: Int
    _empty_/App1.x(). => var method x: Int
    
    Occurrences:
    [0:7..0:11) <= _empty_/App1.
    [1:6..1:7) <= _empty_/App1.x().
    

    And

    import scala.meta.internal.semanticdb.TypeRef
    import scala.meta.internal.semanticdb.SignatureMessage.SealedValue.{ClassSignature, MethodSignature, TypeSignature, ValueSignature, Empty}
    
    Locator(
      Paths.get("./target/scala-2.12/classes/META-INF/semanticdb/src/main/scala/App1.scala.semanticdb")
    )((path, textDocuments) =>
      for {
        document <- textDocuments.documents
        symbol <- document.symbols
      } println(s"symbol=${symbol.displayName}, ${symbol.signature.asMessage.sealedValue match {
        case v: ValueSignature => s"ValueSignature, type=${v.value.tpe match { case t: TypeRef => t.symbol}}"
        case m: MethodSignature => s"MethodSignature, returnType=${m.value.returnType match { case t: TypeRef => t.symbol}}"
        case c: ClassSignature => "ClassSignature"
        case t: TypeSignature => "TypeSignature"
        case Empty => "Empty"
      }}")
    )
    

    produces

    symbol=x_=, MethodSignature, returnType=scala/Unit#
    symbol=App1, ClassSignature
    symbol=x$1, ValueSignature, type=scala/Int#
    symbol=x, MethodSignature, returnType=scala/Int#
    

    Scheme is here.

    Also you can try Scalafix

    sbt new scalacenter/scalafix.g8 --repo="scalafixdemo"
    cd scalafix
    sbt ~tests/test
    

    If you write in input/src/main/scala/fix/Scalafixdemo.scala

    package fix
    
    object Scalafixdemo {
      var x = 10
    }
    

    and in rules/src/main/scala/fix/Scalafixdemo.scala

    package fix
    
    import scalafix.v1._
    import scala.meta._
    
    class Scalafixdemo extends SemanticRule("Scalafixdemo") {
    
      override def fix(implicit doc: SemanticDocument): Patch = {
    //    println("Tree.syntax: " + doc.tree.syntax)
    //    println("Tree.structure: " + doc.tree.structure)
    //    println("Tree.structureLabeled: " + doc.tree.structureLabeled)
    
        doc.tree.traverse {
          case t@q"..$mods var ..$patsnel: $tpeopt = $expropt" =>
            println(t.symbol.info.get.signature)
        }
    
        Patch.empty
      }
    }
    

    then it will print : Int

    https://github.com/DmytroMitin/scalafix-codegen