All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dotty.tools.dotc.interactive.InteractiveDriver.scala Maven / Gradle / Ivy

package dotty.tools
package dotc
package interactive

import java.net.URI
import java.io._
import java.nio.file._
import java.nio.file.attribute.BasicFileAttributes
import java.util.stream._
import java.util.zip._
import java.util.function._

import scala.collection._
import JavaConverters._
import scala.io.Codec

import dotty.tools.io.{ ClassPath, ClassRepresentation, PlainFile, VirtualFile }

import ast.{Trees, tpd}
import core._, core.Decorators._
import Contexts._, Flags._, Names._, NameOps._, Symbols._, SymDenotations._, Trees._, Types._
import classpath._
import reporting._, reporting.diagnostic.MessageContainer
import util._

/** A Driver subclass designed to be used from IDEs */
class InteractiveDriver(val settings: List[String]) extends Driver {
  import tpd._
  import InteractiveDriver._

  override def sourcesRequired = false

  private val myInitCtx: Context = {
    val rootCtx = initCtx.fresh.addMode(Mode.ReadPositions).addMode(Mode.Interactive).addMode(Mode.ReadComments)
    rootCtx.setSetting(rootCtx.settings.YretainTrees, true)
    val ctx = setup(settings.toArray, rootCtx)._2
    ctx.initialize()(ctx)
    ctx
  }

  private[this] var myCtx: Context = myInitCtx

  def currentCtx: Context = myCtx

  private val myOpenedFiles = new mutable.LinkedHashMap[URI, SourceFile] {
    override def default(key: URI) = NoSource
  }

  private val myOpenedTrees = new mutable.LinkedHashMap[URI, List[SourceTree]] {
    override def default(key: URI) = Nil
  }

  private val myCompilationUnits = new mutable.LinkedHashMap[URI, CompilationUnit]

  def openedFiles: Map[URI, SourceFile] = myOpenedFiles
  def openedTrees: Map[URI, List[SourceTree]] = myOpenedTrees
  def compilationUnits: Map[URI, CompilationUnit] = myCompilationUnits

  def allTrees(implicit ctx: Context): List[SourceTree] = allTreesContaining("")

  def allTreesContaining(id: String)(implicit ctx: Context): List[SourceTree] = {
    val fromSource = openedTrees.values.flatten.toList
    val fromClassPath = (dirClassPathClasses ++ zipClassPathClasses).flatMap { cls =>
      val className = cls.toTypeName
      List(tree(className, id), tree(className.moduleClassName, id)).flatten
    }
    (fromSource ++ fromClassPath).distinct
  }

  private def tree(className: TypeName, id: String)(implicit ctx: Context): Option[SourceTree] = {
    val clsd = ctx.base.staticRef(className)
    clsd match {
      case clsd: ClassDenotation =>
        clsd.ensureCompleted()
        SourceTree.fromSymbol(clsd.symbol.asClass, id)
      case _ =>
        None
    }
  }

  // Presence of a file with one of these suffixes indicates that the
  // corresponding class has been pickled with TASTY.
  private val tastySuffixes = List(".hasTasty", ".tasty")

  private def classNames(cp: ClassPath, packageName: String): List[String] = {
    def className(classSegments: List[String]) =
      classSegments.mkString(".").stripSuffix(".class")

    val ClassPathEntries(pkgs, classReps) = cp.list(packageName)

    classReps
      .filter((classRep: ClassRepresentation) => classRep.binary match {
        case None =>
          true
        case Some(binFile) =>
          val prefix =
            if (binFile.name.endsWith(".class"))
              binFile.name.stripSuffix(".class")
            else
              null
          prefix != null && {
            binFile match {
              case pf: PlainFile =>
                tastySuffixes.map(suffix => pf.givenPath.parent / (prefix + suffix)).exists(_.exists)
              case _ =>
                sys.error(s"Unhandled file type: $binFile [getClass = ${binFile.getClass}]")
            }
          }
      })
      .map(classRep => (packageName ++ (if (packageName != "") "." else "") ++ classRep.name)).toList ++
    pkgs.flatMap(pkg => classNames(cp, pkg.name))
  }

  // FIXME: All the code doing classpath handling is very fragile and ugly,
  // improving this requires changing the dotty classpath APIs to handle our usecases.
  // We also need something like sbt server-mode to be informed of changes on
  // the classpath.

  private val (zipClassPaths, dirClassPaths) = currentCtx.platform.classPath(currentCtx) match {
    case AggregateClassPath(cps) =>
      val (zipCps, dirCps) = cps.partition(_.isInstanceOf[ZipArchiveFileLookup[_]])
      // This will be wrong if any other subclass of ClassPath is either used,
      // like `JrtClassPath` once we get Java 9 support
      (zipCps.asInstanceOf[Seq[ZipArchiveFileLookup[_]]], dirCps.asInstanceOf[Seq[JFileDirectoryLookup[_]]])
    case _ =>
      (Seq(), Seq())
  }

