Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unpickle annotations to ignore "exclude annotations" (on Scala 3) #647

Merged
merged 11 commits into from
Aug 23, 2021
14 changes: 12 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
name: ci

on:
pull_request:
push:
branches: ['main']
tags: ['v[0-9]']
tags: ['[0-9]']

jobs:
build:
Expand All @@ -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:
Expand All @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.typesafe.tools.mima.core

private[mima] final case class AnnotInfo(name: String)
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }
Expand Down
18 changes: 6 additions & 12 deletions core/src/main/scala/com/typesafe/tools/mima/core/ClassPath.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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"))
Expand All @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.typesafe.tools.mima.core

import java.io.IOException
import java.nio.file.Files

import ClassfileConstants._

Expand Down Expand Up @@ -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) = {
Expand Down Expand Up @@ -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() = {
Expand All @@ -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() = {
Expand Down Expand Up @@ -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;"
Expand All @@ -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)
Expand All @@ -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(
Expand Down
60 changes: 27 additions & 33 deletions core/src/main/scala/com/typesafe/tools/mima/core/ConstantPool.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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")
Expand All @@ -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] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down
Loading