sbt.internal.inc.classfile.JavaAnalyze.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of zinc-classfile_2.12 Show documentation
Show all versions of zinc-classfile_2.12 Show documentation
Incremental compiler of Scala
The newest version!
/*
* Zinc - The incremental compiler for Scala.
* Copyright Scala Center, Lightbend, and Mark Harrah
*
* Licensed under Apache License 2.0
* SPDX-License-Identifier: Apache-2.0
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/
package sbt
package internal
package inc
package classfile
import scala.collection.mutable
import mutable.{ ArrayBuffer, Buffer }
import scala.annotation.tailrec
import java.io.File
import java.net.URL
import xsbti.{ VirtualFile, VirtualFileRef }
import xsbti.api.DependencyContext
import xsbti.api.DependencyContext._
import sbt.io.IO
import sbt.util.Logger
import xsbti.compile.Output
import java.nio.file.Path
private[sbt] object JavaAnalyze {
def apply[T](
newClasses: Seq[Path],
sources: Seq[VirtualFile],
log: Logger,
output: Output,
finalJarOutput: Option[Path]
)(
analysis: xsbti.AnalysisCallback,
loader: ClassLoader,
readAPI: (VirtualFileRef, Seq[Class[?]]) => Set[(String, String)]
): Unit = {
val sourceMap = sources
.toSet[VirtualFile]
.groupBy(_.name)
// For performance reasons, precompute these as they are static throughout this analysis
val singleOutputOrNull: Path = output.getSingleOutputAsPath.orElse(null)
val directOutputJarOrNull: Path = JarUtils.getOutputJar(output).getOrElse(null)
val mappedOutputJarOrNull: Path = finalJarOutput.getOrElse(null)
def load(tpe: String, errMsg: => Option[String]): Option[Class[?]] = {
if (tpe.endsWith("module-info")) None
else
try {
Some(Class.forName(tpe, false, loader))
} catch {
case e: Throwable => errMsg.foreach(msg => log.warn(msg + " : " + e.toString)); None
}
}
def remapClassFile(classFile: Path) =
if (directOutputJarOrNull != null && classFile.getFileSystem.provider.getScheme == "jar")
// convert to the class-in-jar path format that zinc uses. we make an assumption here that
// if we've got a jar-based path, it's referring to a class in the output jar.
JarUtils
.ClassInJar(directOutputJarOrNull, classFile.getRoot.relativize(classFile).toString)
.toPath
else if (singleOutputOrNull != null && mappedOutputJarOrNull != null)
resolveFinalClassFile(classFile, singleOutputOrNull, mappedOutputJarOrNull, log)
else
classFile
val sourceToClassFiles = mutable.HashMap[VirtualFile, Buffer[ClassFile]](
sources.map(vf => vf -> new ArrayBuffer[ClassFile]): _*
)
val binaryClassNameToLoadedClass = new mutable.HashMap[String, Class[?]]
val classfilesCache = mutable.Map.empty[String, Path]
// parse class files and assign classes to sources. This must be done before dependencies, since the information comes
// as class->class dependencies that must be mapped back to source->class dependencies using the source+class assignment
for {
newClass <- newClasses
classFile = Parser(newClass, log)
_ <- classFile.sourceFile orElse guessSourceName(newClass.getFileName.toString)
source <- guessSourcePath(sourceMap, classFile, log)
binaryClassName = classFile.className
loadedClass <- load(
binaryClassName,
Some("Error reading API from class file: " + binaryClassName)
)
} {
binaryClassNameToLoadedClass.update(binaryClassName, loadedClass)
val srcClassName = loadEnclosingClass(loadedClass)
val finalClassFile: Path = remapClassFile(newClass)
srcClassName match {
case Some(className) =>
analysis.generatedNonLocalClass(source, finalClassFile, binaryClassName, className)
case None => analysis.generatedLocalClass(source, finalClassFile)
}
sourceToClassFiles(source) += classFile
}
// get class to class dependencies and map back to source to class dependencies
for ((source, classFiles) <- sourceToClassFiles) {
analysis.startSource(source)
val loadedClasses = classFiles.map(c => binaryClassNameToLoadedClass(c.className))
// Local classes are either local, anonymous or inner Java classes
val (nonLocalClasses, localClassesOrStale) =
loadedClasses.partition(_.getCanonicalName != null)
// Map local classes to the sources of their enclosing classes
val localClassesToSources = {
val localToSourcesSeq = for {
cls <- localClassesOrStale
sourceOfEnclosing <- loadEnclosingClass(cls)
} yield (cls.getName, sourceOfEnclosing)
localToSourcesSeq.toMap
}
/* Get the mapped source file from a given class name. */
def getMappedSource(className: String): Option[String] = {
val nonLocalSourceName: Option[String] = for {
loadedClass <- binaryClassNameToLoadedClass.get(className)
sourceName <- binaryToSourceName(loadedClass)
} yield sourceName
nonLocalSourceName.orElse(localClassesToSources.get(className))
}
def processDependency(
onBinaryName: String,
context: DependencyContext,
fromBinaryName: String
): Unit = {
def loadFromClassloader(): Option[Path] = {
for {
url <- Option(loader.getResource(classNameToClassFile(onBinaryName)))
file <- urlAsFile(url, log, finalJarOutput)
} yield { classfilesCache(onBinaryName) = file; file }
}
(getMappedSource(fromBinaryName), getMappedSource(onBinaryName)) match {
case (Some(fromClassName), Some(onClassName)) =>
trapAndLog(log) {
analysis.classDependency(onClassName, fromClassName, context)
}
case (Some(fromClassName), None) =>
trapAndLog(log) {
val cachedOrigin = classfilesCache.get(onBinaryName)
for (file <- cachedOrigin.orElse(loadFromClassloader())) {
val binaryFile: Path = remapClassFile(file)
analysis.binaryDependency(
binaryFile,
onBinaryName,
fromClassName,
source,
context
)
}
}
case (None, _) => // It could be a stale class file, ignore
}
}
def processDependencies(
binaryClassNames: Iterable[String],
context: DependencyContext,
fromBinaryClassName: String
): Unit =
binaryClassNames.foreach(binaryClassName =>
processDependency(binaryClassName, context, fromBinaryClassName)
)
// Get all references to types in a given class file (via constant pool)
val typesInSource = classFiles.map(cf => cf.className -> cf.types).toMap
// Process dependencies by member references
typesInSource foreach {
case (binaryClassName, binaryClassNameDeps) =>
processDependencies(binaryClassNameDeps, DependencyByMemberRef, binaryClassName)
}
def readInheritanceDependencies(classes: Seq[Class[?]]) = {
val api = readAPI(source, classes)
// avoid .mapValues(...) because of its viewness (scala/bug#10919)
api.groupBy(_._1).iterator.map { case (k, v) => k -> v.map(_._2) }
}
// Read API of non-local classes and process dependencies by inheritance
val nonLocalInherited: Map[String, Set[String]] =
readInheritanceDependencies(nonLocalClasses.toSeq).toMap
nonLocalInherited foreach {
case (className, inheritanceDeps) =>
processDependencies(inheritanceDeps, DependencyByInheritance, className)
}
// Read API of local classes and process local dependencies by inheritance
val localClasses =
localClassesOrStale.filter(cls => localClassesToSources.contains(cls.getName))
val localInherited: Map[String, Set[String]] =
readInheritanceDependencies(localClasses.toSeq).toMap
localInherited foreach {
case (className, inheritanceDeps) =>
processDependencies(inheritanceDeps, LocalDependencyByInheritance, className)
}
}
}
/**
* When straight-to-jar compilation is enabled on a javac which doesn't support it, classes are compiled to a
* temporary directory because javac cannot compile to jar directly. The paths to class files that can be observed
* here through the file system or class loaders are located in temporary output directory for
* javac. As this output will be eventually included in the output jar (`finalJarOutput`), the
* analysis (products) have to be changed accordingly.
*
* Given `finalJarOutput = Some("/develop/zinc/target/output.jar")` and
* `output = "/develop/zinc/target/output.jar-javac-output"`, this method turns
* `/develop/zinc/target/output.jar-javac-output/sbt/internal/inc/Compile.class`
* into
* `/develop/zinc/target/output.jar!/sbt/internal/inc/Compile.class`
*/
private def resolveFinalClassFile(
realClassFile: Path,
outputDir: Path,
outputJar: Path,
log: Logger
): Path = {
def toFile(p: Path): File = if (p == null) null else p.toFile
IO.relativize(toFile(outputDir), toFile(realClassFile)) match {
case Some(relativeClass) => JarUtils.ClassInJar(outputJar, relativeClass).toPath
case None => realClassFile
}
}
private[this] def urlAsFile(url: URL, log: Logger, finalJarOutput: Option[Path]): Option[Path] =
try urlAsFile(url, finalJarOutput)
catch {
case e: Exception =>
log.warn("Could not convert URL '" + url.toExternalForm + "' to File: " + e.toString)
None
}
private def urlAsFile(url: URL, finalJarOutput: Option[Path]): Option[Path] = {
IO.urlAsFile(url).map { file =>
val p = file.toPath
// IO.urlAsFile removes the class reference in the jar url, let's add it back.
if (finalJarOutput.exists(_ == p)) {
JarUtils.ClassInJar.fromURL(url, p).toPath
} else {
p
}
}
}
private def trapAndLog(log: Logger)(execute: => Unit): Unit = {
try {
execute
} catch { case e: Throwable => log.trace(e); log.error(e.toString) }
}
private def guessSourceName(name: String) = Some(takeToDollar(trimClassExt(name)))
private def takeToDollar(name: String) = {
val dollar = name.indexOf('$')
if (dollar < 0) name else name.substring(0, dollar)
}
private final val ClassExt = ".class"
private def trimClassExt(name: String) =
if (name.endsWith(ClassExt)) name.substring(0, name.length - ClassExt.length) else name
private def classNameToClassFile(name: String) = name.replace('.', '/') + ClassExt
private def binaryToSourceName(loadedClass: Class[?]): Option[String] =
Option(loadedClass.getCanonicalName)
@tailrec
private def loadEnclosingClass(clazz: Class[?]): Option[String] = {
binaryToSourceName(clazz) match {
case None if clazz.getEnclosingClass != null =>
loadEnclosingClass(clazz.getEnclosingClass)
case other => other
}
}
/*
* given mapping between getName and sources, try to guess
* where the *.class file is coming from.
*/
private def guessSourcePath(
sourceNameMap: Map[String, Set[VirtualFile]],
classFile: ClassFile,
log: Logger
): List[VirtualFile] = {
val classNameParts = classFile.className.split("""\.""")
val pkg = classNameParts.init
val simpleClassName = classNameParts.last
val sourceFileName =
classFile.sourceFile.getOrElse(simpleClassName.takeWhile(_ != '$').mkString("", "", ".java"))
val candidates = findSource(sourceNameMap, pkg.toList, sourceFileName)
candidates match {
case Nil => log.warn("Could not determine source for class " + classFile.className)
case _ :: Nil => ()
case _ =>
log.warn(
"Multiple sources matched for class " + classFile.className + ": " + candidates
.mkString(", ")
)
}
candidates
}
private def findSource(
sourceNameMap: Map[String, Iterable[VirtualFile]],
pkg: List[String],
sourceFileName: String
): List[VirtualFile] = {
@tailrec def refine(
sources: List[(VirtualFile, List[String])],
pkgRev: List[String]
): List[VirtualFile] = {
def make = sources.map(_._1)
if (sources.isEmpty || sources.tail.isEmpty) make
else
pkgRev match {
case Nil => shortest(make)
case x :: xs =>
val retain = sources flatMap {
case (src, `x` :: presRev) => (src, presRev) :: Nil
case _ => Nil
}
refine(retain, xs)
}
}
def shortest(files: List[VirtualFile]): List[VirtualFile] =
if (files.isEmpty) files
else {
val fs = files.groupBy(x => x.names.size)
fs(fs.keys.min)
}
refine(
(sourceNameMap get sourceFileName).toList.flatten map { x =>
(x, x.names.toList.reverse.drop(1))
},
pkg.reverse
)
}
}