diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeb6fa0d4..e4020982b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,10 @@ +name: ci + on: pull_request: push: branches: ['main'] - tags: ['v[0-9]'] + tags: ['[0-9]'] jobs: build: @@ -17,7 +19,7 @@ jobs: with: java-version: ${{ matrix.java }} - uses: coursier/cache-action@v6 - - run: "sbt test mimaReportBinaryIssues 'set sbtplugin/scriptedSbt := \"1.2.8\"' 'scripted sbt-mima-plugin/minimal' IntegrationTest/test" + - run: "sbt test mimaReportBinaryIssues 'set sbtplugin/scriptedSbt := \"1.2.8\"' 'scripted sbt-mima-plugin/minimal'" testFunctional: needs: build strategy: @@ -42,3 +44,11 @@ jobs: - uses: olafurpg/setup-scala@v13 - uses: coursier/cache-action@v6 - run: sbt "scripted sbt-mima-plugin/*${{ matrix.scripted }}" + testIntegration: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: olafurpg/setup-scala@v13 + - uses: coursier/cache-action@v6 + - run: sbt IntegrationTest/test diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/AnnotInfo.scala b/core/src/main/scala/com/typesafe/tools/mima/core/AnnotInfo.scala new file mode 100644 index 000000000..0ca7de402 --- /dev/null +++ b/core/src/main/scala/com/typesafe/tools/mima/core/AnnotInfo.scala @@ -0,0 +1,3 @@ +package com.typesafe.tools.mima.core + +private[mima] final case class AnnotInfo(name: String) diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/BufferReader.scala b/core/src/main/scala/com/typesafe/tools/mima/core/BufferReader.scala index ad22af924..582d5830a 100644 --- a/core/src/main/scala/com/typesafe/tools/mima/core/BufferReader.scala +++ b/core/src/main/scala/com/typesafe/tools/mima/core/BufferReader.scala @@ -5,7 +5,10 @@ import java.lang.Double.longBitsToDouble import java.nio.charset.StandardCharsets /** Reads and interprets the bytes in a class file byte-buffer. */ -private[core] sealed class BytesReader(buf: Array[Byte]) { +private[core] sealed abstract class BytesReader(buf: Array[Byte]) { + def pos: Int + def file: AbsFile + final def getByte(idx: Int): Byte = buf(idx) final def getChar(idx: Int): Char = (((buf(idx) & 0xff) << 8) + (buf(idx + 1) & 0xff)).toChar @@ -17,15 +20,17 @@ private[core] sealed class BytesReader(buf: Array[Byte]) { final def getFloat(idx: Int): Float = intBitsToFloat(getInt(idx)) final def getDouble(idx: Int): Double = longBitsToDouble(getLong(idx)) +//final def getString(idx: Int, len: Int): String = new java.io.DataInputStream(new java.io.ByteArrayInputStream(buf, idx, len)).readUTF final def getString(idx: Int, len: Int): String = new String(buf, idx, len, StandardCharsets.UTF_8) final def getBytes(idx: Int, bytes: Array[Byte]): Unit = System.arraycopy(buf, idx, bytes, 0, bytes.length) } /** A BytesReader which also holds a mutable pointer to where it will read next. */ -private[core] final class BufferReader(buf: Array[Byte], val path: String) extends BytesReader(buf) { +private[core] final class BufferReader(val file: AbsFile) extends BytesReader(file.toByteArray) { /** the buffer pointer */ var bp: Int = 0 + def pos = bp def nextByte: Byte = { val b = getByte(bp); bp += 1; b } def nextChar: Char = { val c = getChar(bp); bp += 2; c } // Char = unsigned 2-bytes, aka u16 @@ -35,4 +40,10 @@ private[core] final class BufferReader(buf: Array[Byte], val path: String) exten def acceptChar(exp: Char, ctx: => String = "") = { val obt = nextChar; assert(obt == exp, s"Expected $exp, obtained $obt$ctx"); obt } def skip(n: Int): Unit = bp += n + + def atIndex[T](i: Int)(body: => T): T = { + val saved = bp + bp = i + try body finally bp = saved + } } diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/ClassInfo.scala b/core/src/main/scala/com/typesafe/tools/mima/core/ClassInfo.scala index cbc7584ca..2b3310e2c 100644 --- a/core/src/main/scala/com/typesafe/tools/mima/core/ClassInfo.scala +++ b/core/src/main/scala/com/typesafe/tools/mima/core/ClassInfo.scala @@ -59,6 +59,7 @@ private[mima] sealed abstract class ClassInfo(val owner: PackageInfo) extends In final var _methods: Members[MethodInfo] = NoMembers final var _flags: Int = 0 final var _scopedPrivate: Boolean = false + final var _annotations: List[AnnotInfo] = Nil final var _implClass: ClassInfo = NoClass final var _moduleClass: ClassInfo = NoClass final var _module: ClassInfo = NoClass @@ -75,6 +76,7 @@ private[mima] sealed abstract class ClassInfo(val owner: PackageInfo) extends In final def methods: Members[MethodInfo] = afterLoading(_methods) final def flags: Int = afterLoading(_flags) final def isScopedPrivate: Boolean = afterLoading(_scopedPrivate) + final def annotations: List[AnnotInfo] = afterLoading(_annotations) final def implClass: ClassInfo = { owner.setImplClasses; _implClass } // returns NoClass if this is not a trait final def moduleClass: ClassInfo = { owner.setModules; if (_moduleClass == NoClass) this else _moduleClass } final def module: ClassInfo = { owner.setModules; if (_module == NoClass) this else _module } diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/ClassPath.scala b/core/src/main/scala/com/typesafe/tools/mima/core/ClassPath.scala index c6825ed5b..e648c42c2 100644 --- a/core/src/main/scala/com/typesafe/tools/mima/core/ClassPath.scala +++ b/core/src/main/scala/com/typesafe/tools/mima/core/ClassPath.scala @@ -4,19 +4,13 @@ import java.nio.file._ import scala.collection.JavaConverters._ -private object AbsFile { - def apply(p: Path): AbsFile = { - val name = p.getFileName.toString.stripPrefix("/") - val path = p.toString.stripPrefix("/") - AbsFile(name)(path, () => Files.readAllBytes(p)) - } -} - -private[core] final case class AbsFile(name: String)(path: String, bytes: () => Array[Byte]) { +private[core] final case class AbsFile(name: String)(val jpath: Path) { // Not defined as a simple wrapper of java.nio.file.Path, because Path#equals uses its FileSystem, // differently to scala-reflect's AbstractFile, which breaks things like `distinct`. + def this(path: Path) = this(path.getFileName.toString.stripPrefix("/"))(path) - def toByteArray = bytes() + val path = jpath.toString.stripPrefix("/") + def toByteArray = Files.readAllBytes(jpath) override def toString = path } @@ -74,7 +68,7 @@ private[mima] object ClassPath { private final case class JrtCp(fs: FileSystem) extends ClassPath { def packages(pkg: String) = packageToModules.keys.toStream.filter(pkgContains(pkg, _)).sorted - def classes(pkg: String) = packageToModules(pkg).flatMap(pkgClasses(_, pkg)).sortBy(_.toString).map(AbsFile(_)) + def classes(pkg: String) = packageToModules(pkg).flatMap(pkgClasses(_, pkg)).sortBy(_.toString).map(new AbsFile(_)) def asClassPathString = fs.toString private val packageToModules = listDir(fs.getPath("/packages")) @@ -84,7 +78,7 @@ private[mima] object ClassPath { private final case class PathCp(src: Path)(root: Path) extends ClassPath { def packages(pkg: String) = listDir(pkgResolve(root, pkg)).filter(isPackage).map(pkgEntry(pkg, _)) - def classes(pkg: String) = listDir(pkgResolve(root, pkg)).filter(isClass).map(AbsFile(_)) + def classes(pkg: String) = listDir(pkgResolve(root, pkg)).filter(isClass).map(new AbsFile(_)) def asClassPathString = src.toString } diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/ClassfileParser.scala b/core/src/main/scala/com/typesafe/tools/mima/core/ClassfileParser.scala index 107d3b7ef..d1c952139 100644 --- a/core/src/main/scala/com/typesafe/tools/mima/core/ClassfileParser.scala +++ b/core/src/main/scala/com/typesafe/tools/mima/core/ClassfileParser.scala @@ -1,6 +1,7 @@ package com.typesafe.tools.mima.core import java.io.IOException +import java.nio.file.Files import ClassfileConstants._ @@ -56,14 +57,11 @@ final class ClassfileParser private (in: BufferReader, pool: ConstantPool) { case ScalaSignatureATTR => isScala = true case EnclosingMethodATTR => clazz._isLocalClass = true case InnerClassesATTR => clazz._innerClasses = parseInnerClasses(clazz) + case TASTYATTR => parseTasty(clazz) case _ => } - if (isScala) { - val end = in.bp - in.bp = runtimeAnnotStart - parsePickle(clazz) - in.bp = end - } + if (isScala) + in.atIndex(runtimeAnnotStart)(parsePickle(clazz)) } private def parseMemberAttributes(member: MemberInfo) = { @@ -100,7 +98,7 @@ final class ClassfileParser private (in: BufferReader, pool: ConstantPool) { private def parsePickle(clazz: ClassInfo) = { def parseScalaSigBytes() = { in.acceptByte(STRING_TAG, s" for ${clazz.description}") - pool.getBytes(in.nextChar, in.bp) + pool.getBytes(in.nextChar) } def parseScalaLongSigBytes() = { @@ -109,7 +107,7 @@ final class ClassfileParser private (in: BufferReader, pool: ConstantPool) { in.acceptByte(STRING_TAG, s" for ${clazz.description}") in.nextChar.toInt } - pool.getBytes(entries.toList, in.bp) + pool.getBytes(entries.toList) } def checkScalaSigAnnotArg() = { @@ -142,7 +140,14 @@ final class ClassfileParser private (in: BufferReader, pool: ConstantPool) { } i += 1 } - MimaUnpickler.unpickleClass(new PickleBuffer(bytes), clazz, in.path) + MimaUnpickler.unpickleClass(new PickleBuffer(bytes), clazz, in.file.path) + } + + private def parseTasty(clazz: ClassInfo) = { + // TODO: sanity check UUIDs + val tpath = pool.file.jpath.resolveSibling(pool.file.name.stripSuffix(".class") + ".tasty") + val bytes = Files.readAllBytes(tpath) + TastyUnpickler.unpickleClass(new TastyReader(bytes), clazz, tpath.toString) } private final val ScalaSignatureAnnot = "Lscala.reflect.ScalaSignature;" @@ -154,12 +159,13 @@ final class ClassfileParser private (in: BufferReader, pool: ConstantPool) { private final val RuntimeAnnotationATTR = "RuntimeVisibleAnnotations" private final val ScalaSignatureATTR = "ScalaSig" private final val SignatureATTR = "Signature" + private final val TASTYATTR = "TASTY" } object ClassfileParser { private[core] def parseInPlace(clazz: ClassInfo, file: AbsFile): Unit = { - val in = new BufferReader(file.toByteArray, file.toString) - parseHeader(in, file.toString) + val in = new BufferReader(file) + parseHeader(in, file) val pool = ConstantPool.parseNew(clazz.owner.definitions, in) val parser = new ClassfileParser(in, pool) parser.parseClass(clazz) @@ -176,7 +182,7 @@ object ClassfileParser { def isSynthetic(flags: Int) = 0 != (flags & JAVA_ACC_SYNTHETIC) def isAnnotation(flags: Int) = 0 != (flags & JAVA_ACC_ANNOTATION) - private def parseHeader(in: BufferReader, file: String) = { + private def parseHeader(in: BufferReader, file: AbsFile) = { val magic = in.nextInt if (magic != JAVA_MAGIC) throw new IOException( diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/ConstantPool.scala b/core/src/main/scala/com/typesafe/tools/mima/core/ConstantPool.scala index 298a8126c..99cc0c91f 100644 --- a/core/src/main/scala/com/typesafe/tools/mima/core/ConstantPool.scala +++ b/core/src/main/scala/com/typesafe/tools/mima/core/ConstantPool.scala @@ -12,18 +12,15 @@ private[core] object ConstantPool { starts(i) = in.bp i += 1 (in.nextByte.toInt: @switch) match { - case CONSTANT_UTF8 | CONSTANT_UNICODE => in.skip(in.nextChar) - case CONSTANT_CLASS | CONSTANT_STRING - | CONSTANT_METHODTYPE - | CONSTANT_MODULE | CONSTANT_PACKAGE => in.skip(2) - case CONSTANT_METHODHANDLE => in.skip(3) - case CONSTANT_INTEGER | CONSTANT_FLOAT - | CONSTANT_FIELDREF | CONSTANT_METHODREF - | CONSTANT_INTFMETHODREF - | CONSTANT_NAMEANDTYPE - | CONSTANT_INVOKEDYNAMIC => in.skip(4) - case CONSTANT_LONG | CONSTANT_DOUBLE => in.skip(8); i += 1 - case tag => errorBadTag(tag, in.bp - 1) + case CONSTANT_UTF8 | CONSTANT_UNICODE => in.skip(in.nextChar) + case CONSTANT_CLASS | CONSTANT_STRING | CONSTANT_METHODTYPE => in.skip(2) + case CONSTANT_MODULE | CONSTANT_PACKAGE => in.skip(2) + case CONSTANT_METHODHANDLE => in.skip(3) + case CONSTANT_FIELDREF | CONSTANT_METHODREF | CONSTANT_INTFMETHODREF => in.skip(4) + case CONSTANT_NAMEANDTYPE | CONSTANT_INTEGER | CONSTANT_FLOAT => in.skip(4) + case CONSTANT_INVOKEDYNAMIC => in.skip(4) + case CONSTANT_LONG | CONSTANT_DOUBLE => in.skip(8); i += 1 + case tag => errorBadTag(tag, in.bp - 1) } } new ConstantPool(definitions, in, starts) @@ -43,6 +40,8 @@ private[core] final class ConstantPool private (definitions: Definitions, in: BytesReader, starts: Array[Int]) { import ConstantPool._, ClassInfo.ObjectClass + def file: AbsFile = in.file + private val length = starts.length private val values = new Array[AnyRef](length) private val internalized = new Array[String](length) @@ -76,35 +75,30 @@ final class ConstantPool private (definitions: Definitions, in: BytesReader, sta def getSuperClass(index: Int): ClassInfo = if (index == 0) ObjectClass else getClassInfo(index) - def getBytes(index: Int, pos: Int): Array[Byte] = { - if (index <= 0 || length <= index) errorBadIndex(index, pos: Int) + def getBytes(index: Int): Array[Byte] = { + if (index <= 0 || length <= index) errorBadIndex(index, in.pos) else values(index) match { case xs: Array[Byte] => xs - case _ => - val start = firstExpecting(index, CONSTANT_UTF8) - val len = in.getChar(start).toInt - val bytes = new Array[Byte](len) - in.getBytes(start + 2, bytes) - recordAtIndex(getSubArray(bytes), index) + case _ => recordAtIndex(getSubArray(readBytes(index)), index) } } - def getBytes(indices: List[Int], pos: Int): Array[Byte] = { - val head = indices.head - values(head) match { + def getBytes(indices: List[Int]): Array[Byte] = { + for (index <- indices) if (index <= 0 || length <= index) errorBadIndex(index, in.pos) + val index = indices.head + values(index) match { case xs: Array[Byte] => xs - case _ => - val arr: Array[Byte] = indices.toArray.flatMap { index => - if (index <= 0 || length <= index) errorBadIndex(index, pos) - val start = firstExpecting(index, CONSTANT_UTF8) - val result = new Array[Byte](in.getChar(start).toInt) - in.getBytes(start + 2, result) - result - } - recordAtIndex(getSubArray(arr), head) + case _ => recordAtIndex(getSubArray(indices.flatMap(readBytes).toArray), index) } } + private def readBytes(index: Int) = { + val start = firstExpecting(index, CONSTANT_UTF8) + val bytes = new Array[Byte](in.getChar(start).toInt) + in.getBytes(start + 2, bytes) + bytes + } + private def indexedOrUpdate[A <: AnyRef, R <: A](arr: Array[A], index: Int)(mk: => R): R = { if (index <= 0 || index >= length) throw new RuntimeException(s"bad constant pool index: $index, length: $length") @@ -120,7 +114,7 @@ final class ConstantPool private (definitions: Definitions, in: BytesReader, sta val start = starts(index) val tag = in.getByte(start).toInt if (tag == expectedTag) start + 1 - else ConstantPool.errorBadTag(tag, start) + else errorBadTag(tag, start) } private def getSubArray(bytes: Array[Byte]): Array[Byte] = { diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/MemberInfo.scala b/core/src/main/scala/com/typesafe/tools/mima/core/MemberInfo.scala index 420a6e7fd..6a17ddfb2 100644 --- a/core/src/main/scala/com/typesafe/tools/mima/core/MemberInfo.scala +++ b/core/src/main/scala/com/typesafe/tools/mima/core/MemberInfo.scala @@ -36,6 +36,9 @@ private[mima] final class FieldInfo(owner: ClassInfo, bytecodeName: String, flag private[mima] final class MethodInfo(owner: ClassInfo, bytecodeName: String, flags: Int, descriptor: String) extends MemberInfo(owner, bytecodeName, flags, descriptor) { + final var _annotations: List[AnnotInfo] = Nil + final def annotations: List[AnnotInfo] = _annotations + def methodString: String = s"$shortMethodString in ${owner.classString}" def shortMethodString: String = { val prefix = if (hasSyntheticName) if (isExtensionMethod) "extension " else "synthetic " else "" diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/MimaUnpickler.scala b/core/src/main/scala/com/typesafe/tools/mima/core/MimaUnpickler.scala index 2e0af1c2e..b20ffb52c 100644 --- a/core/src/main/scala/com/typesafe/tools/mima/core/MimaUnpickler.scala +++ b/core/src/main/scala/com/typesafe/tools/mima/core/MimaUnpickler.scala @@ -6,37 +6,150 @@ object MimaUnpickler { def unpickleClass(buf: PickleBuffer, clazz: ClassInfo, path: String): Unit = { if (buf.bytes.length == 0) return + val doPrint = false + //val doPrint = path.contains("v1") && !path.contains("exclude.class") + if (doPrint) { + println(s"unpickling $path") + PicklePrinter.printPickle(buf) + } + buf.readNat(); buf.readNat() // major, minor version val index = buf.createIndex - val classes = new Array[ClassInfo](index.length) - val syms = new Array[SymbolInfo](index.length) - def nnSyms = syms.iterator.zipWithIndex.filter(_._1 != null) - def defnSyms = nnSyms.filter { case (sym, _) => sym.tag == CLASSsym || sym.tag == MODULEsym } - def methSyms = nnSyms.filter { case (sym, _) => sym.tag == VALsym } - val entries = PickleEntries(buf.toIndexedSeq.zipWithIndex.map { case ((tag, data), num) => - PickleEntry(num, index(num), tag, data) - }) - - // SymbolInfo = name_Ref owner_Ref flags_LongNat [privateWithin_Ref] info_Ref - def readSymbol(): SymbolInfo = { + val entries = new Array[Entry](index.length) + val classes = new scala.collection.mutable.HashMap[SymInfo, ClassInfo] + def nnSyms = entries.iterator.collect { case s: SymbolInfo => s } + def defnSyms = nnSyms.filter(sym => sym.tag == CLASSsym || sym.tag == MODULEsym) + def methSyms = nnSyms.filter(sym => sym.tag == VALsym) + + def until[T](end: Int, op: () => T): List[T] = + if (buf.readIndex == end) Nil else op() :: until(end, op) + + def at[T <: Entry](num: Int, op: () => T): T = { + var r = entries(num) + if (r eq null) { + buf.atIndex(index(num)) { + r = op() + assert(entries(num) == null, entries(num)) + r match { + case _: UnknownType => + case _: UnknownEntry => + case _ => entries(num) = r + } + } + } + r.asInstanceOf[T] + } + + def readEnd(): Int = buf.readNat() + buf.readIndex + def readNameRef(): Name = at(buf.readNat(), readName) + def readSymRef(): SymInfo = at(buf.readNat(), () => readSym(buf.readByte())) + def readSymbolRef(): SymbolInfo = at(buf.readNat(), () => readSymbol(buf.readByte())) + def readTypeRef(): TypeInfo = at(buf.readNat(), () => readType()) + + def readName(): Name = { val tag = buf.readByte() - val end = buf.readNat() + buf.readIndex - val name = entries.nameAt(buf.readNat()) - val owner = buf.readNat() + val end = readEnd() + val bytes = buf.bytes.slice(buf.readIndex, end) + tag match { + case TERMname => TermName(new String(bytes, "UTF-8")) + case TYPEname => TypeName(new String(bytes, "UTF-8")) + case tag => TypeName(s"?(tag=$tag)") + } + } + + def readSym(tag: Int): SymInfo = tag match { + case NONEsym => readSymbol(tag) + case TYPEsym => readSymbol(tag) + case ALIASsym => readSymbol(tag) + case CLASSsym => readSymbol(tag) // CLASSsym len_Nat SymbolInfo [thistype_Ref] + case MODULEsym => readSymbol(tag) + case VALsym => readSymbol(tag) + case EXTref => readExt(tag) + case EXTMODCLASSref => readExt(tag) + case tag => sys.error(s"Unexpected tag ${tag2string(tag)}") + } + + def readSymbol(tag: Int): SymbolInfo = { + // SymbolInfo = name_Ref owner_Ref flags_LongNat [privateWithin_Ref] info_Ref + val end = readEnd() + if (tag == NONEsym) { + buf.readIndex = readEnd() + return NoSymbol + } + val name = readNameRef() + val owner = readSymRef() val flags = buf.readLongNat() - buf.readNat() // privateWithin or symbol info (compare to end) - val isScopedPrivate = buf.readIndex != end - buf.readIndex = end + val (privateWithin, info) = buf.readNat() match { + case info if buf.readIndex == end => (-1, info) + case privateWithin => (privateWithin, buf.readNat()) + } + if (tag == CLASSsym && buf.readIndex != end) buf.readNat() // thistype_Ref + buf.assertEnd(end) + val isScopedPrivate = privateWithin != -1 SymbolInfo(tag, name, owner, flags, isScopedPrivate) } + def readExt(tag: Int): ExtInfo = { + // EXTref len_Nat name_Ref [owner_Ref] + // EXTMODCLASSref len_Nat name_Ref [owner_Ref] + val end = readEnd() + val name = readNameRef() + val owner = if (buf.readIndex == end) NoSymbol else readSymRef() + buf.assertEnd(end) + ExtInfo(tag, name, owner) + } + + def readType(): TypeInfo = { + // THIStpe len_Nat sym_Ref + // TYPEREFtpe len_Nat type_Ref sym_Ref {targ_Ref} + val tag = buf.readByte() + val end = readEnd() + tag match { + case THIStpe => ThisTypeInfo(readSymRef()) + case TYPEREFtpe => TypeRefInfo(readTypeRef(), readSymRef(), until(end, () => readTypeRef())) + case _ => UnknownType(tag) + } + } + + def readSymbolAnnotation(): SymAnnotInfo = { + // SYMANNOT = len_Nat sym_Ref AnnotInfoBody + // AnnotInfoBody = info_Ref {annotArg_Ref} {name_Ref constAnnotArg_Ref} + val end = readEnd() + val sym = readSymbolRef() + val tpe = readTypeRef() + buf.readIndex = end + SymAnnotInfo(sym, tpe) + } + + def readEntry() = { + val tag = buf.readByte() + tag match { + case NONEsym => readSymbol(tag) + case TYPEsym => readSymbol(tag) + case ALIASsym => readSymbol(tag) + case CLASSsym => readSymbol(tag) + case MODULEsym => readSymbol(tag) + case VALsym => readSymbol(tag) + case SYMANNOT => readSymbolAnnotation() + case _ => buf.readIndex = readEnd(); UnknownEntry(tag) + } + } + + for (num <- index.indices) at(num, readEntry) + + if (doPrint) { + entries.iterator.zipWithIndex.filter(_._1 != null).foreach { case (entry, num) => + println(s"$num: ${entry.getClass.getSimpleName} $entry") + } + } + def symbolToClass(symbolInfo: SymbolInfo): ClassInfo = { - if (symbolInfo.name == REFINE_CLASS_NAME) { + if (symbolInfo.name.value == REFINE_CLASS_NAME) { // eg: CLASSsym 4: 89() 0 0[] 87 // Nsc's UnPickler also excludes these with "isRefinementSymbolEntry" NoClass - } else if (symbolInfo.name == LOCAL_CHILD) { + } else if (symbolInfo.name.value == LOCAL_CHILD) { // Predef$$less$colon$less$ NoClass } else { @@ -44,16 +157,16 @@ object MimaUnpickler { def lookup(cls: ClassInfo) = { val clsName = cls.bytecodeName val separator = if (clsName.endsWith("$")) "" else "$" - val name0 = symbolInfo.name + val name0 = symbolInfo.name.value val name = if (name0.startsWith(clsName)) name0.substring(name0.lastIndexOf('$') + 1) else name0 val suffix = if (symbolInfo.isModuleOrModuleClass) "$" else "" val newName = clsName + separator + name + suffix clazz.owner.classes.getOrElse(newName, NoClass) } - classes(symbolInfo.owner) match { - case null if symbolInfo.owner == 0 => lookup(fallback) - case null => fallback - case cls => lookup(cls) + classes.getOrElse(symbolInfo.owner, null) match { + case null if symbolInfo.owner == entries(0) => lookup(fallback) + case null => fallback + case cls => lookup(cls) } } } @@ -61,14 +174,14 @@ object MimaUnpickler { def doMethods(clazz: ClassInfo, methods: List[SymbolInfo]) = { methods.iterator .filter(!_.isParam) - .filter(_.name != CONSTRUCTOR) // TODO support package private constructors + .filter(_.name.value != CONSTRUCTOR) // TODO support package private constructors .toSeq.groupBy(_.name).foreach { case (name, pickleMethods) => doMethodOverloads(clazz, name, pickleMethods) } } - def doMethodOverloads(clazz: ClassInfo, name: String, pickleMethods: Seq[SymbolInfo]) = { - val bytecodeMethods = clazz.methods.get(name).filter(!_.isBridge).toList + def doMethodOverloads(clazz: ClassInfo, name: Name, pickleMethods: Seq[SymbolInfo]) = { + val bytecodeMethods = clazz.methods.get(name.value).filter(!_.isBridge).toList // #630 one way this happens with mixins: // trait Foo { def bar(x: Int): Int = x } // class Bar extends Foo { private[foo] def bar: String = "" } @@ -86,61 +199,81 @@ object MimaUnpickler { } } - for (num <- index.indices) { - buf.atIndex(index(num)) { - val tag = buf.readByte() - buf.readIndex -= 1 - tag match { - case CLASSsym => syms(num) = readSymbol() - case MODULEsym => syms(num) = readSymbol() - case VALsym => syms(num) = readSymbol() - case _ => + for (sym <- defnSyms) classes(sym) = symbolToClass(sym) + + for (clsSym <- defnSyms) { + val cls = classes(clsSym) + if (clsSym.isScopedPrivate && cls != NoClass) cls.module._scopedPrivate = true + doMethods(cls, methSyms.filter(_.owner == clsSym).toList) + } + + for (symAnnot <- entries.iterator.collect { case s: SymAnnotInfo => s }) { + val cls = classes.getOrElse(symAnnot.sym, null) // add support for @experimental methods? + if (cls != null && cls != NoClass) { + val annotName = symAnnot.tpe match { + case ThisTypeInfo(sym) => s"$sym" + case TypeRefInfo(_, sym, Nil) => s"$sym" + case _ => "?" } + cls._annotations :+= AnnotInfo(annotName) } } + } + + sealed trait Entry { + override def toString = this match { + case TermName(value) => s"$value." + case TypeName(value) => s"$value#" + case x: SymInfo => s"$x" + case ThisTypeInfo(sym) => s"$sym" + case TypeRefInfo(tpe, sym, targs) => s"$sym${if (targs.isEmpty) "" else targs.mkString("[", ", ", "]")}" + case SymAnnotInfo(sym, tpe) => s"@$tpe $sym" + case UnknownType(tag) => s"UnknownType(${tag2string(tag)})" + case UnknownEntry(tag) => s"UnknownEntry(${tag2string(tag)})" + } + } + final case class UnknownEntry(tag: Int) extends Entry + + sealed trait Name extends Entry { def tag: Int; def value: String } + final case class TermName(value: String) extends Name { def tag = TERMname } + final case class TypeName(value: String) extends Name { def tag = TYPEname } + + object nme { + def NoSymbol = TermName("NoSymbol") + def Empty = TermName("") + } + + sealed trait SymInfo extends Entry { + def tag: Int + def name: Name + def owner: SymInfo - for ((sym, num) <- defnSyms) - classes(num) = symbolToClass(sym) + def isNoSymbol = tag == NONEsym && name == nme.NoSymbol + def isEmpty = name == nme.Empty - for ((clsSym, num) <- defnSyms) { - val clazz = classes(num) - if (clsSym.isScopedPrivate) - clazz.module._scopedPrivate = true - val methods = methSyms.collect { case (sym, _) if sym.owner == num => sym }.toList - doMethods(clazz, methods) + override def toString = { + if (isNoSymbol) "NoSymbol" + else if (owner.isNoSymbol || owner.isEmpty) name.value + else s"$owner.${name.value}" } } - final case class SymbolInfo(tag: Int, name: String, owner: Int, flags: Long, isScopedPrivate: Boolean) { + final case class SymbolInfo(tag: Int, name: Name, owner: SymInfo, flags: Long, isScopedPrivate: Boolean) extends SymInfo { def hasFlag(flag: Long): Boolean = (flags & flag) != 0L def isModuleOrModuleClass = hasFlag(Flags.MODULE_PKL) def isParam = hasFlag(Flags.PARAM) - override def toString = s"SymbolInfo(${tag2string(tag)}, $name, owner=$owner, isScopedPrivate=$isScopedPrivate)" } + val NoSymbol: SymbolInfo = SymbolInfo(NONEsym, nme.NoSymbol, null, 0, false) - final case class PickleEntry(num: Int, startIndex: Int, tag: Int, bytes: Array[Byte]) { - override def toString = s"$num,$startIndex: ${tag2string(tag)}" - } + final case class ExtInfo(tag: Int, name: Name, owner: SymInfo) extends SymInfo - final case class PickleEntries(entries: IndexedSeq[PickleEntry]) { - def nameAt(idx: Int) = { - val entry = entries(idx) - def readStr() = new String(entry.bytes, "UTF-8") - def readStrRef() = new String(entries(readNat(entry.bytes)).bytes, "UTF-8") - entry.tag match { - case TERMname => readStr() - case TYPEname => readStr() - case TYPEsym => readStrRef() - case ALIASsym => readStrRef() - case CLASSsym => readStrRef() - case MODULEsym => readStrRef() - case VALsym => readStrRef() - case EXTref => readStrRef() - case EXTMODCLASSref => readStrRef() - case _ => "?" - } - } - } + sealed trait TypeInfo extends Entry + final case class UnknownType(tag: Int) extends TypeInfo + + final case class ThisTypeInfo(sym: SymInfo) extends TypeInfo + final case class TypeRefInfo(tpe: TypeInfo, sym: SymInfo, targs: List[TypeInfo]) extends TypeInfo + + final case class SymAnnotInfo(sym: SymbolInfo, tpe: TypeInfo) extends Entry def readNat(data: Array[Byte]): Int = { var idx = 0 diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/PickleBuffer.scala b/core/src/main/scala/com/typesafe/tools/mima/core/PickleBuffer.scala index 7bd2e234b..e10d5e00a 100644 --- a/core/src/main/scala/com/typesafe/tools/mima/core/PickleBuffer.scala +++ b/core/src/main/scala/com/typesafe/tools/mima/core/PickleBuffer.scala @@ -4,6 +4,7 @@ final class PickleBuffer(val bytes: Array[Byte]) { var readIndex = 0 def readByte(): Int = { val x = bytes(readIndex).toInt; readIndex += 1; x } + def nextByte(): Int = bytes(readIndex + 1).toInt /** Read a natural number in big endian format, base 128. All but the last digits have bit 0x80 set. */ def readNat(): Int = readLongNat().toInt @@ -30,6 +31,8 @@ final class PickleBuffer(val bytes: Array[Byte]) { x << leading >> leading } + /** The indices in the bytes array where each consecutive entry starts. + * The length of the array is the number of entries in the pickle bytes. */ def createIndex: Array[Int] = { atIndex(0) { readNat(); readNat() // discard version @@ -43,22 +46,11 @@ final class PickleBuffer(val bytes: Array[Byte]) { } } - /** Returns the buffer as a sequence of (Int, Array[Byte]) representing - * (tag, data) of the individual entries. Saves and restores buffer state. - */ - def toIndexedSeq: IndexedSeq[(Int, Array[Byte])] = { - for (idx <- createIndex) yield { - atIndex(idx) { - val tag = readByte() - val len = readNat() - tag -> bytes.slice(readIndex, readIndex + len) - } - } - } - def atIndex[T](i: Int)(body: => T): T = { val saved = readIndex readIndex = i try body finally readIndex = saved } + + def assertEnd(end: Int) = assert(readIndex == end, s"Expected at end=$end but readIndex=$readIndex") } diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/PickleFormat.scala b/core/src/main/scala/com/typesafe/tools/mima/core/PickleFormat.scala index fd4a1a89c..6112e5149 100644 --- a/core/src/main/scala/com/typesafe/tools/mima/core/PickleFormat.scala +++ b/core/src/main/scala/com/typesafe/tools/mima/core/PickleFormat.scala @@ -2,8 +2,7 @@ package com.typesafe.tools.mima.core object PickleFormat { - /*************************************************** - * Symbol table attribute format: + /** Symbol table attribute format: * Symtab = nentries_Nat {Entry} * * NameInfo = @@ -15,8 +14,9 @@ object PickleFormat { * ConstAnnotArg = Constant | AnnotInfo | AnnotArgArray * * len is remaining length after `len`. - * - * Entry = \ + */ + + /** Entry = \ * 1 TERMNAME len_Nat NameInfo * | 2 TYPENAME len_Nat NameInfo * | @@ -66,8 +66,9 @@ object PickleFormat { * | 44 ANNOTARGARRAY len_Nat {constAnnotArg_Ref} * | 47 DEBRUIJNINDEXtpe len_Nat level_Nat index_Nat // deprecated * | 48 EXISTENTIALtpe len_Nat type_Ref {symbol_Ref} - * | - * | 49 TREE len_Nat + */ + + /** | 49 TREE len_Nat * | 1 EMPTYtree * | 2 PACKAGEtree type_Ref sym_Ref mods_Ref name_Ref {tree_Ref} * | 3 CLASStree type_Ref sym_Ref mods_Ref name_Ref tree_Ref {tree_Ref} @@ -138,15 +139,15 @@ object PickleFormat { // tpe final val NOtpe = 11 final val NOPREFIXtpe = 12 - final val THIStpe = 13 + final val THIStpe = 13 final val SINGLEtpe = 14 - final val CONSTANTtpe = 15 - final val TYPEREFtpe = 16 - final val TYPEBOUNDStpe= 17 - final val REFINEDtpe = 18 + final val CONSTANTtpe = 15 + final val TYPEREFtpe = 16 + final val TYPEBOUNDStpe = 17 + final val REFINEDtpe = 18 final val CLASSINFOtpe = 19 - final val METHODtpe = 20 - final val POLYtpe = 21 + final val METHODtpe = 20 + final val POLYtpe = 21 final val IMPLICITMETHODtpe = 22 // no longer generated // literal diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/PicklePrinter.scala b/core/src/main/scala/com/typesafe/tools/mima/core/PicklePrinter.scala new file mode 100644 index 000000000..dc1e08486 --- /dev/null +++ b/core/src/main/scala/com/typesafe/tools/mima/core/PicklePrinter.scala @@ -0,0 +1,153 @@ +package com.typesafe.tools.mima.core + +import java.lang.Long.toHexString +import java.lang.Float.intBitsToFloat +import java.lang.Double.longBitsToDouble +import scala.annotation.tailrec +import scala.io.Codec + +import PickleFormat._ + +object PicklePrinter { + def printPickle(buf: PickleBuffer): Unit = buf.atIndex(0) { + println(s"Version ${buf.readNat()}.${buf.readNat()}") + + val index = buf.createIndex + val entries = PickleEntries(toIndexedSeq(buf).zipWithIndex.map { case ((tag, data), num) => + PickleEntry(num, index(num), tag, data) + }) + buf.readIndex = 0 + + def p(s: String) = print(s) + def nameAt(idx: Int) = s"$idx(${entries.nameAt(idx)})" + def printNat() = p(s" ${buf.readNat()}") + def printNameRef() = p(s" ${nameAt(buf.readNat())}") + def printSymbolRef() = printNat() + def printTypeRef() = printNat() + def printConstantRef() = printNat() + def printAnnotInfoRef() = printNat() + def printConstAnnotArgRef() = printNat() + def printAnnotArgRef() = printNat() + + def printSymInfo(end: Int): Unit = { + printNameRef() + printSymbolRef() // owner + val pflags = buf.readLongNat() + val (flagString, info) = buf.readNat() match { + case pw if buf.readIndex != end => (nameAt(pw), buf.readNat()) + case info => ("", info) + } + p(s" ${toHexString(pflags)}[$flagString] $info") + } + + def printEntry(i: Int): Unit = { + buf.readIndex = index(i) + p(s"$i,${buf.readIndex}: ") + + val tag = buf.readByte() + p(tag2string(tag)) + + val len = buf.readNat() + val end = len + buf.readIndex + p(s" $len:") + + def all[T](body: => T): List[T] = if (buf.readIndex == end) Nil else body :: all(body) + def printTypes() = all(printTypeRef()) + def printSymbols() = all(printSymbolRef()) + + @tailrec def printTag(tag: Int): Unit = tag match { + case TERMname => p(s" ${showName(buf.bytes, buf.readIndex, len)}"); buf.readIndex = end + case TYPEname => p(s" ${showName(buf.bytes, buf.readIndex, len)}"); buf.readIndex = end + + case NONEsym => + case TYPEsym => printSymInfo(end) + case ALIASsym => printSymInfo(end) + case CLASSsym => printSymInfo(end); if (buf.readIndex < end) printTypeRef() + case MODULEsym => printSymInfo(end) + case VALsym => printSymInfo(end) + + case EXTref => printNameRef() ; if (buf.readIndex < end) printSymbolRef() + case EXTMODCLASSref => printNameRef() ; if (buf.readIndex < end) printSymbolRef() + + case NOtpe => + case NOPREFIXtpe => + case THIStpe => printSymbolRef() + case SINGLEtpe => printTypeRef() ; printSymbolRef() + case CONSTANTtpe => printTypeRef() ; printConstantRef() + case TYPEREFtpe => printTypeRef() ; printSymbolRef() ; printTypes() + case TYPEBOUNDStpe => printTypeRef() ; printTypeRef() + case REFINEDtpe => printSymbolRef() ; printTypes() + case CLASSINFOtpe => printSymbolRef() ; printTypes() + case METHODtpe => printTypeRef() ; printTypes() + case POLYtpe => printTypeRef() ; printSymbols() + case IMPLICITMETHODtpe => printTypeRef() ; printTypes() + case SUPERtpe => printTypeRef() ; printTypeRef() + + case LITERALunit => + case LITERALboolean => p(if (buf.readLong(len) == 0L) " false" else " true") + case LITERALbyte => p(" " + buf.readLong(len).toByte) + case LITERALshort => p(" " + buf.readLong(len).toShort) + case LITERALchar => p(" " + buf.readLong(len).toChar) + case LITERALint => p(" " + buf.readLong(len).toInt) + case LITERALlong => p(" " + buf.readLong(len)) + case LITERALfloat => p(" " + intBitsToFloat(buf.readLong(len).toInt)) + case LITERALdouble => p(" " + longBitsToDouble(buf.readLong(len))) + case LITERALstring => printNameRef(); p(scala.io.AnsiColor.RESET) + case LITERALnull => p(" ") + case LITERALclass => printTypeRef() + case LITERALenum => printSymbolRef() + case LITERALsymbol => printNameRef() + + case SYMANNOT => printSymbolRef() ; printTag(ANNOTINFO) + case CHILDREN => printSymbolRef() ; printSymbols() + case ANNOTATEDtpe => printTypeRef() ; all(printAnnotInfoRef()) + case ANNOTINFO => printTypeRef() ; all(printAnnotArgRef()) + case ANNOTARGARRAY => all(printConstAnnotArgRef()) + case DEBRUIJNINDEXtpe => printNat() ; printNat() + case EXISTENTIALtpe => printTypeRef() ; printSymbols() + + case TREE => // skipped + case MODIFIERS => // skipped + + case _ => throw new RuntimeException(s"malformed Scala signature at ${buf.readIndex}; unknown tree type ($tag)") + } + printTag(tag) + + println() + if (buf.readIndex != end) { + val bytes = buf.bytes.slice(index(i), end.max(buf.readIndex)).mkString(", ") + println(s"BAD ENTRY END: computed = $end, actual = ${buf.readIndex}, bytes = $bytes") + } + } + + for (i <- index.indices) + printEntry(i) + } + + private def showName(bs: Array[Byte], idx: Int, len: Int) = new String(Codec.fromUTF8(bs, idx, len)) + + private final case class PickleEntry(num: Int, startIndex: Int, tag: Int, bytes: Array[Byte]) { + override def toString = s"$num,$startIndex: ${tag2string(tag)}" + } + + private final case class PickleEntries(entries: IndexedSeq[PickleEntry]) { + def nameAt(idx: Int) = entries(idx) match { + case PickleEntry(_, _, TERMname, bytes) => new String(bytes, "UTF-8") + case PickleEntry(_, _, TYPEname, bytes) => new String(bytes, "UTF-8") + case _ => "?" + } + } + + /** Returns the buffer as a sequence of (Int, Array[Byte]) representing + * (tag, data) of the individual entries. Saves and restores buffer state. + */ + private def toIndexedSeq(buf: PickleBuffer): IndexedSeq[(Int, Array[Byte])] = { + for (idx <- buf.createIndex) yield { + buf.atIndex(idx) { + val tag = buf.readByte() + val len = buf.readNat() + tag -> buf.bytes.slice(buf.readIndex, buf.readIndex + len) + } + } + } +} diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/TastyFormat.scala b/core/src/main/scala/com/typesafe/tools/mima/core/TastyFormat.scala new file mode 100644 index 000000000..363e2ba00 --- /dev/null +++ b/core/src/main/scala/com/typesafe/tools/mima/core/TastyFormat.scala @@ -0,0 +1,414 @@ +package com.typesafe.tools.mima.core + +/** BNF notation. + * Terminal symbols start with 2 upper case letters, and are represented as a single byte tag. + * Non-terminals are mixed case. + * Lower case letter prefixes, followed by an underscore, are only descriptive. + * + * Micro-syntax: + * Digit = 0 | ... | 127 + * StopDigit = 128 | ... | 255 -- value = digit - 128 + * LongInt = Digit* StopDigit -- big endian 2's complement, value fits in a Long w/o overflow + * Int = LongInt -- big endian 2's complement, fits in an Int w/o overflow + * Nat = LongInt -- non-negative value, fits in an Int without overflow + * Length = Nat -- length of rest of entry in bytes + * + * Macro-format: + * File = Header FormatVersion ToolingVersion UUID nameTable_Length Name* Section* + * Header = 0x5C 0xA1 0xAB 0x1F + * FormatVersion = majorVersion_Nat minorVersion_Nat experimentalVersion_Nat + * ToolingVersion = Length UTF8-CodePoint* -- string that represents the tool that produced the TASTy + * UUID = Byte*16 -- random UUID + * Section = NameRef Length Bytes */ +object TastyFormat { + val header = Array[Int](0x5C, 0xA1, 0xAB, 0x1F) + val MajorVersion = 28 + val MinorVersion = 0 + val ExperimentalVersion = 0 + + /** Name table: + * Name = UTF8 Length UTF8-CodePoint* + * QUALIFIED Length qualified_NameRef selector_NameRef -- A.B + * EXPANDED Length qualified_NameRef selector_NameRef -- A$$B, semantically a NameKinds.ExpandedName + * EXPANDPREFIX Length qualified_NameRef selector_NameRef -- A$B, prefix of expanded name, see NamedKinds.ExpandPrefixName + * + * UNIQUE Length separator_NameRef uniqid_Nat underlying_NameRef? -- Unique name A + * DEFAULTGETTER Length underlying_NameRef index_Nat -- DefaultGetter$ + * + * SUPERACCESSOR Length underlying_NameRef -- super$A + * INLINEACCESSOR Length underlying_NameRef -- inline$A + * OBJECTCLASS Length underlying_NameRef -- A$ (name of the module class for module A) + * + * SIGNED Length original_NameRef resultSig_NameRef ParamSig* -- name + signature + * TARGETSIGNED Length original_NameRef target_NameRef resultSig_NameRef ParamSig* + * + * ParamSig = Int -- If negative, the absolute value represents the length of a type parameter section + * -- If positive, this is a NameRef for the fully qualified name of a term parameter. + * + * NameRef = Nat -- ordinal number of name in name table, starting from 1. + * + * Note: Unqualified names in the name table are strings. The context decides whether a name is + * a type-name or a term-name. The same string can represent both. + */ + object NameTags { + val UTF8 = 1 // A simple name in UTF8 encoding. + val QUALIFIED = 2 // A fully qualified name `.`. + val EXPANDED = 3 // An expanded name `$$`, used by Scala 2 for private names. + val EXPANDPREFIX = 4 // An expansion prefix `$`, used by Scala 2 for private names. + val UNIQUE = 10 // A unique name `$` where `` is used only once for each ``. + val DEFAULTGETTER = 11 // The name `$default$` of a default getter that returns a default argument. + val SUPERACCESSOR = 20 // The name of a super accessor `super$name` created by SuperAccesors. + val INLINEACCESSOR = 21 // The name of an inline accessor `inline$name` + val BODYRETAINER = 22 // The name of a synthetic method that retains the runtime body of an inline method + val OBJECTCLASS = 23 // The name of an object class (or: module class) `$`. + val SIGNED = 63 // A pair of a name and a signature, used to identify possibly overloaded methods. + val TARGETSIGNED = 62 // A triple of a name, a targetname and a signature, used to identify possibly overloaded methods that carry a @targetName annotation. + } + + /** Standard-Section: "ASTs" Tree* + * Tree = PACKAGE Length Path Tree* -- package path { topLevelStats } + * Stat + * + * Stat = Term + * ValOrDefDef + * TYPEDEF Length NameRef (type_Term | Template) Modifier* -- modifiers type name (= type | bounds) | modifiers class name template + * IMPORT Length qual_Term Selector* -- import qual selectors + * EXPORT Length qual_Term Selector* -- export qual selectors + * ValOrDefDef = VALDEF Length NameRef type_Term rhs_Term? Modifier* -- modifiers val name : type (= rhs)? + * DEFDEF Length NameRef Param* returnType_Term rhs_Term? Modifier* -- modifiers def name [typeparams] paramss : returnType (= rhs)? + * Selector = IMPORTED NameRef -- name, "_" for normal wildcards, "" for given wildcards + * RENAMED to_NameRef -- => name + * BOUNDED type_Term -- type bound + * + * TypeParam = TYPEPARAM Length NameRef type_Term Modifier* -- modifiers name bounds + * TermParam = PARAM Length NameRef type_Term Modifier* -- modifiers name : type. + * EMPTYCLAUSE -- an empty parameter clause () + * SPLITCLAUSE -- splits two non-empty parameter clauses of the same kind + * Param = TypeParam + * TermParam + * Template = TEMPLATE Length TypeParam* TermParam* parent_Term* Self? Stat* -- [typeparams] paramss extends parents { self => stats }, where Stat* always starts with the primary constructor. + * Self = SELFDEF selfName_NameRef selfType_Term -- selfName : selfType + * + * Term = Path -- Paths represent both types and terms + * IDENT NameRef Type -- Used when term ident’s type is not a TermRef + * SELECT possiblySigned_NameRef qual_Term -- qual.name + * SELECTin Length possiblySigned_NameRef qual_Term owner_Type -- qual.name, referring to a symbol declared in owner that has the given signature (see note below) + * QUALTHIS typeIdent_Tree -- id.this, different from THIS in that it contains a qualifier ident with position. + * NEW clsType_Term -- new cls + * THROW throwableExpr_Term -- throw throwableExpr + * NAMEDARG paramName_NameRef arg_Term -- paramName = arg + * APPLY Length fn_Term arg_Term* -- fn(args) + * TYPEAPPLY Length fn_Term arg_Type* -- fn[args] + * SUPER Length this_Term mixinTypeIdent_Tree? -- super[mixin] + * TYPED Length expr_Term ascriptionType_Term -- expr: ascription + * ASSIGN Length lhs_Term rhs_Term -- lhs = rhs + * BLOCK Length expr_Term Stat* -- { stats; expr } + * INLINED Length expr_Term call_Term? ValOrDefDef* -- Inlined code from call, with given body `expr` and given bindings + * LAMBDA Length meth_Term target_Type? -- Closure over method `f` of type `target` (omitted id `target` is a function type) + * IF Length [INLINE] cond_Term then_Term else_Term -- inline? if cond then thenPart else elsePart + * MATCH Length (IMPLICIT | [INLINE] sel_Term) CaseDef* -- (inline? sel | implicit) match caseDefs + * TRY Length expr_Term CaseDef* finalizer_Term? -- try expr catch {casdeDef} (finally finalizer)? + * RETURN Length meth_ASTRef expr_Term? -- return expr?, `methASTRef` is method from which is returned + * WHILE Length cond_Term body_Term -- while cond do body + * REPEATED Length elem_Type elem_Term* -- Varargs argument of type `elem` + * SELECTouter Length levels_Nat qual_Term underlying_Type -- Follow `levels` outer links, starting from `qual`, with given `underlying` type + * -- patterns: + * BIND Length boundName_NameRef patType_Type pat_Term -- name @ pat, wherev `patType` is the type of the bound symbol + * ALTERNATIVE Length alt_Term* -- alt1 | ... | altn as a pattern + * UNAPPLY Length fun_Term ImplicitArg* pat_Type pat_Term* -- Unapply node `fun(_: pat_Type)(implicitArgs)` flowing into patterns `pat`. + * -- type trees: + * IDENTtpt NameRef Type -- Used for all type idents + * SELECTtpt NameRef qual_Term -- qual.name + * SINGLETONtpt ref_Term -- ref.type + * REFINEDtpt Length underlying_Term refinement_Stat* -- underlying {refinements} + * APPLIEDtpt Length tycon_Term arg_Term* -- tycon [args] + * LAMBDAtpt Length TypeParam* body_Term -- [TypeParams] => body + * TYPEBOUNDStpt Length low_Term high_Term? -- {{{ >: low <: high }}} + * ANNOTATEDtpt Length underlying_Term fullAnnotation_Term -- underlying @ annotation + * MATCHtpt Length bound_Term? sel_Term CaseDef* -- sel match { CaseDef } where `bound` is optional upper bound of all rhs + * BYNAMEtpt underlying_Term -- => underlying + * SHAREDterm term_ASTRef -- Link to previously serialized term + * HOLE Length idx_Nat arg_Tree* -- Hole where a splice goes with sequence number idx, splice is applied to arguments `arg`s + * + * CaseDef = CASEDEF Length pat_Term rhs_Tree guard_Tree? -- case pat if guard => rhs + * ImplicitArg = IMPLICITARG arg_Term -- implicit unapply argument + * + * ASTRef = Nat -- Byte position in AST payload + * + * Path = Constant + * TERMREFdirect sym_ASTRef -- A reference to a local symbol (without a prefix). Reference is to definition node of symbol. + * TERMREFsymbol sym_ASTRef qual_Type -- A reference `qual.sym` to a local member with prefix `qual` + * TERMREFpkg fullyQualified_NameRef -- A reference to a package member with given fully qualified name + * TERMREF possiblySigned_NameRef qual_Type -- A reference `qual.name` to a non-local member + * TERMREFin Length possiblySigned_NameRef qual_Type owner_Type -- A reference `qual.name` referring to a non-local symbol declared in owner that has the given signature (see note below) + * THIS clsRef_Type -- cls.this + * RECthis recType_ASTRef -- The `this` in a recursive refined type `recType`. + * SHAREDtype path_ASTRef -- link to previously serialized path + * + * Constant = UNITconst -- () + * FALSEconst -- false + * TRUEconst -- true + * BYTEconst Int -- A byte number + * SHORTconst Int -- A short number + * CHARconst Nat -- A character + * INTconst Int -- An int number + * LONGconst LongInt -- A long number + * FLOATconst Int -- A float number + * DOUBLEconst LongInt -- A double number + * STRINGconst NameRef -- A string literal + * NULLconst -- null + * CLASSconst Type -- classOf[Type] + * + * Type = Path -- Paths represent both types and terms + * TYPEREFdirect sym_ASTRef -- A reference to a local symbol (without a prefix). Reference is to definition node of symbol. + * TYPEREFsymbol sym_ASTRef qual_Type -- A reference `qual.sym` to a local member with prefix `qual` + * TYPEREFpkg fullyQualified_NameRef -- A reference to a package member with given fully qualified name + * TYPEREF NameRef qual_Type -- A reference `qual.name` to a non-local member + * TYPEREFin Length NameRef qual_Type namespace_Type -- A reference `qual.name` to a non-local member that's private in `namespace`. + * RECtype parent_Type -- A wrapper for recursive refined types + * SUPERtype Length this_Type underlying_Type -- A super type reference to `underlying` + * REFINEDtype Length underlying_Type refinement_NameRef info_Type -- underlying { refinement_name : info } + * APPLIEDtype Length tycon_Type arg_Type* -- tycon[args] + * TYPEBOUNDS Length lowOrAlias_Type high_Type? Variance* -- = alias or {{{ >: low <: high }}}, possibly with variances of lambda parameters + * ANNOTATEDtype Length underlying_Type annotation_Term -- underlying @ annotation + * ANDtype Length left_Type right_Type -- left & right + * ORtype Length left_Type right_Type -- lefgt | right + * MATCHtype Length bound_Type sel_Type case_Type* -- sel match {cases} with optional upper `bound` + * MATCHCASEtype Length pat_type rhs_Type -- match cases are MATCHCASEtypes or TYPELAMBDAtypes over MATCHCASEtypes + * BIND Length boundName_NameRef bounds_Type Modifier* -- boundName @ bounds, for type-variables defined in a type pattern + * BYNAMEtype underlying_Type -- => underlying + * PARAMtype Length binder_ASTRef paramNum_Nat -- A reference to parameter # paramNum in lambda type `binder` + * POLYtype Length result_Type TypesNames -- A polymorphic method type `[TypesNames]result`, used in refinements + * METHODtype Length result_Type TypesNames Modifier* -- A method type `(Modifier* TypesNames)result`, needed for refinements, with optional modifiers for the parameters + * TYPELAMBDAtype Length result_Type TypesNames -- A type lambda `[TypesNames] => result` + * SHAREDtype type_ASTRef -- link to previously serialized type + * TypesNames = TypeName* + * TypeName = typeOrBounds_ASTRef paramName_NameRef -- (`termName`: `type`) or (`typeName` `bounds`) + * + * Modifier = PRIVATE -- private + * PROTECTED -- protected + * PRIVATEqualified qualifier_Type -- private[qualifier] (to be dropped(?) + * PROTECTEDqualified qualifier_Type -- protected[qualifier] (to be dropped(?) + * ABSTRACT -- abstract + * FINAL -- final + * SEALED -- sealed + * CASE -- case (for classes or objects) + * IMPLICIT -- implicit + * GIVEN -- given + * ERASED -- erased + * LAZY -- lazy + * OVERRIDE -- override + * OPAQUE -- opaque, also used for classes containing opaque aliases + * INLINE -- inline + * MACRO -- Inline method containing toplevel splices + * INLINEPROXY -- Symbol of binding with an argument to an inline method as rhs (TODO: do we still need this?) + * STATIC -- Mapped to static Java member + * OBJECT -- An object or its class + * TRAIT -- A trait + * ENUM -- A enum class or enum case + * LOCAL -- private[this] or protected[this], used in conjunction with PRIVATE or PROTECTED + * SYNTHETIC -- Generated by Scala compiler + * ARTIFACT -- To be tagged Java Synthetic + * MUTABLE -- A var + * FIELDaccessor -- A getter or setter (note: the corresponding field is not serialized) + * CASEaccessor -- A getter for a case class parameter + * COVARIANT -- A type parameter marked “+” + * CONTRAVARIANT -- A type parameter marked “-” + * HASDEFAULT -- Parameter with default arg; method with default parameters (default arguments are separate methods with DEFAULTGETTER names) + * STABLE -- Method that is assumed to be stable, i.e. its applications are legal paths + * EXTENSION -- An extension method + * PARAMsetter -- The setter part `x_=` of a var parameter `x` which itself is pickled as a PARAM + * PARAMalias -- Parameter is alias of a superclass parameter + * EXPORTED -- An export forwarder + * OPEN -- an open class + * INVISIBLE -- invisible during typechecking + * Annotation + * + * Variance = STABLE -- invariant + * | COVARIANT + * | CONTRAVARIANT + * + * Annotation = ANNOTATION Length tycon_Type fullAnnotation_Term -- An annotation, given (class) type of constructor, and full application tree + * + * Note: The signature of a SELECTin or TERMREFin node is the signature of the selected symbol, + * not the signature of the reference. The latter undergoes an asSeenFrom but the former + * does not. + * + * Note: Tree tags are grouped into 5 categories that determine what follows, + * and thus allow to compute the size of the tagged tree in a generic way. + */ + val ASTsSection = "ASTs" + + val AstCat1 = 1 to 59 // Cat. 1: tag + val UNITconst = 2 + val FALSEconst = 3 + val TRUEconst = 4 + val NULLconst = 5 + val PRIVATE = 6 + val PROTECTED = 8 + val ABSTRACT = 9 + val FINAL = 10 + val SEALED = 11 + val CASE = 12 + val IMPLICIT = 13 + val LAZY = 14 + val OVERRIDE = 15 + val INLINEPROXY = 16 + val INLINE = 17 + val STATIC = 18 + val OBJECT = 19 + val TRAIT = 20 + val ENUM = 21 + val LOCAL = 22 + val SYNTHETIC = 23 + val ARTIFACT = 24 + val MUTABLE = 25 + val FIELDaccessor = 26 + val CASEaccessor = 27 + val COVARIANT = 28 + val CONTRAVARIANT = 29 + val HASDEFAULT = 31 + val STABLE = 32 + val MACRO = 33 + val ERASED = 34 + val OPAQUE = 35 + val EXTENSION = 36 + val GIVEN = 37 + val PARAMsetter = 38 + val EXPORTED = 39 + val OPEN = 40 + val PARAMalias = 41 + val TRANSPARENT = 42 + val INFIX = 43 + val INVISIBLE = 44 + val EMPTYCLAUSE = 45 + val SPLITCLAUSE = 46 + + val AstCat2 = 60 to 89 // Cat. 2: tag Nat + val SHAREDterm = 60 + val SHAREDtype = 61 + val TERMREFdirect = 62 + val TYPEREFdirect = 63 + val TERMREFpkg = 64 + val TYPEREFpkg = 65 + val RECthis = 66 + val BYTEconst = 67 + val SHORTconst = 68 + val CHARconst = 69 + val INTconst = 70 + val LONGconst = 71 + val FLOATconst = 72 + val DOUBLEconst = 73 + val STRINGconst = 74 + val IMPORTED = 75 + val RENAMED = 76 + + val AstCat3 = 90 to 109 // Cat. 3: tag AST + val THIS = 90 + val QUALTHIS = 91 + val CLASSconst = 92 + val BYNAMEtype = 93 + val BYNAMEtpt = 94 + val NEW = 95 + val THROW = 96 + val IMPLICITarg = 97 + val PRIVATEqualified = 98 + val PROTECTEDqualified = 99 + val RECtype = 100 + val SINGLETONtpt = 101 + val BOUNDED = 102 + + val AstCat4 = 100 to 127 // Cat. 4: tag Nat AST + val IDENT = 110 + val IDENTtpt = 111 + val SELECT = 112 + val SELECTtpt = 113 + val TERMREFsymbol = 114 + val TERMREF = 115 + val TYPEREFsymbol = 116 + val TYPEREF = 117 + val SELFDEF = 118 + val NAMEDARG = 119 + + val AstCat5 = 128 to 255 // Cat. 5: tag Length [payload] + val PACKAGE = 128 + val VALDEF = 129 + val DEFDEF = 130 + val TYPEDEF = 131 + val IMPORT = 132 + val TYPEPARAM = 133 + val PARAM = 134 + val APPLY = 136 + val TYPEAPPLY = 137 + val TYPED = 138 + val ASSIGN = 139 + val BLOCK = 140 + val IF = 141 + val LAMBDA = 142 + val MATCH = 143 + val RETURN = 144 + val WHILE = 145 + val TRY = 146 + val INLINED = 147 + val SELECTouter = 148 + val REPEATED = 149 + val BIND = 150 + val ALTERNATIVE = 151 + val UNAPPLY = 152 + val ANNOTATEDtype = 153 + val ANNOTATEDtpt = 154 + val CASEDEF = 155 + val TEMPLATE = 156 + val SUPER = 157 + val SUPERtype = 158 + val REFINEDtype = 159 + val REFINEDtpt = 160 + val APPLIEDtype = 161 + val APPLIEDtpt = 162 + val TYPEBOUNDS = 163 + val TYPEBOUNDStpt = 164 + val ANDtype = 165 + val ORtype = 167 + val POLYtype = 169 + val TYPELAMBDAtype = 170 + val LAMBDAtpt = 171 + val PARAMtype = 172 + val ANNOTATION = 173 + val TERMREFin = 174 + val TYPEREFin = 175 + val SELECTin = 176 + val EXPORT = 177 + val METHODtype = 180 + val MATCHtype = 190 + val MATCHtpt = 191 + val MATCHCASEtype = 192 + val HOLE = 255 + + /** Standard-Section: "Positions" LinesSizes Assoc* + * + * LinesSizes = Nat Nat* // Number of lines followed by the size of each line not counting the trailing `\n` + * + * Assoc = Header offset_Delta? offset_Delta? point_Delta? + * | SOURCE nameref_Int + * Header = addr_Delta + // in one Nat: difference of address to last recorded node << 3 + + * hasStartDiff + // one bit indicating whether there follows a start address delta << 2 + * hasEndDiff + // one bit indicating whether there follows an end address delta << 1 + * hasPoint // one bit indicating whether the new position has a point (i.e ^ position) + * // Nodes which have the same positions as their parents are omitted. + * // offset_Deltas give difference of start/end offset wrt to the + * // same offset in the previously recorded node (or 0 for the first recorded node) + * Delta = Int // Difference between consecutive offsets, + * SOURCE = 4 // Impossible as header, since addr_Delta = 0 implies that we refer to the + * // same tree as the previous one, but then hasStartDiff = 1 implies that + * // the tree's range starts later than the range of itself. + * + * All elements of a position section are serialized as Ints + */ + val PositionsSection = "Positions" + val SOURCE = 4 // Position header + + /** Standard Section: "Comments" Comment* + * Comment = Length Bytes LongInt // Raw comment's bytes encoded as UTF-8, followed by the comment's coordinates. + */ + val CommentsSection = "Comments" +} diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/TastyPrinter.scala b/core/src/main/scala/com/typesafe/tools/mima/core/TastyPrinter.scala new file mode 100644 index 000000000..8dcd76672 --- /dev/null +++ b/core/src/main/scala/com/typesafe/tools/mima/core/TastyPrinter.scala @@ -0,0 +1,144 @@ +package com.typesafe.tools.mima.core + +import TastyFormat._, TastyTagOps._ +import TastyUnpickler._ + +object TastyPrinter { + def printPickle(in: TastyReader, path: String): Unit = { + println(s"unpickling $path") + + //val header = + readHeader(in) + //printHeader(header) + + val names = readNames(in) + //printNames(names) + + printAllTrees(getTreeReader(in, names), names) + } + + def printHeader(header: Header) = { + val (h1, h2, h3, h4) = header.header + val (versionMaj, versionMin, versionExp) = header.version + println(s"Header: ${List(h1, h2, h3, h4).map(_.toHexString.toUpperCase).mkString(" ")}") + println(s"Version: $versionMaj.$versionMin-$versionExp") + println(s"Tooling: ${header.toolingVersion}") + println(s"UUID: ${header.uuid.toString.toUpperCase}") + } + + def printNames(names: Names) = { + println("Names:") + for ((name, idx) <- names.zipWithIndex) + println(s"${nameStr(f"$idx%4d")}: ${name.debug}") + } + + def printAllTrees(in: TastyReader, names: Names) = { + import in._ + print(s"Trees: start=${startAddr.index} base=$base current=${currentAddr.index} end=${endAddr.index}; ${endAddr.index - startAddr.index} bytes of AST") + + var indent = 0 + def newLine() = print(s"\n ${treeStr(f"${index(currentAddr) - index(startAddr)}%5d")}:" + " " * indent) + def printNat() = print(treeStr(" " + readNat())) + def printName() = { val ref = readNat(); print(nameStr(s" $ref [${names(ref).debug}]")) } + + def printTree(): Unit = { + newLine() + indent += 2 + val tag = readByte() + print(s" ${astTagToString(tag)}") + + def printLengthTree() = { + val len = readNat() + val end = currentAddr + len + def printTrees() = doUntil(end)(printTree()) + def printMethodic() = { + printTree() + while (currentAddr.index < end.index && !isModifierTag(nextByte)) { + printTree() + printName() + } + printTrees() + } + + print(s"(${lengthStr(len.toString)})") + tag match { + case VALDEF => printName(); printTrees() + case DEFDEF => printName(); printTrees() + case TYPEDEF => printName(); printTrees() + case TYPEPARAM => printName(); printTrees() + case PARAM => printName(); printTrees() + + case RETURN => printNat(); printTrees() + + case BIND => printName(); printTrees() + case REFINEDtype => printName(); printTrees() + + case POLYtype => printMethodic() + case TYPELAMBDAtype => printMethodic() + + case PARAMtype => printNat(); printNat() // target/ref Addr + paramNum + + case TERMREFin => printName(); printTrees() + case TYPEREFin => printName(); printTrees() + case SELECTin => printName(); printTrees() + + case METHODtype => printMethodic() + + case _ => printTrees() + } + if (currentAddr != end) { + print(s"incomplete read, current = $currentAddr, end = $end\n") + goto(end) + } + } + + def printNatASTTree() = tag match { + case TERMREFsymbol | TYPEREFsymbol => printNat(); printTree() + case _ => printName(); printTree() + } + + def printNatTree() = tag match { + case TERMREFpkg | TYPEREFpkg | STRINGconst | IMPORTED | RENAMED => printName() + case _ => printNat() + } + + astCategory(tag) match { + case AstCat1TagOnly => + case AstCat2Nat => printNatTree() + case AstCat3AST => printTree() + case AstCat4NatAST => printNatASTTree() + case AstCat5Length => printLengthTree() + } + + indent -= 2 + } + + while (!isAtEnd) printTree() + println() + } + + def printClassNames(in: TastyReader, path: String): Unit = { + readHeader(in) + val names = readNames(in) + val (pkg, nme) = unpicklePkgAndClsName(getTreeReader(in, names), names) + println(s"${lengthStr(path)} -> ${treeStr(pkg.source)}.${nameStr(nme.source)}") + } + + private def unpicklePkgAndClsName(in: TastyReader, names: Names): (Name, Name) = { + import in._ + def readName(r: TastyReader = in) = names(r.readNat()) + def readNames(packageName: Name): (Name, Name) = readByte() match { + case TYPEDEF => readEnd(); (packageName, readName()) + case PACKAGE => readEnd(); readNames(packageName) + case TERMREFpkg => readNames(readName()) + case TYPEREFpkg => readNames(readName()) + case SHAREDtype => val r = forkAt(readAddr()); r.readByte(); readNames(readName(r)) + case t => skipTreeTagged(in, t); readNames(packageName) + } + readNames(nme.Empty) + } + + private def nameStr(str: String) = Console.MAGENTA + str + Console.RESET + private def treeStr(str: String) = Console.YELLOW + str + Console.RESET + private def lengthStr(str: String) = Console.CYAN + str + Console.RESET +} diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/TastyReader.scala b/core/src/main/scala/com/typesafe/tools/mima/core/TastyReader.scala new file mode 100644 index 000000000..575aa30a8 --- /dev/null +++ b/core/src/main/scala/com/typesafe/tools/mima/core/TastyReader.scala @@ -0,0 +1,107 @@ +package com.typesafe.tools.mima.core + +import scala.collection.mutable.ListBuffer + +import TastyRefs._ + +final class TastyReader(val bytes: Array[Byte], start: Int, end: Int, val base: Int = 0) { + def this(bytes: Array[Byte]) = this(bytes, 0, bytes.length) + + private[this] var bp: Int = start + + def fork = new TastyReader(bytes, bp, end, base) + def forkAt(start: Addr) = new TastyReader(bytes, index(start), end, base) + + def addr(idx: Int): Addr = Addr(idx - base) + def index(addr: Addr): Int = addr.index + base + def startAddr: Addr = addr(start) // The address of the first byte to read, respectively byte that was read + def currentAddr: Addr = addr(bp) // The address of the next byte to read + def endAddr: Addr = addr(end) // The address one greater than the last brte to read + def isAtEnd: Boolean = bp == end // Have all bytes been read? + def nextByte: Int = bytes(bp) & 0xff // Returns the next byte of data as a natural number without advancing the read position + def readNat(): Int = readLongNat().toInt // Read a natural number fitting in an Int in big endian format, base 128. All but the last digits have bit 0x80 set. + def readInt(): Int = readLongInt().toInt // Read an integer number in 2's complement big endian format, base 128. All but the last digits have bit 0x80 set. + def readAddr(): Addr = Addr(readNat()) // Read a natural number and return as an address + def readEnd(): Addr = addr(readNat() + bp) // Read a length number and return the absolute end address implied by it, given as (address following length field) + (length-value-read) + def goto(addr: Addr): Unit = bp = index(addr) // Set read position to the one pointed to by `addr` + def readByte(): Int = { val b = nextByte; bp += 1; b } // Read a byte of data + + /** Read the next `n` bytes of `data`. */ + def readBytes(n: Int): Array[Byte] = { + val result = new Array[Byte](n) + System.arraycopy(bytes, bp, result, 0, n) + bp += n + result + } + + /** Read a natural number fitting in a Long in big endian format, base 128. All but the last digits have bit 0x80 set. */ + def readLongNat(): Long = { + var b = 0L + var x = 0L + while ({ + b = bytes(bp) + x = (x << 7) | (b & 0x7f) + bp += 1 + (b & 0x80) == 0 + }) () + x + } + + /** Read a long integer number in 2's complement big endian format, base 128. */ + def readLongInt(): Long = { + var b = bytes(bp) + var x: Long = (b << 1).toByte >> 1 // sign extend with bit 6. + bp += 1 + while ((b & 0x80) == 0) { + b = bytes(bp) + x = (x << 7) | (b & 0x7f) + bp += 1 + } + x + } + + /** Read an uncompressed Long stored in 8 bytes in big endian format. */ + def readUncompressedLong(): Long = { + var x = 0L + for (_ <- 0 to 7) + x = (x << 8) | readByte() + x + } + + /** Perform `op` until `end` address is reached and collect results in a list. */ + def until[T](end: Addr)(op: => T): List[T] = { + val buf = new ListBuffer[T] + doUntil(end)(buf += op) + buf.toList + } + + def doUntil(end: Addr)(op: => Unit): Unit = { + val endIdx = index(end) + while (bp < endIdx) op + assertEnd(end) + } + + def assertEnd(end: Addr, info: String = "") = { + assert(bp == index(end), s"incomplete read: curr=$currentAddr != end=$end$info") + } + + def softAssertEnd(end: Addr, info: String = "") = { + assertSoft(currentAddr == end, s"incomplete read: curr=$currentAddr != end=$end$info") + goto(end) + } + + def assertSoft(cond: Boolean, msg: => String) = if (!cond) println(msg) + def assertShort(cond: Boolean, msg: => String) = if (!cond) shortExc(s"$msg") + def shortExc(msg: String) = throw new Exception(msg, null, false, false) {} + + /** If before given `end` address, the result of `op`, otherwise `default` */ + def ifBefore[T](end: Addr)(op: => T, default: T): T = + if (bp < index(end)) op else default + + /** Perform `op` while condition `cond` holds and collect results in a list. */ + def collectWhile[T](cond: => Boolean)(op: => T): List[T] = { + val buf = new ListBuffer[T] + while (cond) buf += op + buf.toList + } +} diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/TastyRefs.scala b/core/src/main/scala/com/typesafe/tools/mima/core/TastyRefs.scala new file mode 100644 index 000000000..aacf4d23e --- /dev/null +++ b/core/src/main/scala/com/typesafe/tools/mima/core/TastyRefs.scala @@ -0,0 +1,21 @@ +package com.typesafe.tools.mima.core + +object TastyRefs { + /** An address pointing to an index in a TASTy buffer's byte array */ + case class Addr(index: Int) extends AnyVal { + def - (delta: Int): Addr = Addr(index - delta) + def + (delta: Int): Addr = Addr(index + delta) + + def relativeTo(base: Addr): Addr = this - base.index - AddrWidth + + def ==(that: Addr): Boolean = index == that.index + def !=(that: Addr): Boolean = index != that.index + + def <(that: Addr): Boolean = index < that.index + } + + /** The maximal number of address bytes. + * Since addresses are written as base-128 natural numbers, + * the value of 4 gives a maximal array size of 256M. */ + final val AddrWidth = 4 +} diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/TastyTagOps.scala b/core/src/main/scala/com/typesafe/tools/mima/core/TastyTagOps.scala new file mode 100644 index 000000000..b402b0462 --- /dev/null +++ b/core/src/main/scala/com/typesafe/tools/mima/core/TastyTagOps.scala @@ -0,0 +1,218 @@ +package com.typesafe.tools.mima.core + +import TastyFormat._, NameTags._ + +object TastyTagOps { + def nameTagToString(tag: Int) = tag match { + case UTF8 => "UTF8" + case QUALIFIED => "QUALIFIED" + case EXPANDED => "EXPANDED" + case EXPANDPREFIX => "EXPANDPREFIX" + case UNIQUE => "UNIQUE" + case DEFAULTGETTER => "DEFAULTGETTER" + case SUPERACCESSOR => "SUPERACCESSOR" + case INLINEACCESSOR => "INLINEACCESSOR" + case BODYRETAINER => "BODYRETAINER" + case OBJECTCLASS => "OBJECTCLASS" + case SIGNED => "SIGNED" + case TARGETSIGNED => "TARGETSIGNED" + case id => s"NotANameTag($id)" + } + + def isModifierTag(tag: Int): Boolean = tag match { + case PRIVATE => true + case PROTECTED => true + case ABSTRACT => true + case FINAL => true + case SEALED => true + case CASE => true + case IMPLICIT => true + case ERASED => true + case LAZY => true + case OVERRIDE => true + case INLINE => true + case INLINEPROXY => true + case MACRO => true + case OPAQUE => true + case STATIC => true + case OBJECT => true + case TRAIT => true + case ENUM => true + case LOCAL => true + case SYNTHETIC => true + case ARTIFACT => true + case MUTABLE => true + case FIELDaccessor => true + case CASEaccessor => true + case COVARIANT => true + case CONTRAVARIANT => true + case HASDEFAULT => true + case STABLE => true + case EXTENSION => true + case GIVEN => true + case PARAMsetter => true + case EXPORTED => true + case OPEN => true + case PARAMalias => true + case TRANSPARENT => true + case INFIX => true + case INVISIBLE => true + case ANNOTATION => true + case PRIVATEqualified => true + case PROTECTEDqualified => true + case _ => false + } + + def astTagToString(tag: Int): String = tag match { + case UNITconst => "UNITconst" + case FALSEconst => "FALSEconst" + case TRUEconst => "TRUEconst" + case NULLconst => "NULLconst" + case PRIVATE => "PRIVATE" + case PROTECTED => "PROTECTED" + case ABSTRACT => "ABSTRACT" + case FINAL => "FINAL" + case SEALED => "SEALED" + case CASE => "CASE" + case IMPLICIT => "IMPLICIT" + case ERASED => "ERASED" + case LAZY => "LAZY" + case OVERRIDE => "OVERRIDE" + case INLINE => "INLINE" + case INLINEPROXY => "INLINEPROXY" + case MACRO => "MACRO" + case OPAQUE => "OPAQUE" + case STATIC => "STATIC" + case OBJECT => "OBJECT" + case TRAIT => "TRAIT" + case ENUM => "ENUM" + case LOCAL => "LOCAL" + case SYNTHETIC => "SYNTHETIC" + case ARTIFACT => "ARTIFACT" + case MUTABLE => "MUTABLE" + case FIELDaccessor => "FIELDaccessor" + case CASEaccessor => "CASEaccessor" + case COVARIANT => "COVARIANT" + case CONTRAVARIANT => "CONTRAVARIANT" + case HASDEFAULT => "HASDEFAULT" + case STABLE => "STABLE" + case EXTENSION => "EXTENSION" + case GIVEN => "GIVEN" + case PARAMsetter => "PARAMsetter" + case EXPORTED => "EXPORTED" + case OPEN => "OPEN" + case PARAMalias => "PARAMalias" + case TRANSPARENT => "TRANSPARENT" + case INFIX => "INFIX" + case INVISIBLE => "INVISIBLE" + case EMPTYCLAUSE => "EMPTYCLAUSE" + case SPLITCLAUSE => "SPLITCLAUSE" + + case SHAREDterm => "SHAREDterm" + case SHAREDtype => "SHAREDtype" + case TERMREFdirect => "TERMREFdirect" + case TYPEREFdirect => "TYPEREFdirect" + case TERMREFpkg => "TERMREFpkg" + case TYPEREFpkg => "TYPEREFpkg" + case RECthis => "RECthis" + case BYTEconst => "BYTEconst" + case SHORTconst => "SHORTconst" + case CHARconst => "CHARconst" + case INTconst => "INTconst" + case LONGconst => "LONGconst" + case FLOATconst => "FLOATconst" + case DOUBLEconst => "DOUBLEconst" + case STRINGconst => "STRINGconst" + case IMPORTED => "IMPORTED" + case RENAMED => "RENAMED" + + case THIS => "THIS" + case QUALTHIS => "QUALTHIS" + case CLASSconst => "CLASSconst" + case BYNAMEtype => "BYNAMEtype" + case BYNAMEtpt => "BYNAMEtpt" + case NEW => "NEW" + case THROW => "THROW" + case IMPLICITarg => "IMPLICITarg" + case PRIVATEqualified => "PRIVATEqualified" + case PROTECTEDqualified => "PROTECTEDqualified" + case RECtype => "RECtype" + case SINGLETONtpt => "SINGLETONtpt" + case BOUNDED => "BOUNDED" + + case IDENT => "IDENT" + case IDENTtpt => "IDENTtpt" + case SELECT => "SELECT" + case SELECTtpt => "SELECTtpt" + case TERMREFsymbol => "TERMREFsymbol" + case TERMREF => "TERMREF" + case TYPEREFsymbol => "TYPEREFsymbol" + case TYPEREF => "TYPEREF" + case SELFDEF => "SELFDEF" + case NAMEDARG => "NAMEDARG" + + case PACKAGE => "PACKAGE" + case VALDEF => "VALDEF" + case DEFDEF => "DEFDEF" + case TYPEDEF => "TYPEDEF" + case IMPORT => "IMPORT" + case TYPEPARAM => "TYPEPARAM" + case PARAM => "PARAM" + case APPLY => "APPLY" + case TYPEAPPLY => "TYPEAPPLY" + case TYPED => "TYPED" + case ASSIGN => "ASSIGN" + case BLOCK => "BLOCK" + case IF => "IF" + case LAMBDA => "LAMBDA" + case MATCH => "MATCH" + case RETURN => "RETURN" + case WHILE => "WHILE" + case TRY => "TRY" + case INLINED => "INLINED" + case SELECTouter => "SELECTouter" + case REPEATED => "REPEATED" + case BIND => "BIND" + case ALTERNATIVE => "ALTERNATIVE" + case UNAPPLY => "UNAPPLY" + case ANNOTATEDtype => "ANNOTATEDtype" + case ANNOTATEDtpt => "ANNOTATEDtpt" + case CASEDEF => "CASEDEF" + case TEMPLATE => "TEMPLATE" + case SUPER => "SUPER" + case SUPERtype => "SUPERtype" + case REFINEDtype => "REFINEDtype" + case REFINEDtpt => "REFINEDtpt" + case APPLIEDtype => "APPLIEDtype" + case APPLIEDtpt => "APPLIEDtpt" + case TYPEBOUNDS => "TYPEBOUNDS" + case TYPEBOUNDStpt => "TYPEBOUNDStpt" + case ANDtype => "ANDtype" + case ORtype => "ORtype" + case POLYtype => "POLYtype" + case TYPELAMBDAtype => "TYPELAMBDAtype" + case LAMBDAtpt => "LAMBDAtpt" + case PARAMtype => "PARAMtype" + case ANNOTATION => "ANNOTATION" + case TERMREFin => "TERMREFin" + case TYPEREFin => "TYPEREFin" + case SELECTin => "SELECTin" + case EXPORT => "EXPORT" + case METHODtype => "METHODtype" + case MATCHtype => "MATCHtype" + case MATCHtpt => "MATCHtpt" + case MATCHCASEtype => "MATCHCASEtype" + case HOLE => "HOLE" + + case tag => s"BadTag($tag)" + } + + sealed abstract class AstCategory(val range: Range.Inclusive) + case object AstCat1TagOnly extends AstCategory(AstCat1) + case object AstCat2Nat extends AstCategory(AstCat2) + case object AstCat3AST extends AstCategory(AstCat3) + case object AstCat4NatAST extends AstCategory(AstCat4) + case object AstCat5Length extends AstCategory(AstCat5) + val astCategories = List(AstCat1TagOnly, AstCat2Nat, AstCat3AST, AstCat4NatAST, AstCat5Length) + def astCategory(tag: Int): AstCategory = astCategories.iterator.filter(_.range.contains(tag)).next() +} diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/TastyUnpickler.scala b/core/src/main/scala/com/typesafe/tools/mima/core/TastyUnpickler.scala new file mode 100644 index 000000000..04f0e1fda --- /dev/null +++ b/core/src/main/scala/com/typesafe/tools/mima/core/TastyUnpickler.scala @@ -0,0 +1,498 @@ +package com.typesafe.tools.mima.core + +import java.util.UUID + +import scala.annotation.tailrec +import scala.collection.mutable, mutable.{ ArrayBuffer, ListBuffer } + +import TastyFormat._, NameTags._, TastyTagOps._, TastyRefs._ + +object TastyUnpickler { + def unpickleClass(in: TastyReader, clazz: ClassInfo, path: String): Unit = { + val doPrint = false + //val doPrint = path.contains("v1") && !path.contains("exclude.tasty") + //if (doPrint) TastyPrinter.printClassNames(in.fork, path) + if (doPrint) TastyPrinter.printPickle(in.fork, path) + + readHeader(in) + val names = readNames(in) + val tree = unpickleTree(getTreeReader(in, names), names) + + copyAnnotations(tree, clazz) + } + + def copyAnnotations(tree: Tree, clazz: ClassInfo): Unit = { + new Traverser { + var pkgNames = List.empty[Name] + var clsNames = List.empty[Name] + + override def traversePkg(pkg: Pkg): Unit = { + pkgNames ::= getPkgName(pkg) + super.traversePkg(pkg) + pkgNames = pkgNames.tail + } + + override def traverseClsDef(clsDef: ClsDef): Unit = { + forEachClass(clsDef, pkgNames, clsNames) + clsNames ::= clsDef.name + super.traverseClsDef(clsDef) + clsNames = clsNames.tail + } + }.traverse(tree) + + def getPkgName(pkg: Pkg) = pkg.path match { + case TypeRefPkg(fullyQual) => fullyQual + case p: UnknownPath => SimpleName(p.show) + } + + def forEachClass(clsDef: ClsDef, pkgNames: List[Name], clsNames: List[Name]): Unit = { + val pkgName = pkgNames.headOption.getOrElse(nme.Empty) + if (pkgName.source == clazz.owner.fullName) { + val clsName = (clsDef.name :: clsNames).reverseIterator.mkString("$") + val cls = clazz.owner.classes.getOrElse(clsName, NoClass) + if (cls != NoClass) { + cls._annotations ++= clsDef.annots.map(annot => AnnotInfo(annot.tycon.toString)) + + for (defDef <- clsDef.template.meths) { + val annots = defDef.annots.map(annot => AnnotInfo(annot.tycon.toString)) + for (meth <- cls.lookupClassMethods(new MethodInfo(cls, defDef.name.source, 0, "()V"))) + meth._annotations ++= annots + } + } + } + } + } + + def unpickleTree(in: TastyReader, names: Names): Tree = { + import in._ + + def readName() = names(readNat()) + def skipTree(tag: Int) = { skipTreeTagged(in, tag); UnknownTree(tag) } + def readTypeRefPkg() = TypeRefPkg(readName()) // fullyQualified_NameRef -- A reference to a package member with given fully qualified name + def readTypeRef() = TypeRef(name = readName(), qual = readType()) // NameRef qual_Type -- A reference `qual.name` to a non-local member + def readAnnot() = { readEnd(); Annot(readType(), skipTree(readByte())) } // tycon_Type fullAnnotation_Tree -- An annotation, given (class) type of constructor, and full application tree + def readSharedType() = unpickleTree(forkAt(readAddr()), names).asInstanceOf[Type] + + def readPath() = readByte() match { + case TERMREFpkg => readTypeRefPkg() + case tag => skipTree(tag); UnknownPath(tag) + } + + def readType(): Type = readByte() match { + case TYPEREF => readTypeRef() + case TERMREFpkg => readTypeRefPkg() + case SHAREDtype => readSharedType() + case tag => skipTree(tag); UnknownType(tag) + } + + def readTree(): Tree = { + val start = currentAddr + val tag = readByte() + + def processLengthTree() = { + def readTrees(end: Addr) = until(end)(readTree()).filter(!_.isInstanceOf[UnknownTree]) + def readPackage() = { val end = readEnd(); Pkg(readPath(), readTrees(end)) } // Path Tree* -- package path { topLevelStats } + + def nothingButMods(end: Addr) = currentAddr == end || isModifierTag(nextByte) + + def readDefDef() = { + // Length NameRef Param* returnType_Term rhs_Term? Modifier* -- modifiers def name [typeparams] paramss : returnType (= rhs)? + // Param = TypeParam | TermParam + val end = readEnd() + val name = readName() + while (nextByte == TYPEPARAM || nextByte == PARAM || nextByte == EMPTYCLAUSE || nextByte == SPLITCLAUSE) skipTree(readByte()) // params + skipTree(readByte()) // returnType + if (!nothingButMods(end)) skipTree(readByte()) // rhs + val annots = readAnnotsInMods(end) + DefDef(name, annots) + } + + def readTemplate(): Template = { + // TypeParam* TermParam* parent_Term* Self? Stat* -- [typeparams] paramss extends parents { self => stats }, where Stat* always starts with the primary constructor. + // TypeParam = TYPEPARAM Length NameRef type_Term Modifier* -- modifiers name bounds + // TermParam = PARAM Length NameRef type_Term Modifier* -- modifiers name : type. + // EMPTYCLAUSE -- an empty parameter clause () + // SPLITCLAUSE -- splits two non-empty parameter clauses of the same kind + // Self = SELFDEF selfName_NameRef selfType_Term -- selfName : selfType + assert(readByte() == TEMPLATE) + val end = readEnd() + while (nextByte == TYPEPARAM) skipTree(readByte()) // vparams + while (nextByte == PARAM || nextByte == EMPTYCLAUSE || nextByte == SPLITCLAUSE) skipTree(readByte()) // tparams + while (nextByte != SELFDEF && nextByte != DEFDEF) skipTree(readByte()) // parents + if (nextByte == SELFDEF) skipTree(readByte()) // self + val classes = new ListBuffer[ClsDef] + val meths = new ListBuffer[DefDef] + doUntil(end)(readByte match { + case TYPEDEF => readTypeDef() match { case clsDef: ClsDef => classes += clsDef case _ => } + case DEFDEF => meths += readDefDef() + case tag => skipTree(tag) + }) + Template(classes.toList, meths.toList) + } + + def readClassDef(name: Name, end: Addr) = ClsDef(name.toTypeName, readTemplate(), readAnnotsInMods(end)) // NameRef Template Modifier* -- modifiers class name template + + def readTypeMemberDef(end: Addr) = { goto(end); UnknownTree(TYPEDEF) } // NameRef type_Term Modifier* -- modifiers type name (= type | bounds) + + def readAnnotsInMods(end: Addr) = { + val annots = new ListBuffer[Annot] + doUntil(end)(readByte() match { + case ANNOTATION => annots += readAnnot() + case tag if isModifierTag(tag) => skipTree(tag) + case tag => assert(false, s"illegal modifier tag $tag at ${currentAddr.index - 1}, end = $end") + }) + annots.toList + } + + def readTypeDef() = { + val end = readEnd() + val name = readName() + if (nextByte == TEMPLATE) readClassDef(name, end) else readTypeMemberDef(end) + } + + val end = fork.readEnd() + val tree = tag match { + case PACKAGE => readPackage() + case TYPEDEF => readTypeDef() + case _ => skipTree(tag) + } + softAssertEnd(end, s" start=$start tag=${astTagToString(tag)}") + tree + } + + astCategory(tag) match { + case AstCat1TagOnly => skipTree(tag) + case AstCat2Nat => tag match { + case TERMREFpkg => readTypeRefPkg() + case _ => skipTree(tag) + } + case AstCat3AST => readTree() + case AstCat4NatAST => tag match { + case TYPEREF => readTypeRef() + case _ => skipTree(tag) + } + case AstCat5Length => processLengthTree() + } + } + + readTree() + } + + def skipTreeTagged(in: TastyReader, tag: Int): Unit = { + import in._ + astCategory(tag) match { + case AstCat1TagOnly => + case AstCat2Nat => readNat() + case AstCat3AST => skipTreeTagged(in, in.readByte()) + case AstCat4NatAST => readNat(); skipTreeTagged(in, in.readByte()) + case AstCat5Length => goto(readEnd()) + } + } + + sealed trait Tree extends ShowSelf + + final case class UnknownTree(tag: Int) extends Tree { + val id = unknownTreeId.getAndIncrement() + def show = s"UnknownTree(${astTagToString(tag)})" //+ s"#$id" + } + var unknownTreeId = new java.util.concurrent.atomic.AtomicInteger() + + final case class Pkg(path: Path, trees: List[Tree]) extends Tree { def show = s"package $path${trees.map("\n " + _).mkString}" } + + final case class ClsDef(name: TypeName, template: Template, annots: List[Annot]) extends Tree { def show = s"${annots.map("" + _ + " ").mkString}class $name$template" } + final case class Template(classes: List[ClsDef], meths: List[DefDef]) extends Tree { def show = s"${(classes ::: meths).map("\n " + _).mkString}" } + final case class DefDef(name: Name, annots: List[Annot] = Nil) extends Tree { def show = s"${annots.map("" + _ + " ").mkString}def $name" } + + sealed trait Type extends Tree + final case class UnknownType(tag: Int) extends Type { def show = s"UnknownType(${astTagToString(tag)})" } + final case class TypeRef(qual: Type, name: Name) extends Type { def show = s"$qual.$name" } + + sealed trait Path extends Type + final case class UnknownPath(tag: Int) extends Path { def show = s"UnknownPath(${astTagToString(tag)})" } + final case class TypeRefPkg(fullyQual: Name) extends Path { def show = s"$fullyQual" } + + final case class Annot(tycon: Type, fullAnnotation: Tree) extends Tree { def show = s"@$tycon" } + + sealed class Traverser { + def traverse(tree: Tree): Unit = tree match { + case pkg: Pkg => traversePkg(pkg) + case clsDef: ClsDef => traverseClsDef(clsDef) + case tmpl: Template => traverseTemplate(tmpl) + case defDef: DefDef => traverseDefDef(defDef) + case tp: Type => traverseType(tp) + case annot: Annot => traverseAnnot(annot) + case UnknownTree(_) => + } + + def traverseName(name: Name) = () + + def traversePkg(pkg: Pkg) = { val Pkg(path, trees) = pkg; traverse(path); trees.foreach(traverse) } + def traverseClsDef(clsDef: ClsDef) = { val ClsDef(name, tmpl, annots) = clsDef; traverseName(name); traverseTemplate(tmpl); annots.foreach(traverse) } + def traverseTemplate(tmpl: Template) = { val Template(classes, meths) = tmpl; classes.foreach(traverse); meths.foreach(traverse) } + def traverseDefDef(defDef: DefDef) = { val DefDef(name, annots) = defDef; traverseName(name); annots.foreach(traverse) } + def traverseAnnot(annot: Annot) = { val Annot(tycon, fullAnnotation) = annot; traverse(tycon); traverse(fullAnnotation) } + + def traversePath(path: Path) = path match { + case TypeRefPkg(fullyQual) => traverseName(fullyQual) + case UnknownPath(_) => + } + + def traverseType(tp: Type): Unit = tp match { + case path: Path => traversePath(path) + case TypeRef(qual, name) => traverse(qual); traverseName(name) + case UnknownType(_) => + } + } + + class ForeachTraverser(f: Tree => Unit) extends Traverser { + override def traverse(t: Tree) = { f(t); super.traverse(t) } + } + + class FilterTraverser(p: Tree => Boolean) extends Traverser { + val hits = mutable.ListBuffer[Tree]() + override def traverse(t: Tree) = { if (p(t)) hits += t; super.traverse(t) } + } + + class CollectTraverser[T](pf: PartialFunction[Tree, T]) extends Traverser { + val results = mutable.ListBuffer[T]() + override def traverse(tree: Tree): Unit = { pf.runWith(results += _)(tree); super.traverse(tree) } + } + + def getTreeReader(in: TastyReader, names: Names): TastyReader = { + getSectionReader(in, names, ASTsSection).getOrElse(sys.error(s"No $ASTsSection section?!")) + } + + def getSectionReader(in: TastyReader, names: Names, name: String): Option[TastyReader] = { + import in._ + @tailrec def loop(): Option[TastyReader] = { + if (isAtEnd) None + else if (names(readNat()).debug == name) Some(nextSectionReader(in)) + else { goto(readEnd()); loop() } + } + loop() + } + + private def nextSectionReader(in: TastyReader) = { + import in._ + val end = readEnd() + val curr = currentAddr + goto(end) + new TastyReader(bytes, curr.index, end.index, curr.index) + } + + def readNames(in: TastyReader): Names = { + val names = new ArrayBuffer[Name] + in.doUntil(in.readEnd())(names += readNewName(in, names)) + names.toIndexedSeq + } + + private def readNewName(in: TastyReader, names: ArrayBuffer[Name]): Name = { + import in._ + + val tag = readByte() + val end = readEnd() + + def nameAtIdx(idx: Int) = try names(idx) catch { + case e: ArrayIndexOutOfBoundsException => + throw new Exception(s"trying to read name @ idx=$idx tag=${nameTagToString(tag)}", e) + } + + def readName() = nameAtIdx(readNat()) + + def readParamSig(): ParamSig[ErasedTypeRef] = { + val ref = readInt() + Either.cond(ref >= 0, ErasedTypeRef(nameAtIdx(ref)), ref.abs) + } + + def readSignedRest(orig: Name, target: Name) = { + SignedName(orig, new MethodSignature(result = ErasedTypeRef(readName()), params = until(end)(readParamSig())), target) + } + + val name = tag match { + case UTF8 => val start = currentAddr; goto(end); SimpleName(new String(bytes.slice(start.index, end.index), "UTF-8")) + case QUALIFIED => QualifiedName(readName(), Name.PathSep, readName().asSimpleName) + case EXPANDED => QualifiedName(readName(), Name.ExpandedSep, readName().asSimpleName) + case EXPANDPREFIX => QualifiedName(readName(), Name.ExpandPrefixSep, readName().asSimpleName) + + case UNIQUE => UniqueName(sep = readName().asSimpleName, num = readNat(), qual = ifBefore(end)(readName(), Name.Empty)) + case DEFAULTGETTER => DefaultName(readName(), readNat()) + + case SUPERACCESSOR => PrefixName( Name.SuperPrefix, readName()) + case INLINEACCESSOR => PrefixName(Name.InlinePrefix, readName()) + case BODYRETAINER => SuffixName(readName(), Name.BodyRetainerSuffix) + case OBJECTCLASS => ObjectName(readName()) + + case SIGNED => val name = readName(); readSignedRest(name, name) + case TARGETSIGNED => readSignedRest(readName(), readName()) + + case _ => sys.error(s"at NameRef(${names.size}): name `${readName().debug}` is qualified by tag ${nameTagToString(tag)}") + } + assertEnd(end, s" bad name=${name.debug}") + name + } + + type Names = IndexedSeq[Name] + + sealed abstract class Name extends ShowSelf { + final def asSimpleName = this match { + case x: SimpleName => x + case _ => throw new AssertionError(s"not simplename: $debug") + } + + final def toTermName = this match { + case TypeName(name) => name + case name => name + } + + final def isObjectName = this.isInstanceOf[ObjectName] + final def toTypeName = TypeName(this) + final def source = SourceEncoder.encode(this) + final def debug = DebugEncoder.encode(this) + + final def show = source + } + + final case class SimpleName(raw: String) extends Name + final case class ObjectName(base: Name) extends Name + final case class TypeName private (base: Name) extends Name + final case class QualifiedName(qual: Name, sep: SimpleName, sel: SimpleName) extends Name + + final case class UniqueName(qual: Name, sep: SimpleName, num: Int) extends Name + final case class DefaultName(qual: Name, num: Int) extends Name + + final case class PrefixName(pref: SimpleName, qual: Name) extends Name + final case class SuffixName(qual: Name, suff: SimpleName) extends Name + + final case class SignedName(qual: Name, sig: MethodSignature[ErasedTypeRef], target: Name) extends Name + + object Name { + val Empty = SimpleName("") + val PathSep = SimpleName(".") + val ExpandedSep = SimpleName("$$") + val ExpandPrefixSep = SimpleName("$") + val InlinePrefix = SimpleName("inline$") + val SuperPrefix = SimpleName("super$") + val BodyRetainerSuffix = SimpleName("$retainedBody") + val Constructor = SimpleName("") + } + + object nme { + def NoSymbol = SimpleName("NoSymbol") + def Empty = SimpleName("") + } + + type ParamSig[T] = Either[Int, T] + + sealed abstract class Signature[+T] { + final def show = this match { + case MethodSignature(params, result) => params.map(_.merge).mkString("(", ",", ")") + result + } + } + + final case class MethodSignature[T](params: List[ParamSig[T]], result: T) extends Signature[T] { + def map[U](f: T => U): MethodSignature[U] = this match { + case MethodSignature(params, result) => MethodSignature(params.map(_.map(f)), f(result)) + } + } + + final case class ErasedTypeRef(qualifiedName: TypeName, arrayDims: Int) { + def signature: String = { + val qualified = qualifiedName.source + val pref = if (qualifiedName.toTermName.isObjectName) "object " else "" + s"${"[" * arrayDims}$pref$qualified" + } + } + + object ErasedTypeRef { + private val InnerRegex = """(.*)\[\]""".r + + def apply(tname: Name): ErasedTypeRef = { + def name(qual: Name, tname: SimpleName, isModule: Boolean) = { + val qualified = if (qual == Name.Empty) tname else QualifiedName(qual, Name.PathSep, tname) + if (isModule) ObjectName(qualified) else qualified + } + def specialised(qual: Name, terminal: String, isModule: Boolean, arrayDims: Int = 0): ErasedTypeRef = terminal match { + case InnerRegex(inner) => specialised(qual, inner, isModule, arrayDims + 1) + case clazz => ErasedTypeRef(name(qual, SimpleName(clazz), isModule).toTypeName, arrayDims) + } + def transform(name: Name, isModule: Boolean = false): ErasedTypeRef = name match { + case ObjectName(classKind) => transform(classKind, isModule = true) + case SimpleName(raw) => specialised(Name.Empty, raw, isModule) // unqualified in the package + case QualifiedName(path, Name.PathSep, sel) => specialised(path, sel.raw, isModule) + case _ => throw new AssertionError(s"Unexpected erased type ref ${name.debug}") + } + transform(tname.toTermName) + } + } + + sealed trait NameEncoder[U] { + final def encode[O](name: Name)(init: => U, finish: U => O) = finish(traverse(init, name)) + def traverse(u: U, name: Name): U + } + + sealed trait StringBuilderEncoder extends NameEncoder[StringBuilder] { + final def encode(name: Name) = name match { + case SimpleName(raw) => raw + case _ => super.encode(name)(new StringBuilder(25), _.toString) + } + } + + object SourceEncoder extends StringBuilderEncoder { + def traverse(sb: StringBuilder, name: Name): StringBuilder = name match { + case Name.Constructor => sb + case SimpleName(raw) => sb ++= raw + case ObjectName(base) => traverse(sb, base) + case TypeName(base) => traverse(sb, base) + case SignedName(qual, _, _) => traverse(sb, qual) + case UniqueName(qual, sep, num) => traverse(sb, qual) ++= sep.raw ++= s"$num" + case QualifiedName(qual, sep, sel) => traverse(sb, qual) ++= sep.raw ++= sel.raw + case PrefixName(pref, qual) => traverse(sb ++= pref.raw, qual) + case SuffixName(qual, suff) => traverse(sb, qual) ++= suff.raw + case DefaultName(qual, num) => traverse(sb, qual) ++= s"$$default$$${num + 1}" + } + } + + object DebugEncoder extends StringBuilderEncoder { + def traverse(sb: StringBuilder, name: Name): StringBuilder = name match { + case SimpleName(raw) => sb ++= raw + case DefaultName(qual, num) => traverse(sb, qual) ++= "[Default " ++= s"${num + 1}" += ']' + case PrefixName(pref, qual) => traverse(sb, qual) ++= "[Prefix " ++= pref.raw += ']' + case SuffixName(qual, suff) => traverse(sb, qual) ++= "[Suffix " ++= suff.raw += ']' + case ObjectName(base) => traverse(sb, base) ++= "[ModuleClass]" + case TypeName(base) => traverse(sb, base) ++= "[Type]" + case SignedName(name, sig, target) => traverse(sb, name) ++= "[Signed (" ++= sig.map(_.signature).show ++= " @" ++= target.source += ']' + case QualifiedName(qual, sep, name) => traverse(sb, qual) ++= "[Qualified " ++= sep.raw += ' ' ++= name.raw += ']' + case UniqueName(qual, sep, num) => traverse(sb, qual) ++= "[Unique " ++= sep.raw += ' ' ++= s"$num" += ']' + } + } + + final case class Header( + val header: (Int, Int, Int, Int), + val version: (Int, Int, Int), + val toolingVersion: String, + val uuid: UUID, + ) + + def readHeader(in: TastyReader) = { + import in._ + + def readToolingVersion() = { + val toolingLen = readNat() + val toolingVersion = new String(bytes, currentAddr.index, toolingLen) + goto(currentAddr + toolingLen) + toolingVersion + } + + Header( + header = (readByte(), readByte(), readByte(), readByte()), + version = (readNat(), readNat(), readNat()), + toolingVersion = readToolingVersion(), + uuid = new UUID(readUncompressedLong(), readUncompressedLong()), + ) + } + + trait ShowSelf extends Any { + def show: String + override def toString = show + } +} diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/TastyVersionCompatibility.scala b/core/src/main/scala/com/typesafe/tools/mima/core/TastyVersionCompatibility.scala new file mode 100644 index 000000000..c7653e5d9 --- /dev/null +++ b/core/src/main/scala/com/typesafe/tools/mima/core/TastyVersionCompatibility.scala @@ -0,0 +1,45 @@ +package com.typesafe.tools.mima.core + +import TastyFormat._ + +object TastyVersionCompatibility { + /** This method implements a binary relation (`<:<`) between two TASTy versions. + * + * Given the TASTy version of a `file` and the TASTy version of the `compiler`, + * if `file <:< compiler` then the TASTy file is valid to be read. + * + * A TASTy version, e.g. `v := 28.0-3` is composed of three fields: + * - v.major == 28 + * - v.minor == 0 + * - v.experimental == 3 + * + * TASTy versions have a partial order, for example, + * `a <:< b` and `b <:< a` are both false if + * - `a` and `b` have different `major` fields. + * - `a` and `b` have the same `major` & `minor` fields, + * but different, non-zero, `experimental` fields. + * + * A TASTy version with a zero value for its `experimental` field + * is considered to be stable. Files with a stable TASTy version + * can be read by a compiler with an unstable TASTy version, + * (where the compiler's TASTy version has a higher `minor` field). + * + * A compiler with a stable TASTy version can never read a file + * with an unstable TASTy version. + * + * We follow the given algorithm: + * + * ``` + * file match + * case (MajorVersion, MinorVersion, ExperimentalVersion) => true // full equality + * case (MajorVersion, minor, 0) if minor < MinorVersion => true // stable backwards compatibility + * case _ => false + * ``` + */ + def isVersionCompatible(fileMajor: Int, fileMinor: Int, fileExperimental: Int): Boolean = ( + fileMajor == MajorVersion && + ( fileMinor == MinorVersion && fileExperimental == ExperimentalVersion // full equality + || fileMinor < MinorVersion && fileExperimental == 0 // stable backwards compatibility + ) + ) +} diff --git a/core/src/main/scala/com/typesafe/tools/mima/lib/MiMaLib.scala b/core/src/main/scala/com/typesafe/tools/mima/lib/MiMaLib.scala index 8999c04a4..63adc46c2 100644 --- a/core/src/main/scala/com/typesafe/tools/mima/lib/MiMaLib.scala +++ b/core/src/main/scala/com/typesafe/tools/mima/lib/MiMaLib.scala @@ -20,21 +20,21 @@ final class MiMaLib(cp: Seq[File], log: Logging = ConsoleLogging) { pkg } - private def traversePackages(oldpkg: PackageInfo, newpkg: PackageInfo): List[Problem] = { + private def traversePackages(oldpkg: PackageInfo, newpkg: PackageInfo, excludeAnnots: List[AnnotInfo]): List[Problem] = { log.verbose(s"traversing $oldpkg") - Analyzer.analyze(oldpkg, newpkg, log) ++ oldpkg.packages.values.toSeq.sortBy(_.name).flatMap { p => + Analyzer.analyze(oldpkg, newpkg, log, excludeAnnots) ++ oldpkg.packages.values.toSeq.sortBy(_.name).flatMap { p => val q = newpkg.packages.getOrElse(p.name, NoPackageInfo) - traversePackages(p, q) + traversePackages(p, q, excludeAnnots) } } /** Return a list of problems for the two versions of the library. */ - def collectProblems(oldJarOrDir: File, newJarOrDir: File): List[Problem] = { + def collectProblems(oldJarOrDir: File, newJarOrDir: File, excludeAnnots: List[String]): List[Problem] = { val oldPackage = createPackage(oldJarOrDir) val newPackage = createPackage(newJarOrDir) log.debug(s"[old version in: ${oldPackage.definitions}]") log.debug(s"[new version in: ${newPackage.definitions}]") log.debug(s"classpath: ${classpath.asClassPathString}") - traversePackages(oldPackage, newPackage) + traversePackages(oldPackage, newPackage, excludeAnnots.map(AnnotInfo(_))) } } diff --git a/core/src/main/scala/com/typesafe/tools/mima/lib/analyze/Analyzer.scala b/core/src/main/scala/com/typesafe/tools/mima/lib/analyze/Analyzer.scala index 95e320334..8dc8c419e 100644 --- a/core/src/main/scala/com/typesafe/tools/mima/lib/analyze/Analyzer.scala +++ b/core/src/main/scala/com/typesafe/tools/mima/lib/analyze/Analyzer.scala @@ -7,7 +7,7 @@ import com.typesafe.tools.mima.lib.analyze.method.MethodChecker import com.typesafe.tools.mima.lib.analyze.template.TemplateChecker object Analyzer { - def analyze(oldpkg: PackageInfo, newpkg: PackageInfo, log: Logging): List[Problem] = { + def analyze(oldpkg: PackageInfo, newpkg: PackageInfo, log: Logging, excludeAnnots: List[AnnotInfo]): List[Problem] = { for { oldclazz <- oldpkg.accessibleClasses.toList.sortBy(_.bytecodeName) _ = log.verbose(s"analyzing $oldclazz") @@ -15,8 +15,9 @@ object Analyzer { // if it is missing a trait implementation class, then no error should be reported // since there should be already errors, i.e., missing methods... if !oldclazz.isImplClass + if !excludeAnnots.exists(oldclazz.annotations.contains) problem <- newpkg.classes.get(oldclazz.bytecodeName) match { - case Some(newclazz) => analyze(oldclazz, newclazz, log) + case Some(newclazz) => analyze(oldclazz, newclazz, log, excludeAnnots) case None => List(MissingClassProblem(oldclazz)) } } yield { @@ -25,7 +26,7 @@ object Analyzer { } } - def analyze(oldclazz: ClassInfo, newclazz: ClassInfo, log: Logging): List[Problem] = { + def analyze(oldclazz: ClassInfo, newclazz: ClassInfo, log: Logging, excludeAnnots: List[AnnotInfo]): List[Problem] = { if ((if (newclazz.isModuleClass) newclazz.module else newclazz).isScopedPrivate) Nil else { TemplateChecker.check(oldclazz, newclazz) match { @@ -36,7 +37,7 @@ object Analyzer { case maybeProblem => maybeProblem.toList ::: FieldChecker.check(oldclazz, newclazz) ::: - MethodChecker.check(oldclazz, newclazz) + MethodChecker.check(oldclazz, newclazz, excludeAnnots) } } } diff --git a/core/src/main/scala/com/typesafe/tools/mima/lib/analyze/method/MethodChecker.scala b/core/src/main/scala/com/typesafe/tools/mima/lib/analyze/method/MethodChecker.scala index 09b22dcbb..c4f5e4b48 100644 --- a/core/src/main/scala/com/typesafe/tools/mima/lib/analyze/method/MethodChecker.scala +++ b/core/src/main/scala/com/typesafe/tools/mima/lib/analyze/method/MethodChecker.scala @@ -3,24 +3,24 @@ package com.typesafe.tools.mima.lib.analyze.method import com.typesafe.tools.mima.core._ private[analyze] object MethodChecker { - def check(oldclazz: ClassInfo, newclazz: ClassInfo): List[Problem] = - checkExisting(oldclazz, newclazz) ::: checkNew(oldclazz, newclazz) + def check(oldclazz: ClassInfo, newclazz: ClassInfo, excludeAnnots: List[AnnotInfo]): List[Problem] = + checkExisting(oldclazz, newclazz, excludeAnnots) ::: checkNew(oldclazz, newclazz, excludeAnnots) /** Analyze incompatibilities that may derive from changes in existing methods. */ - private def checkExisting(oldclazz: ClassInfo, newclazz: ClassInfo): List[Problem] = { - for (oldmeth <- oldclazz.methods.value; problem <- checkExisting1(oldmeth, newclazz)) yield problem + private def checkExisting(oldclazz: ClassInfo, newclazz: ClassInfo, excludeAnnots: List[AnnotInfo]): List[Problem] = { + for (oldmeth <- oldclazz.methods.value; problem <- checkExisting1(oldmeth, newclazz, excludeAnnots)) yield problem } /** Analyze incompatibilities that may derive from new methods in `newclazz`. */ - private def checkNew(oldclazz: ClassInfo, newclazz: ClassInfo): List[Problem] = { + private def checkNew(oldclazz: ClassInfo, newclazz: ClassInfo, excludeAnnots: List[AnnotInfo]): List[Problem] = { val problems1 = if (newclazz.isClass) Nil else checkEmulatedConcreteMethodsProblems(oldclazz, newclazz) - val problems2 = checkDeferredMethodsProblems(oldclazz, newclazz) - val problems3 = checkInheritedNewAbstractMethodProblems(oldclazz, newclazz) + val problems2 = checkDeferredMethodsProblems(oldclazz, newclazz, excludeAnnots) + val problems3 = checkInheritedNewAbstractMethodProblems(oldclazz, newclazz, excludeAnnots) problems1 ::: problems2 ::: problems3 } - private def checkExisting1(oldmeth: MethodInfo, newclazz: ClassInfo): Option[Problem] = { - if (oldmeth.nonAccessible) + private def checkExisting1(oldmeth: MethodInfo, newclazz: ClassInfo, excludeAnnots: List[AnnotInfo]): Option[Problem] = { + if (oldmeth.nonAccessible || excludeAnnots.exists(oldmeth.annotations.contains)) None else if (newclazz.isClass) { if (oldmeth.isDeferred) @@ -135,9 +135,10 @@ private[analyze] object MethodChecker { } yield problem }.toList - private def checkDeferredMethodsProblems(oldclazz: ClassInfo, newclazz: ClassInfo): List[Problem] = { + private def checkDeferredMethodsProblems(oldclazz: ClassInfo, newclazz: ClassInfo, excludeAnnots: List[AnnotInfo]): List[Problem] = { for { newmeth <- newclazz.deferredMethods.iterator + if !excludeAnnots.exists(newmeth.annotations.contains) problem <- oldclazz.lookupMethods(newmeth).find(_.descriptor == newmeth.descriptor) match { case None => Some(ReversedMissingMethodProblem(newmeth)) case Some(oldmeth) if newclazz.isClass && oldmeth.isConcrete => Some(ReversedAbstractMethodProblem(newmeth)) @@ -146,7 +147,7 @@ private[analyze] object MethodChecker { } yield problem }.toList - private def checkInheritedNewAbstractMethodProblems(oldclazz: ClassInfo, newclazz: ClassInfo): List[Problem] = { + private def checkInheritedNewAbstractMethodProblems(oldclazz: ClassInfo, newclazz: ClassInfo, excludeAnnots: List[AnnotInfo]): List[Problem] = { def allInheritedTypes(clazz: ClassInfo) = clazz.superClasses ++ clazz.allInterfaces val diffInheritedTypes = allInheritedTypes(newclazz).diff(allInheritedTypes(oldclazz)) @@ -158,6 +159,7 @@ private[analyze] object MethodChecker { newInheritedType <- diffInheritedTypes.iterator // if `newInheritedType` is a trait, then the trait's concrete methods should be counted as deferred methods newDeferredMethod <- newInheritedType.deferredMethodsInBytecode + if !excludeAnnots.exists(newDeferredMethod.annotations.contains) // checks that the newDeferredMethod did not already exist in one of the oldclazz supertypes if noInheritedMatchingMethod(oldclazz, newDeferredMethod)(_ => true) && // checks that no concrete implementation of the newDeferredMethod is provided by one of the newclazz supertypes diff --git a/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/AppRunTest.scala b/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/AppRunTest.scala index 50aad470e..64a77e142 100644 --- a/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/AppRunTest.scala +++ b/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/AppRunTest.scala @@ -23,7 +23,7 @@ object AppRunTest { case _ => Success(()) } () <- testCase.runMain(v2) match { // test: run app, compiled with v1, with v2 - case Failure(t) if !pending && expectOk => Failure(t) + case Failure(t) if !pending && expectOk => Failure(t) case Success(()) if !pending && !expectOk => Failure(new Exception("expected running App to fail")) case _ => Success(()) } diff --git a/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/CollectProblemsTest.scala b/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/CollectProblemsTest.scala index d4fd3aa4d..eccebe28a 100644 --- a/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/CollectProblemsTest.scala +++ b/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/CollectProblemsTest.scala @@ -18,7 +18,7 @@ object CollectProblemsTest { def collectProblems(cp: Seq[File], v1: File, v2: File, direction: Direction): List[Problem] = { val (lhs, rhs) = direction.ordered(v1, v2) - new MiMaLib(cp).collectProblems(lhs, rhs) + new MiMaLib(cp).collectProblems(lhs, rhs, excludeAnnots) } def readOracleFile(oracleFile: File): List[String] = { @@ -48,7 +48,9 @@ object CollectProblemsTest { case "\n" => Success(()) case msg => Console.err.println(msg) - Failure(new Exception("CollectProblemsTest failure")) + Failure(new Exception("CollectProblemsTest failure", null, false, false) {}) } } + + private val excludeAnnots = List("mima.annotation.exclude") } diff --git a/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/IntegrationTests.scala b/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/IntegrationTests.scala index ee878973c..999b06c71 100644 --- a/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/IntegrationTests.scala +++ b/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/IntegrationTests.scala @@ -65,19 +65,24 @@ object IntegrationTests { object CompareJars { def main(args: Array[String]): Unit = args.toList match { + case Seq(file) => runTry(compare(new File(file), new File(file), Nil)) case Seq(groupId, artifactId, version1, version2, attrStrs @ _*) => - val direction = Backwards val attrs = attrStrs.map { s => val Array(k, v) = s.split('='); k -> v }.toMap val module = Module(Organization(groupId), ModuleName(artifactId)).withAttributes(attrs) - val tri = for { - (v1, _) <- IntegrationTests.fetchArtifact(Dependency(module, version1)) + runTry(for { + (v1, _) <- IntegrationTests.fetchArtifact(Dependency(module, version1)) (v2, cp) <- IntegrationTests.fetchArtifact(Dependency(module, version2)) - problems = CollectProblemsTest.collectProblems(cp, v1, v2, direction) - () <- CollectProblemsTest.diffProblems(problems, Nil, direction) - } yield () - tri match { - case Success(()) => - case Failure(exc) => System.err.println(s"$exc"); throw new Exception("fail") - } + () <- compare(v1, v2, cp) + } yield ()) + } + + def compare(v1: File, v2: File, cp: Seq[File], direction: Direction = Backwards): Try[Unit] = { + val problems = CollectProblemsTest.collectProblems(cp, v1, v2, direction) + CollectProblemsTest.diffProblems(problems, Nil, direction) + } + + def runTry(tri: Try[Unit]) = tri match { + case Success(()) => + case Failure(exc) => System.err.println(s"$exc"); throw new Exception("fail") } } diff --git a/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/Test.scala b/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/Test.scala index 005a845d5..db4238767 100644 --- a/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/Test.scala +++ b/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/Test.scala @@ -17,10 +17,10 @@ object Test { val failureNames = failures.map { case ((t1, _)) => t1.name } println("Failures:") failureNames.foreach(name => println(s"* $name")) - println(s"functional-tests/Test/run ${failureNames.mkString(" ")}") + println(s"Run: functional-tests/Test/run ${failureNames.mkString(" ")}") } failures.foldLeft(Try(())) { - case (res @ Failure(e1), (_, Failure(e2))) => e1.addSuppressed(e2); res + case (res @ Failure(e1), (_, Failure(e2))) => if (e1 != e2) e1.addSuppressed(e2); res case (res @ Failure(_), _) => res case (_, (_, res)) => res } diff --git a/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/TestCase.scala b/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/TestCase.scala index ca286b0ac..7c6fd9ec6 100644 --- a/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/TestCase.scala +++ b/functional-tests/src/main/scala/com/typesafe/tools/mima/lib/TestCase.scala @@ -108,10 +108,12 @@ final class TestCase(val baseDir: Directory, val scalaCompiler: ScalaCompiler, v val p = baseDir.resolve(path).toFile val p211 = (p.parent / (s"${p.stripExtension}-2.11")).addExtension(p.extension).toFile val p212 = (p.parent / (s"${p.stripExtension}-2.12")).addExtension(p.extension).toFile + val p213 = (p.parent / (s"${p.stripExtension}-2.13")).addExtension(p.extension).toFile val p3 = (p.parent / (s"${p.stripExtension}-3" )).addExtension(p.extension).toFile scalaBinaryVersion match { case "2.11" => if (p211.exists) p211 else if (p212.exists) p212 else p case "2.12" => if (p212.exists) p212 else p + case "2.13" => if (p213.exists) p213 else if (p212.exists) p212 else p case "3" => if (p3.exists) p3 else p case _ => p } diff --git a/functional-tests/src/test/changes-in-experimental-are-ok/app/App.scala b/functional-tests/src/test/changes-in-experimental-are-ok/app/App.scala new file mode 100644 index 000000000..f283548d3 --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-are-ok/app/App.scala @@ -0,0 +1,3 @@ +object App { + def main(args: Array[String]): Unit = () +} diff --git a/functional-tests/src/test/changes-in-experimental-are-ok/problems.txt b/functional-tests/src/test/changes-in-experimental-are-ok/problems.txt new file mode 100644 index 000000000..e69de29bb diff --git a/functional-tests/src/test/changes-in-experimental-are-ok/v1/A.scala b/functional-tests/src/test/changes-in-experimental-are-ok/v1/A.scala new file mode 100644 index 000000000..fb063b5dc --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-are-ok/v1/A.scala @@ -0,0 +1,14 @@ +package mima +package pkg2 + +import mima.annotation.exclude + +@exclude +class Foo { + def foo = 1 +} + +@annotation.exclude +class Bar { + def foo = 1 +} diff --git a/functional-tests/src/test/changes-in-experimental-are-ok/v1/exclude.scala b/functional-tests/src/test/changes-in-experimental-are-ok/v1/exclude.scala new file mode 100644 index 000000000..5cee6bbff --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-are-ok/v1/exclude.scala @@ -0,0 +1,5 @@ +package mima.annotation + +import scala.annotation.StaticAnnotation + +class exclude extends StaticAnnotation diff --git a/functional-tests/src/test/changes-in-experimental-are-ok/v2/A.scala b/functional-tests/src/test/changes-in-experimental-are-ok/v2/A.scala new file mode 100644 index 000000000..d3c066ab8 --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-are-ok/v2/A.scala @@ -0,0 +1,14 @@ +package mima +package pkg2 + +import mima.annotation.exclude + +@exclude +class Foo { + def foo = "1" +} + +@annotation.exclude +class Bar { + def foo = "1" +} diff --git a/functional-tests/src/test/changes-in-experimental-are-ok/v2/exclude.scala b/functional-tests/src/test/changes-in-experimental-are-ok/v2/exclude.scala new file mode 100644 index 000000000..5cee6bbff --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-are-ok/v2/exclude.scala @@ -0,0 +1,5 @@ +package mima.annotation + +import scala.annotation.StaticAnnotation + +class exclude extends StaticAnnotation diff --git a/functional-tests/src/test/changes-in-experimental-methods-are-ok/app/App.scala b/functional-tests/src/test/changes-in-experimental-methods-are-ok/app/App.scala new file mode 100644 index 000000000..f283548d3 --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-methods-are-ok/app/App.scala @@ -0,0 +1,3 @@ +object App { + def main(args: Array[String]): Unit = () +} diff --git a/functional-tests/src/test/changes-in-experimental-methods-are-ok/problems-2.12.txt b/functional-tests/src/test/changes-in-experimental-methods-are-ok/problems-2.12.txt new file mode 100644 index 000000000..e13ee3c5f --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-methods-are-ok/problems-2.12.txt @@ -0,0 +1,3 @@ +# Haven't implemented the ScalaSig unpickling part, only TASTy +method foo()Int in class mima.pkg2.Foo has a different result type in new version, where it is java.lang.String rather than Int +method bar()Int in class mima.pkg2.Foo has a different result type in new version, where it is java.lang.String rather than Int diff --git a/functional-tests/src/test/changes-in-experimental-methods-are-ok/problems.txt b/functional-tests/src/test/changes-in-experimental-methods-are-ok/problems.txt new file mode 100644 index 000000000..e69de29bb diff --git a/functional-tests/src/test/changes-in-experimental-methods-are-ok/testAppRun.pending b/functional-tests/src/test/changes-in-experimental-methods-are-ok/testAppRun.pending new file mode 100644 index 000000000..dfe987d7f --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-methods-are-ok/testAppRun.pending @@ -0,0 +1,2 @@ +# The change is breaking, but the problem is suppressed by the annotation +# This is here to not make the app run test fail diff --git a/functional-tests/src/test/changes-in-experimental-methods-are-ok/v1/A.scala b/functional-tests/src/test/changes-in-experimental-methods-are-ok/v1/A.scala new file mode 100644 index 000000000..a3c50b6e2 --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-methods-are-ok/v1/A.scala @@ -0,0 +1,12 @@ +package mima +package pkg2 + +import mima.annotation.exclude + +class Foo { + @exclude + def foo = 1 + + @annotation.exclude + def bar = 1 +} diff --git a/functional-tests/src/test/changes-in-experimental-methods-are-ok/v1/exclude.scala b/functional-tests/src/test/changes-in-experimental-methods-are-ok/v1/exclude.scala new file mode 100644 index 000000000..5cee6bbff --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-methods-are-ok/v1/exclude.scala @@ -0,0 +1,5 @@ +package mima.annotation + +import scala.annotation.StaticAnnotation + +class exclude extends StaticAnnotation diff --git a/functional-tests/src/test/changes-in-experimental-methods-are-ok/v2/A.scala b/functional-tests/src/test/changes-in-experimental-methods-are-ok/v2/A.scala new file mode 100644 index 000000000..22876e7af --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-methods-are-ok/v2/A.scala @@ -0,0 +1,12 @@ +package mima +package pkg2 + +import mima.annotation.exclude + +class Foo { + @exclude + def foo = "1" + + @annotation.exclude + def bar = "1" +} diff --git a/functional-tests/src/test/changes-in-experimental-methods-are-ok/v2/exclude.scala b/functional-tests/src/test/changes-in-experimental-methods-are-ok/v2/exclude.scala new file mode 100644 index 000000000..5cee6bbff --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-methods-are-ok/v2/exclude.scala @@ -0,0 +1,5 @@ +package mima.annotation + +import scala.annotation.StaticAnnotation + +class exclude extends StaticAnnotation diff --git a/functional-tests/src/test/changes-in-experimental-nested-are-ok/app/App.scala b/functional-tests/src/test/changes-in-experimental-nested-are-ok/app/App.scala new file mode 100644 index 000000000..f283548d3 --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-nested-are-ok/app/App.scala @@ -0,0 +1,3 @@ +object App { + def main(args: Array[String]): Unit = () +} diff --git a/functional-tests/src/test/changes-in-experimental-nested-are-ok/problems-2.12.txt b/functional-tests/src/test/changes-in-experimental-nested-are-ok/problems-2.12.txt new file mode 100644 index 000000000..73a6e5d8e --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-nested-are-ok/problems-2.12.txt @@ -0,0 +1,2 @@ +# Haven't implemented the ScalaSig unpickling part, only TASTy +abstract method errorAndAbort(java.lang.String)scala.runtime.Nothing# in interface mima.pkg2.Quotes#reflectModule#reportModule is present only in new version diff --git a/functional-tests/src/test/changes-in-experimental-nested-are-ok/problems.txt b/functional-tests/src/test/changes-in-experimental-nested-are-ok/problems.txt new file mode 100644 index 000000000..e69de29bb diff --git a/functional-tests/src/test/changes-in-experimental-nested-are-ok/testAppRun-2.12.pending b/functional-tests/src/test/changes-in-experimental-nested-are-ok/testAppRun-2.12.pending new file mode 100644 index 000000000..a2b8d278f --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-nested-are-ok/testAppRun-2.12.pending @@ -0,0 +1 @@ +# Haven't implemented the ScalaSig unpickling part, only TASTy diff --git a/functional-tests/src/test/changes-in-experimental-nested-are-ok/v1/A.scala b/functional-tests/src/test/changes-in-experimental-nested-are-ok/v1/A.scala new file mode 100644 index 000000000..c75a0c064 --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-nested-are-ok/v1/A.scala @@ -0,0 +1,10 @@ +package mima +package pkg2 + +trait Quotes { + val reflect: reflectModule + trait reflectModule { + val report: reportModule + trait reportModule + } +} diff --git a/functional-tests/src/test/changes-in-experimental-nested-are-ok/v2/A.scala b/functional-tests/src/test/changes-in-experimental-nested-are-ok/v2/A.scala new file mode 100644 index 000000000..7bac4f34e --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-nested-are-ok/v2/A.scala @@ -0,0 +1,15 @@ +package mima +package pkg2 + +import mima.annotation.exclude + +trait Quotes { + val reflect: reflectModule + trait reflectModule { + val report: reportModule + trait reportModule { + @exclude + def errorAndAbort(msg: String): Nothing + } + } +} diff --git a/functional-tests/src/test/changes-in-experimental-nested-are-ok/v2/exclude.scala b/functional-tests/src/test/changes-in-experimental-nested-are-ok/v2/exclude.scala new file mode 100644 index 000000000..5cee6bbff --- /dev/null +++ b/functional-tests/src/test/changes-in-experimental-nested-are-ok/v2/exclude.scala @@ -0,0 +1,5 @@ +package mima.annotation + +import scala.annotation.StaticAnnotation + +class exclude extends StaticAnnotation diff --git a/functional-tests/src/test/value-class-added-concrete-method-ok/problems-2.13.txt b/functional-tests/src/test/value-class-added-concrete-method-ok/problems-2.13.txt new file mode 100644 index 000000000..e69de29bb diff --git a/project/MimaSettings.scala b/project/MimaSettings.scala index be81a00ed..17d30575d 100644 --- a/project/MimaSettings.scala +++ b/project/MimaSettings.scala @@ -24,6 +24,11 @@ object MimaSettings { // * com.typesafe.tools.mima.core.ProblemFilters // * com.typesafe.tools.mima.core.*Problem // * com.typesafe.tools.mima.core.util.log.Logging + exclude[Problem]("*mima.core.Pickle*"), + exclude[Problem]("*mima.core.*pickle*"), + exclude[Problem]("*mima.lib.MiMaLib.*"), + exclude[Problem]("*mima.lib.analyze.Analyzer.*"), + exclude[Problem]("*mima.plugin.SbtMima.*"), ), ) } diff --git a/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/MimaKeys.scala b/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/MimaKeys.scala index 73bcd74b0..a23a48f22 100644 --- a/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/MimaKeys.scala +++ b/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/MimaKeys.scala @@ -11,6 +11,7 @@ class MimaKeys { final val mimaPreviousArtifacts = settingKey[Set[ModuleID]]("Previous released artifacts used to test binary compatibility.") final val mimaReportBinaryIssues = taskKey[Unit]("Logs all binary incompatibilities to the sbt console/logs.") + final val mimaExcludeAnnotations = settingKey[Seq[String]]("The fully-qualified class names of annotations that exclude problems") final val mimaBinaryIssueFilters = taskKey[Seq[ProblemFilter]]("Filters to apply to binary issues found. Applies both to backward and forward binary compatibility checking.") final val mimaFailOnProblem = settingKey[Boolean]("if true, fail the build on binary incompatibility detection.") final val mimaFailOnNoPrevious = settingKey[Boolean]("if true, fail the build if no previous artifacts are set.") diff --git a/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/MimaPlugin.scala b/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/MimaPlugin.scala index d74ab2af0..af76578a9 100644 --- a/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/MimaPlugin.scala +++ b/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/MimaPlugin.scala @@ -12,6 +12,7 @@ object MimaPlugin extends AutoPlugin { override def globalSettings: Seq[Def.Setting[_]] = Seq( mimaPreviousArtifacts := NoPreviousArtifacts, + mimaExcludeAnnotations := Nil, mimaBinaryIssueFilters := Nil, mimaFailOnProblem := true, mimaFailOnNoPrevious := true, @@ -51,12 +52,12 @@ object MimaPlugin extends AutoPlugin { }.toMap } }, - mimaCurrentClassfiles := (classDirectory in Compile).value, + mimaCurrentClassfiles := (Compile / classDirectory).value, mimaFindBinaryIssues := binaryIssuesIterator.value.toMap, - fullClasspath in mimaFindBinaryIssues := (fullClasspath in Compile).value, + mimaFindBinaryIssues / fullClasspath := (Compile / fullClasspath).value, mimaBackwardIssueFilters := SbtMima.issueFiltersFromFiles(mimaFiltersDirectory.value, "\\.(?:backward[s]?|both)\\.excludes".r, streams.value), mimaForwardIssueFilters := SbtMima.issueFiltersFromFiles(mimaFiltersDirectory.value, "\\.(?:forward[s]?|both)\\.excludes".r, streams.value), - mimaFiltersDirectory := (sourceDirectory in Compile).value / "mima-filters", + mimaFiltersDirectory := (Compile / sourceDirectory).value / "mima-filters", ) @deprecated("Switch to enablePlugins(MimaPlugin)", "0.7.0") @@ -75,6 +76,7 @@ object MimaPlugin extends AutoPlugin { val currClassfiles = mimaCurrentClassfiles.value val cp = (mimaFindBinaryIssues / fullClasspath).value val sv = scalaVersion.value + val excludeAnnots = mimaExcludeAnnotations.value.toList if (prevClassfiles eq NoPreviousClassfiles) { val msg = "mimaPreviousArtifacts not set, not analyzing binary compatibility" @@ -84,7 +86,7 @@ object MimaPlugin extends AutoPlugin { } prevClassfiles.iterator.map { case (moduleId, prevClassfiles) => - moduleId -> SbtMima.runMima(prevClassfiles, currClassfiles, cp, mimaCheckDirection.value, sv, log) + moduleId -> SbtMima.runMima(prevClassfiles, currClassfiles, cp, mimaCheckDirection.value, sv, log, excludeAnnots) } } diff --git a/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/SbtMima.scala b/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/SbtMima.scala index e542c825b..16d824731 100644 --- a/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/SbtMima.scala +++ b/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/SbtMima.scala @@ -18,11 +18,11 @@ import scala.util.matching._ object SbtMima { /** Runs MiMa and returns a two lists of potential binary incompatibilities, the first for backward compatibility checking, and the second for forward checking. */ - def runMima(prev: File, curr: File, cp: Classpath, dir: String, scalaVersion: String, logger: Logger): (List[Problem], List[Problem]) = { + def runMima(prev: File, curr: File, cp: Classpath, dir: String, scalaVersion: String, logger: Logger, excludeAnnots: List[String]): (List[Problem], List[Problem]) = { sanityCheckScalaVersion(scalaVersion) val mimaLib = new MiMaLib(Attributed.data(cp), new SbtLogger(logger)) - def checkBC = mimaLib.collectProblems(prev, curr) - def checkFC = mimaLib.collectProblems(curr, prev) + def checkBC = mimaLib.collectProblems(prev, curr, excludeAnnots) + def checkFC = mimaLib.collectProblems(curr, prev, excludeAnnots) dir match { case "backward" | "backwards" => (checkBC, Nil) case "forward" | "forwards" => (Nil, checkFC)