  // Like in `ZipArchiveFileLookup` we assume that zips are immutable
  private val zipClassPathClasses: Seq[String] = zipClassPaths.flatMap { zipCp =>
    val zipFile = new ZipFile(zipCp.zipFile)

    try {
      for {
        entry <- zipFile.stream.toArray((size: Int) => new Array[ZipEntry](size))
        name = entry.getName
        tastySuffix <- tastySuffixes.find(name.endsWith)
      } yield name.replace("/", ".").stripSuffix(tastySuffix)
    }
    finally zipFile.close()
  }

  // FIXME: classfiles in directories may change at any point, so we retraverse
  // the directories each time, if we knew when classfiles changed (sbt
  // server-mode might help here), we could do cache invalidation instead.
  private def dirClassPathClasses: Seq[String] = {
    val names = new mutable.ListBuffer[String]
    dirClassPaths.foreach { dirCp =>
      val root = dirCp.dir.toPath
      try
        Files.walkFileTree(root, new SimpleFileVisitor[Path] {
          override def visitFile(path: Path, attrs: BasicFileAttributes) = {
            if (!attrs.isDirectory) {
              val name = path.getFileName.toString
              for {
                tastySuffix <- tastySuffixes
                if name.endsWith(tastySuffix)
              } {
                names += root.relativize(path).toString.replace("/", ".").stripSuffix(tastySuffix)
              }
            }
            FileVisitResult.CONTINUE
          }
        })
      catch {
        case _: NoSuchFileException =>
      }
    }
    names.toList
  }

  private def topLevelClassTrees(topTree: Tree, source: SourceFile): List[SourceTree] = {
    val trees = new mutable.ListBuffer[SourceTree]

    def addTrees(tree: Tree): Unit = tree match {
      case PackageDef(_, stats) =>
        stats.foreach(addTrees)
      case tree: TypeDef =>
        trees += SourceTree(tree, source)
      case _ =>
    }
    addTrees(topTree)

    trees.toList
  }

  private val compiler: Compiler = new InteractiveCompiler

  /** Remove attachments and error out completers. The goal is to avoid
   *  having a completer hanging in a typed tree which can capture the context
   *  of a previous run. Note that typed trees can have untyped or partially
   *  typed children if the source contains errors.
   */
  private def cleanup(tree: tpd.Tree)(implicit ctx: Context): Unit = {
    val seen = mutable.Set.empty[tpd.Tree]
    def cleanupTree(tree: tpd.Tree): Unit = {
      seen += tree
      tree.foreachSubTree { t =>
        if (t.symbol.exists && t.hasType) {
          if (!t.symbol.isCompleted) t.symbol.info = UnspecifiedErrorType
          t.symbol.annotations.foreach { annot =>
            /* In some cases annotations are are used on themself (possibly larger cycles).
            *  This is the case with the java.lang.annotation.Target annotation, would end
            *  in an infinite loop while cleaning. The `seen` is added to ensure that those
            *  trees are not cleand twice.
            *  TODO: Find a less expensive way to check for those cycles.
            */
            if (annot.isEvaluated && !seen(annot.tree))
              cleanupTree(annot.tree)
          }
        }
        t.removeAllAttachments()
      }
    }
    cleanupTree(tree)
  }

  private def toSource(uri: URI, sourceCode: String): SourceFile = {
    val virtualFile = new VirtualFile(uri.toString, Paths.get(uri).toString)
    val writer = new BufferedWriter(new OutputStreamWriter(virtualFile.output, "UTF-8"))
    writer.write(sourceCode)
    writer.close()
    new SourceFile(virtualFile, Codec.UTF8)
  }

  def run(uri: URI, sourceCode: String): List[MessageContainer] = run(uri, toSource(uri, sourceCode))

  def run(uri: URI, source: SourceFile): List[MessageContainer] = {
    val previousCtx = myCtx
    try {
      val reporter =
        new StoreReporter(null) with UniqueMessagePositions with HideNonSensicalMessages

      val run = compiler.newRun(myInitCtx.fresh.setReporter(reporter))
      myCtx = run.runContext

      implicit val ctx = myCtx

      myOpenedFiles(uri) = source

      run.compileSources(List(source))
      run.printSummary()
      val unit = ctx.run.units.head
      val t = unit.tpdTree
      cleanup(t)
      myOpenedTrees(uri) = topLevelClassTrees(t, source)
      myCompilationUnits(uri) = unit

      reporter.removeBufferedMessages
    }
    catch {
      case ex: FatalError  =>
        myCtx = previousCtx
        close(uri)
        Nil
    }
  }

  def close(uri: URI): Unit = {
    myOpenedFiles.remove(uri)
    myOpenedTrees.remove(uri)
    myCompilationUnits.remove(uri)
  }
}

object InteractiveDriver {
  def toUri(source: SourceFile) = Paths.get(source.file.path).toUri
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy