diff options
Diffstat (limited to 'bridge/src/test/scala/xsbt')
4 files changed, 506 insertions, 0 deletions
diff --git a/bridge/src/test/scala/xsbt/DependencySpecification.scala b/bridge/src/test/scala/xsbt/DependencySpecification.scala new file mode 100644 index 000000000..60545091b --- /dev/null +++ b/bridge/src/test/scala/xsbt/DependencySpecification.scala @@ -0,0 +1,151 @@ +/** Adapted from https://github.com/sbt/sbt/blob/0.13/compile/interface/src/test/scala/xsbt/DependencySpecification.scala */ +package xsbt + +import org.junit.runner.RunWith +import xsbti.api.ClassLike +import xsbti.api.Def +import xsbt.api.SameAPI +import org.specs2.mutable.Specification +import org.specs2.runner.JUnitRunner + +import ScalaCompilerForUnitTesting.ExtractedSourceDependencies + +@RunWith(classOf[JUnitRunner]) +class DependencySpecification extends Specification { + + "Extracted source dependencies from public members" in { + val sourceDependencies = extractSourceDependenciesPublic + val memberRef = sourceDependencies.memberRef + val inheritance = sourceDependencies.inheritance + memberRef('A) === Set.empty + inheritance('A) === Set.empty + memberRef('B) === Set('A, 'D) + inheritance('B) === Set('D) + memberRef('C) === Set('A) + inheritance('C) === Set.empty + memberRef('D) === Set.empty + inheritance('D) === Set.empty + memberRef('E) === Set.empty + inheritance('E) === Set.empty + memberRef('F) === Set('A, 'B, 'C, 'D, 'E, 'G) + inheritance('F) === Set('A, 'E) + memberRef('H) === Set('B, 'E, 'G) + // aliases and applied type constructors are expanded so we have inheritance dependency on B + inheritance('H) === Set('B, 'E) + } + + "Extracted source dependencies from private members" in { + val sourceDependencies = extractSourceDependenciesPrivate + val memberRef = sourceDependencies.memberRef + val inheritance = sourceDependencies.inheritance + memberRef('A) === Set.empty + inheritance('A) === Set.empty + memberRef('B) === Set.empty + inheritance('B) === Set.empty + memberRef('C) === Set('A) + inheritance('C) === Set('A) + memberRef('D) === Set('B) + inheritance('D) === Set('B) + } + + "Extracted source dependencies with trait as first parent" in { + val sourceDependencies = extractSourceDependenciesTraitAsFirstPatent + val memberRef = sourceDependencies.memberRef + val inheritance = sourceDependencies.inheritance + memberRef('A) === Set.empty + inheritance('A) === Set.empty + memberRef('B) === Set('A) + inheritance('B) === Set('A) + // verify that memberRef captures the oddity described in documentation of `Relations.inheritance` + // we are mainly interested whether dependency on A is captured in `memberRef` relation so + // the invariant that says that memberRef is superset of inheritance relation is preserved + memberRef('C) === Set('A, 'B) + inheritance('C) === Set('A, 'B) + // same as above but indirect (C -> B -> A), note that only A is visible here + memberRef('D) === Set('A, 'C) + inheritance('D) === Set('A, 'C) + } + + /* + "Extracted source dependencies from macro arguments" in { + val sourceDependencies = extractSourceDependenciesFromMacroArgument + val memberRef = sourceDependencies.memberRef + val inheritance = sourceDependencies.inheritance + + memberRef('A) === Set('B, 'C) + inheritance('A) === Set.empty + memberRef('B) === Set.empty + inheritance('B) === Set.empty + memberRef('C) === Set.empty + inheritance('C) === Set.empty + } + */ + + private def extractSourceDependenciesPublic: ExtractedSourceDependencies = { + val srcA = "class A" + val srcB = "class B extends D[A]" + val srcC = """|class C { + | def a: A = null + |}""".stripMargin + val srcD = "class D[T]" + val srcE = "trait E[T]" + val srcF = "trait F extends A with E[D[B]] { self: G.MyC => }" + val srcG = "object G { type T[x] = B ; type MyC = C }" + // T is a type constructor [x]B + // B extends D + // E verifies the core type gets pulled out + val srcH = "trait H extends G.T[Int] with (E[Int] @unchecked)" + + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val sourceDependencies = compilerForTesting.extractDependenciesFromSrcs('A -> srcA, 'B -> srcB, 'C -> srcC, + 'D -> srcD, 'E -> srcE, 'F -> srcF, 'G -> srcG, 'H -> srcH) + sourceDependencies + } + + private def extractSourceDependenciesPrivate: ExtractedSourceDependencies = { + val srcA = "class A" + val srcB = "class B" + val srcC = "class C { private class Inner1 extends A }" + val srcD = "class D { def foo: Unit = { class Inner2 extends B } }" + + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val sourceDependencies = + compilerForTesting.extractDependenciesFromSrcs('A -> srcA, 'B -> srcB, 'C -> srcC, 'D -> srcD) + sourceDependencies + } + + private def extractSourceDependenciesTraitAsFirstPatent: ExtractedSourceDependencies = { + val srcA = "class A" + val srcB = "trait B extends A" + val srcC = "trait C extends B" + val srcD = "class D extends C" + + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val sourceDependencies = + compilerForTesting.extractDependenciesFromSrcs('A -> srcA, 'B -> srcB, 'C -> srcC, 'D -> srcD) + sourceDependencies + } + + /* + private def extractSourceDependenciesFromMacroArgument: ExtractedSourceDependencies = { + val srcA = "class A { println(B.printTree(C.foo)) }" + val srcB = """ + |import scala.language.experimental.macros + |import scala.reflect.macros._ + |object B { + | def printTree(arg: Any) = macro printTreeImpl + | def printTreeImpl(c: Context)(arg: c.Expr[Any]): c.Expr[String] = { + | val argStr = arg.tree.toString + | val literalStr = c.universe.Literal(c.universe.Constant(argStr)) + | c.Expr[String](literalStr) + | } + |}""".stripMargin + val srcC = "object C { val foo = 1 }" + + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val sourceDependencies = + compilerForTesting.extractDependenciesFromSrcs(List(Map('B -> srcB, 'C -> srcC), Map('A -> srcA))) + sourceDependencies + } + */ +} diff --git a/bridge/src/test/scala/xsbt/ExtractAPISpecification.scala b/bridge/src/test/scala/xsbt/ExtractAPISpecification.scala new file mode 100644 index 000000000..f5af67e45 --- /dev/null +++ b/bridge/src/test/scala/xsbt/ExtractAPISpecification.scala @@ -0,0 +1,45 @@ +/** Adapted from https://github.com/sbt/sbt/blob/0.13/compile/interface/src/test/scala/xsbt/ExtractAPISpecification.scala */ +package xsbt + +import org.junit.runner.RunWith +import xsbti.api.ClassLike +import xsbti.api.Def +import xsbt.api.ShowAPI +import org.specs2.mutable.Specification +import org.specs2.runner.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class ExtractAPISpecification extends Specification { + + "Existential types in method signatures" should { + "have stable names" in { stableExistentialNames } + } + + def stableExistentialNames: Boolean = { + def compileAndGetFooMethodApi(src: String): Def = { + val compilerForTesting = new ScalaCompilerForUnitTesting + val sourceApi = compilerForTesting.extractApiFromSrc(src) + val FooApi = sourceApi.definitions().find(_.name() == "Foo").get.asInstanceOf[ClassLike] + val fooMethodApi = FooApi.structure().declared().find(_.name == "foo").get + fooMethodApi.asInstanceOf[Def] + } + val src1 = """ + |class Box[T] + |class Foo { + | def foo: Box[_] = null + | + }""".stripMargin + val fooMethodApi1 = compileAndGetFooMethodApi(src1) + val src2 = """ + |class Box[T] + |class Foo { + | def bar: Box[_] = null + | def foo: Box[_] = null + | + }""".stripMargin + val fooMethodApi2 = compileAndGetFooMethodApi(src2) + fooMethodApi1 == fooMethodApi2 + // Fails because xsbt.api is compiled with Scala 2.10 + // SameAPI.apply(fooMethodApi1, fooMethodApi2) + } +} diff --git a/bridge/src/test/scala/xsbt/ExtractUsedNamesSpecification.scala b/bridge/src/test/scala/xsbt/ExtractUsedNamesSpecification.scala new file mode 100644 index 000000000..ed463a3e6 --- /dev/null +++ b/bridge/src/test/scala/xsbt/ExtractUsedNamesSpecification.scala @@ -0,0 +1,119 @@ +/** Adapted from https://github.com/sbt/sbt/blob/0.13/compile/interface/src/test/scala/xsbt/ExtractUsedNamesSpecification.scala */ +package xsbt + +import org.junit.runner.RunWith +import xsbti.api.ClassLike +import xsbti.api.Def +import xsbti.api.Package +import xsbt.api.SameAPI +import org.junit.runners.JUnit4 + +import org.specs2.mutable.Specification + +@RunWith(classOf[JUnit4]) +class ExtractUsedNamesSpecification extends Specification { + + /** + * Standard names that appear in every compilation unit that has any class + * definition. + */ + private val standardNames = Set( + // All class extend Object + "Object", + // All class have a default constructor called <init> + "<init>", + // the return type of the default constructor is Unit + "Unit" + ) + + "imported name" in { + val src = """ + |package a { class A } + |package b { + | import a.{A => A2} + |}""".stripMargin + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val usedNames = compilerForTesting.extractUsedNamesFromSrc(src) + val expectedNames = standardNames ++ Set("a", "A", "A2", "b") + usedNames === expectedNames + } + + // test covers https://github.com/gkossakowski/sbt/issues/6 + "names in type tree" in { + val srcA = """| + |package a { + | class A { + | class C { class D } + | } + | class B[T] + | class BB + |}""".stripMargin + val srcB = """| + |package b { + | abstract class X { + | def foo: a.A#C#D + | def bar: a.B[a.BB] + | } + |}""".stripMargin + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val usedNames = compilerForTesting.extractUsedNamesFromSrc(srcA, srcB) + // DOTTY: unlike the scalac sbt phase, this does not contain "X", I believe this is safe + // TODO: report issue against sbt suggesting that they do the same + val expectedNames = standardNames ++ Set("a", "A", "B", "C", "D", "b", "BB") + usedNames === expectedNames + } + + // test for https://github.com/gkossakowski/sbt/issues/5 + "symbolic names" in { + val srcA = """| + |class A { + | def `=`: Int = 3 + |}""".stripMargin + val srcB = """| + |class B { + | def foo(a: A) = a.`=` + |}""".stripMargin + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val usedNames = compilerForTesting.extractUsedNamesFromSrc(srcA, srcB) + // DOTTY TODO: "Int" is not actually used, but we collect it because + // it's the inferred return type so it appears in a TypeTree + // We could avoid this by checking if the untyped tree has a return type + // but is it worth it? Revisit this after https://github.com/sbt/sbt/issues/1104 + // has landed. + val expectedNames = standardNames ++ Set("A", "a", "$eq", "Int") + usedNames === expectedNames + } + + // test for https://github.com/gkossakowski/sbt/issues/3 + "used names from the same compilation unit" in { + val src = "class A { def foo: Int = 0; def bar: Int = foo }" + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val usedNames = compilerForTesting.extractUsedNamesFromSrc(src) + val expectedNames = standardNames ++ Set("A", "foo", "Int") + usedNames === expectedNames + } + + // pending test for https://issues.scala-lang.org/browse/SI-7173 + "names of constants" in { + val src = "class A { final val foo = 12; def bar: Int = foo }" + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val usedNames = compilerForTesting.extractUsedNamesFromSrc(src) + val expectedNames = standardNames ++ Set("A", "foo", "Int") + usedNames === expectedNames + } + + // pending test for https://github.com/gkossakowski/sbt/issues/4 + // TODO: we should fix it by having special treatment of `selectDynamic` and `applyDynamic` calls + "names from method calls on Dynamic" in { + val srcA = """|import scala.language.dynamics + |class A extends Dynamic { + | def selectDynamic(name: String): Int = name.length + |}""".stripMargin + val srcB = "class B { def foo(a: A): Int = a.bla }" + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val usedNames = compilerForTesting.extractUsedNamesFromSrc(srcA, srcB) + val expectedNames = standardNames ++ Set("B", "A", "a", "Int", "selectDynamic", "bla") + usedNames === expectedNames + }.pendingUntilFixed("Call to Dynamic is desugared in type checker so Select nodes is turned into string literal.") + +} diff --git a/bridge/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala b/bridge/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala new file mode 100644 index 000000000..db037effe --- /dev/null +++ b/bridge/src/test/scala/xsbt/ScalaCompilerForUnitTesting.scala @@ -0,0 +1,191 @@ +/** 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.withTemporaryDirectory +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 = false) { + + /** + * 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) + } + + 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) + 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 sequence of temporary files corresponding to passed snippets and analysis + * callback is returned as a result. + */ + private def compileSrcs(groupedSrcs: List[List[String]]): (Seq[File], TestCallback) = { + withTemporaryDirectory { temp => + val analysisCallback = new TestCallback(nameHashing) + val classesDir = new File(temp, "classes") + classesDir.mkdir() + + // val (compiler, ctx) = prepareCompiler(classesDir, analysisCallback, classesDir.toString) + + val files = for ((compilationUnit, unitId) <- groupedSrcs.zipWithIndex) yield { + val (compiler, ctx) = 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)) + } + + 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] + object output extends SingleOutput { + def outputDirectory: File = outputDir + override def toString = s"SingleOutput($outputDirectory)" + } + + import dotty.tools.dotc._ + 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"), 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]]) +} |