/** Adapted from https://github.com/sbt/sbt/blob/0.13/compile/interface/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala */ package xsbt import xsbti.compile.SingleOutput import java.io.File import _root_.scala.tools.nsc.reporters.ConsoleReporter import _root_.scala.tools.nsc.Settings import xsbti._ import xsbti.api.SourceAPI import sbt.IO._ import xsbti.api.ClassLike import xsbti.api.Definition import xsbti.api.Def import xsbt.api.SameAPI import sbt.ConsoleLogger import xsbti.DependencyContext._ import ScalaCompilerForUnitTesting.ExtractedSourceDependencies /** * Provides common functionality needed for unit tests that require compiling * source code using Scala compiler. */ class ScalaCompilerForUnitTesting(nameHashing: Boolean, includeSynthToNameHashing: Boolean = false) { import scala.language.reflectiveCalls /** * Compiles given source code using Scala compiler and returns API representation * extracted by ExtractAPI class. */ def extractApiFromSrc(src: String): SourceAPI = { val (Seq(tempSrcFile), analysisCallback) = compileSrcs(src) analysisCallback.apis(tempSrcFile) } /** * Compiles given source code using Scala compiler and returns API representation * extracted by ExtractAPI class. */ def extractApisFromSrcs(reuseCompilerInstance: Boolean)(srcs: List[String]*): Seq[SourceAPI] = { val (tempSrcFiles, analysisCallback) = compileSrcs(srcs.toList, reuseCompilerInstance) tempSrcFiles.map(analysisCallback.apis) } def extractUsedNamesFromSrc(src: String): Set[String] = { val (Seq(tempSrcFile), analysisCallback) = compileSrcs(src) analysisCallback.usedNames(tempSrcFile) } /** * Extract used names from src provided as the second argument. * * The purpose of the first argument is to define names that the second * source is going to refer to. Both files are compiled in the same compiler * Run but only names used in the second src file are returned. */ def extractUsedNamesFromSrc(definitionSrc: String, actualSrc: String): Set[String] = { // we drop temp src file corresponding to the definition src file val (Seq(_, tempSrcFile), analysisCallback) = compileSrcs(definitionSrc, actualSrc) analysisCallback.usedNames(tempSrcFile) } /** * Compiles given source code snippets (passed as Strings) using Scala compiler and returns extracted * dependencies between snippets. Source code snippets are identified by symbols. Each symbol should * be associated with one snippet only. * * Snippets can be grouped to be compiled together in the same compiler run. This is * useful to compile macros, which cannot be used in the same compilation run that * defines them. * * Symbols are used to express extracted dependencies between source code snippets. This way we have * file system-independent way of testing dependencies between source code "files". */ def extractDependenciesFromSrcs(srcs: List[Map[Symbol, String]]): ExtractedSourceDependencies = { val rawGroupedSrcs = srcs.map(_.values.toList) val symbols = srcs.flatMap(_.keys) val (tempSrcFiles, testCallback) = compileSrcs(rawGroupedSrcs, reuseCompilerInstance = true) val fileToSymbol = (tempSrcFiles zip symbols).toMap val memberRefFileDeps = testCallback.sourceDependencies collect { // false indicates that those dependencies are not introduced by inheritance case (target, src, DependencyByMemberRef) => (src, target) } val inheritanceFileDeps = testCallback.sourceDependencies collect { // true indicates that those dependencies are introduced by inheritance case (target, src, DependencyByInheritance) => (src, target) } def toSymbols(src: File, target: File): (Symbol, Symbol) = (fileToSymbol(src), fileToSymbol(target)) val memberRefDeps = memberRefFileDeps map { case (src, target) => toSymbols(src, target) } val inheritanceDeps = inheritanceFileDeps map { case (src, target) => toSymbols(src, target) } def pairsToMultiMap[A, B](pairs: Seq[(A, B)]): Map[A, Set[B]] = { import scala.collection.mutable.{ HashMap, MultiMap } val emptyMultiMap = new HashMap[A, scala.collection.mutable.Set[B]] with MultiMap[A, B] val multiMap = pairs.foldLeft(emptyMultiMap) { case (acc, (key, value)) => acc.addBinding(key, value) } // convert all collections to immutable variants multiMap.toMap.mapValues(_.toSet).withDefaultValue(Set.empty) } ExtractedSourceDependencies(pairsToMultiMap(memberRefDeps), pairsToMultiMap(inheritanceDeps)) } def extractDependenciesFromSrcs(srcs: (Symbol, String)*): ExtractedSourceDependencies = { val symbols = srcs.map(_._1) assert(symbols.distinct.size == symbols.size, s"Duplicate symbols for srcs detected: $symbols") extractDependenciesFromSrcs(List(srcs.toMap)) } /** * Compiles given source code snippets written to temporary files. Each snippet is * written to a separate temporary file. * * Snippets can be grouped to be compiled together in the same compiler run. This is * useful to compile macros, which cannot be used in the same compilation run that * defines them. * * The `reuseCompilerInstance` parameter controls whether the same Scala compiler instance * is reused between compiling source groups. Separate compiler instances can be used to * test stability of API representation (with respect to pickling) or to test handling of * binary dependencies. * * The sequence of temporary files corresponding to passed snippets and analysis * callback is returned as a result. */ private def compileSrcs(groupedSrcs: List[List[String]], reuseCompilerInstance: Boolean): (Seq[File], TestCallback) = { // withTemporaryDirectory { temp => { val temp = createTemporaryDirectory val analysisCallback = new TestCallback(nameHashing, includeSynthToNameHashing) val classesDir = new File(temp, "classes") classesDir.mkdir() lazy val commonCompilerInstanceAndCtx = prepareCompiler(classesDir, analysisCallback, classesDir.toString) val files = for ((compilationUnit, unitId) <- groupedSrcs.zipWithIndex) yield { // use a separate instance of the compiler for each group of sources to // have an ability to test for bugs in instability between source and pickled // representation of types val (compiler, ctx) = if (reuseCompilerInstance) commonCompilerInstanceAndCtx else prepareCompiler(classesDir, analysisCallback, classesDir.toString) val run = compiler.newRun(ctx) val srcFiles = compilationUnit.toSeq.zipWithIndex map { case (src, i) => val fileName = s"Test-$unitId-$i.scala" prepareSrcFile(temp, fileName, src) } val srcFilePaths = srcFiles.map(srcFile => srcFile.getAbsolutePath).toList run.compile(srcFilePaths) // srcFilePaths.foreach(f => new File(f).delete) srcFiles } (files.flatten.toSeq, analysisCallback) } } private def compileSrcs(srcs: String*): (Seq[File], TestCallback) = { compileSrcs(List(srcs.toList), reuseCompilerInstance = true) } private def prepareSrcFile(baseDir: File, fileName: String, src: String): File = { val srcFile = new File(baseDir, fileName) sbt.IO.write(srcFile, src) srcFile } private def prepareCompiler(outputDir: File, analysisCallback: AnalysisCallback, classpath: String = ".") = { val args = Array.empty[String] import dotty.tools.dotc.{Compiler, Driver} import dotty.tools.dotc.core.Contexts._ val driver = new Driver { protected def newCompiler(implicit ctx: Context): Compiler = new Compiler override protected def sourcesRequired = false def getCompiler(args: Array[String], rootCtx: Context) = { val (fileNames, ctx) = setup(args, rootCtx) (newCompiler(ctx), ctx) } } val ctx = (new ContextBase).initialCtx.fresh.setSbtCallback(analysisCallback) driver.getCompiler(Array("-classpath", classpath, "-usejavacp", "-d", outputDir.getAbsolutePath), ctx) } private object ConsoleReporter extends Reporter { def reset(): Unit = () def hasErrors: Boolean = false def hasWarnings: Boolean = false def printWarnings(): Unit = () def problems: Array[Problem] = Array.empty def log(pos: Position, msg: String, sev: Severity): Unit = println(msg) def comment(pos: Position, msg: String): Unit = () def printSummary(): Unit = () } } object ScalaCompilerForUnitTesting { case class ExtractedSourceDependencies(memberRef: Map[Symbol, Set[Symbol]], inheritance: Map[Symbol, Set[Symbol]]) }